Browse Source

增加店铺授权和编辑功能

liujintao 1 week ago
parent
commit
0d1a9148e6

+ 4 - 4
.env.development

@@ -2,10 +2,10 @@
 ENV='development'
 
 # 本地环境接口地址
-# VITE_API_URL = 'http://127.0.0.1:8000'
-#VITE_API_URL='http://192.168.1.225:82/'
- VITE_API_URL='http://operate.zosi.com.cn/'
-# VITE_API_URL="http://192.168.1.225:82/"
+ VITE_API_URL = 'http://127.0.0.1:8000'
+# VITE_API_URL='http://192.168.1.225:82/'
+# VITE_API_URL='http://operate.zosi.com.cn/'
+# VITE_API_URL="http://192.168.1.231:82/"
 
 # 是否启用按钮权限
 VITE_PM_ENABLED=true

+ 4 - 0
src/views/store-manage/Columns.ts

@@ -31,6 +31,10 @@ export const MarketStoreColumns = [
   {
     field: 'create_datetime', title: '创建时间', width: 180, align: 'center',
     slots: { default: 'create_datetime' }
+  },
+  {
+    field: 'operate', title: '操 作', width: 70, align: 'center', fixed: 'right',
+    slots: { default: 'operate' }
   }
 ];
 

+ 17 - 0
src/views/store-manage/market-store/api.ts

@@ -20,3 +20,20 @@ export function exportData(query: any) {
     responseType: 'blob'
   });
 }
+
+export function genSpAuthLink(query: any) {
+  return request({
+    url: apiPrefix + 'auth_link/',
+    method: 'GET',
+    params: query
+  });
+}
+
+
+export function updateRow(body: any) {
+  return request({
+    url: apiPrefix + body.id + '/?partial=true',
+    method: 'PUT',
+    data: body
+  });
+}

+ 24 - 1
src/views/store-manage/market-store/component/DataTable.vue

@@ -11,6 +11,8 @@ import { useTableData } from '/@/utils/useTableData';
 import { MarketStoreColumns } from '/@/views/store-manage/Columns';
 import DataTableSlot from './DataTableSlot.vue';
 import * as api from '../api';
+import StoreAuthorization from './storeAuthorization.vue';
+import EditDrawer from './EditDrawer.vue';
 
 
 interface Parameter {
@@ -42,6 +44,7 @@ const gridOptions: any = reactive({
     size: 'large',
     // custom: true,
     slots: {
+			buttons: 'toolbar_buttons',
       tools: 'toolbar_tools'
     }
   },
@@ -98,10 +101,28 @@ function handleRefresh() {
 }
 
 defineExpose({ fetchList });
+
+const authorizeDialogVisible = ref(false);
+function handleAddStore(){
+	authorizeDialogVisible.value = true;
+}
+
+const editOpen = ref(false);
+const rowData = ref({});
+
+function handleEdit(row: any) {
+	editOpen.value = true;
+	rowData.value = row;
+}
+
 </script>
 
 <template>
   <vxe-grid ref="gridRef" v-bind="gridOptions">
+		<!-- 左侧按钮区 -->
+		<template #toolbar_buttons>
+			<el-button type="primary" @click="handleAddStore">新增店铺授权</el-button>
+		</template>
     <!-- 工具栏右侧 -->
     <template #toolbar_tools>
       <el-button circle class="toolbar-btn" @click="handleRefresh">
@@ -124,9 +145,11 @@ defineExpose({ fetchList });
     </template>
     <!-- 自定义列插槽 -->
     <template v-for="col in MarketStoreColumns" #[`${col.field}`]="{ row }">
-      <DataTableSlot :key="row.id" :field="col.field" :row="row" />
+      <DataTableSlot :key="row.id" :field="col.field" :row="row" @edit-row="handleEdit"/>
     </template>
   </vxe-grid>
+	<EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData" @refresh="handleRefresh" />
+	<StoreAuthorization v-model="authorizeDialogVisible"/>
 </template>
 
 <style scoped>

+ 18 - 0
src/views/store-manage/market-store/component/DataTableSlot.vue

@@ -7,6 +7,8 @@
 
 import { useCountryInfoStore } from '/@/stores/countryInfo';
 import { getTagType } from '/@/utils/useTagColor';
+import { Operation } from '@element-plus/icons-vue';
+import PermissionButton from '/@/components/PermissionButton/index.vue';
 
 
 const props = defineProps<{
@@ -20,6 +22,12 @@ const country = countryInfoStore.Countries.find(c => c.code == row.country_code)
 const color = country ? country.color : '#3875F6';
 const region = countryInfoStore.Region.find(r => r.code == row.region);
 const status =  row.status === "open" ? "正常" : "关闭";
+
+const emit = defineEmits([ 'edit-row']);
+
+function handleEdit() {
+	emit('edit-row', row);
+}
 </script>
 
 <template>
@@ -42,6 +50,16 @@ const status =  row.status === "open" ? "正常" : "关闭";
 				{{status }}
 			</el-tag>
 		</div>
+		<div v-else-if="field === 'operate'">
+			<el-tooltip :enterable="false" :show-arrow="false" content="评论详情" hide-after="0"
+									placement="top" popper-class="custom-btn-tooltip">
+				<PermissionButton circle plain type="warning" @click="handleEdit">
+					<el-icon>
+						<Operation />
+					</el-icon>
+				</PermissionButton>
+			</el-tooltip>
+		</div>
     <!-- 动态获取 -->
     <div v-else>
       {{ row[field] || '-' }}

+ 166 - 0
src/views/store-manage/market-store/component/EditDrawer.vue

@@ -0,0 +1,166 @@
+<script lang="ts" setup>
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { Close, Finished } from '@element-plus/icons-vue';
+import * as api from '../api';
+import { useResponse } from '/@/utils/useResponse';
+import { hasPermission } from '/@/utils/hasPermission';
+
+// 接口定义
+interface RuleForm {
+	status: string;
+	platform_number: string;
+	name: string;
+}
+
+interface Props {
+	rowData?: {
+		id: string | number;
+		platform_number: string;
+		name: string;
+		status: string;
+	};
+}
+
+// Props 和 Emits
+const props = defineProps<Props>();
+const emit = defineEmits<{
+	refresh: [];
+}>();
+
+// 响应式数据
+const btnLoading = ref(false);
+const editOpen = defineModel({ default: false });
+const editDrawer = useTemplateRef<any>('editDrawer');
+const ruleFormRef = ref<FormInstance>();
+
+// 表单数据初始化
+const initFormData = (): RuleForm => ({
+	platform_number: props.rowData?.platform_number ?? '',
+	name: props.rowData?.name ?? '',
+	status: props.rowData?.status ?? '',
+});
+
+const ruleForm = reactive<RuleForm>(initFormData());
+
+// 表单验证规则
+const rules = reactive<FormRules<RuleForm>>({
+	name: [
+		{ required: true, message: '请输入店铺名称', trigger: 'blur' },
+		{ min: 2, max: 50, message: '店铺名称长度在 2 到 50 个字符', trigger: 'blur' },
+	],
+	platform_number: [
+		{ required: true, message: '请输入平台编号', trigger: 'blur' },
+		{ pattern: /^[A-Za-z0-9_-]+$/, message: '平台编号只能包含字母、数字、下划线和短横线', trigger: 'blur' },
+	],
+	status: [{ required: true, message: '请选择店铺状态', trigger: 'change' }],
+});
+
+// 店铺状态选项
+const statusOptions = [
+	{ label: '启用', value: 'open' },
+	{ label: '关闭', value: 'closed' },
+];
+
+// 提交表单
+const submitForm = async (formEl?: FormInstance) => {
+	if (!formEl || !props.rowData?.id) return;
+
+	try {
+		const valid = await formEl.validate();
+		if (!valid) return;
+
+		btnLoading.value = true;
+
+		const res = await useResponse(api.updateRow, { id: props.rowData.id, ...ruleForm }, btnLoading);
+
+		if (res?.code === 2000) {
+			editOpen.value = false;
+			ElMessage.success({
+				message: '编辑成功',
+				plain: true,
+				icon: 'Operation',
+			});
+			emit('refresh');
+			resetForm();
+		}
+	} catch (error) {
+		console.error('编辑失败:', error);
+		ElMessage.error('编辑失败,请重试');
+	} finally {
+		btnLoading.value = false;
+	}
+};
+
+// 重置表单
+const resetForm = () => {
+	ruleFormRef.value?.clearValidate();
+	Object.assign(ruleForm, initFormData());
+};
+
+// 关闭抽屉
+const closeDrawer = () => {
+	editDrawer.value?.handleClose();
+	resetForm();
+};
+
+// 监听抽屉打开状态,重新初始化表单数据
+watch(editOpen, (newVal) => {
+	if (newVal && props.rowData) {
+		Object.assign(ruleForm, initFormData());
+		nextTick(() => {
+			ruleFormRef.value?.clearValidate();
+		});
+	}
+});
+</script>
+
+<template>
+	<div class="drawer-container">
+		<el-drawer
+			ref="editDrawer"
+			v-model="editOpen"
+			:close-on-click-modal="false"
+			:close-on-press-escape="false"
+			size="30%"
+			title="市场店铺 - 编辑"
+			destroy-on-close
+		>
+			<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" class="mx-2.5 mt-7" label-position="top" label-width="auto" status-icon>
+				<el-form-item class="font-medium" label="店铺名称:" prop="name">
+					<el-input v-model="ruleForm.name" placeholder="请输入店铺名称" maxlength="50" show-word-limit clearable />
+				</el-form-item>
+
+				<el-form-item class="font-medium" label="平台编号:" prop="platform_number">
+					<el-input v-model="ruleForm.platform_number" placeholder="请输入平台编号" clearable />
+				</el-form-item>
+
+				<div v-if="hasPermission('shopStatusEdit')">
+					<el-form-item class="font-medium" label="店铺状态:" prop="status">
+						<el-select v-model="ruleForm.status" placeholder="请选择店铺状态" clearable class="w-full">
+							<el-option v-for="option in statusOptions" :key="option.value" :label="option.label" :value="option.value" />
+						</el-select>
+					</el-form-item>
+				</div>
+
+				<el-form-item>
+					<el-divider />
+					<div class="flex flex-1 justify-end gap-3">
+						<el-button :icon="Close" @click="closeDrawer"> 取 消 </el-button>
+						<el-button :icon="Finished" :loading="btnLoading" type="primary" @click="submitForm(ruleFormRef)"> 确 定 </el-button>
+					</div>
+				</el-form-item>
+			</el-form>
+		</el-drawer>
+	</div>
+</template>
+
+<style scoped>
+.drawer-container :deep(.el-drawer__header) {
+	border-bottom: none;
+	font-weight: 500;
+}
+
+.drawer-container :deep(.el-drawer__title) {
+	font-size: 18px;
+}
+</style>

+ 478 - 0
src/views/store-manage/market-store/component/storeAuthorization.vue

@@ -0,0 +1,478 @@
+<script setup lang="ts">
+import useClipboard from 'vue-clipboard3';
+import { ElMessage } from 'element-plus';
+import { genSpAuthLink } from '/@/views/store-manage/market-store/api';
+import { Check, CircleCheck, Key, Shop, Warning, Link } from '@element-plus/icons-vue';
+
+
+const { toClipboard } = useClipboard();
+const authorizeDialogVisible = defineModel();
+
+const formData = ref({
+	countryCode: 'US',
+	accountName: '',
+	platformCode: '',
+});
+
+const authType = ref('sp');
+const authLink = ref('');
+const isGenerating = ref(false);
+const isCopying = ref(false);
+
+const countryCodeOptions = [
+	// 美洲
+	{ label: '美国', value: 'US', flag: '🇺🇸' },
+	{ label: '加拿大', value: 'CA', flag: '🇨🇦' },
+	{ label: '墨西哥', value: 'MX', flag: '🇲🇽' },
+
+	// 欧洲
+	{ label: '英国', value: 'UK', flag: '🇬🇧' },  // 注意:UK是保留代码,正式ISO代码是GB
+	{ label: '德国', value: 'DE', flag: '🇩🇪' },
+	{ label: '法国', value: 'FR', flag: '🇫🇷' },
+	{ label: '意大利', value: 'IT', flag: '🇮🇹' },
+	{ label: '西班牙', value: 'ES', flag: '🇪🇸' },
+	{ label: '荷兰', value: 'NL', flag: '🇳🇱' },
+	{ label: '瑞典', value: 'SE', flag: '🇸🇪' },
+	{ label: '瑞士', value: 'CH', flag: '🇨🇭' },
+	{ label: '比利时', value: 'BE', flag: '🇧🇪' },
+	{ label: '波兰', value: 'PL', flag: '🇵🇱' },
+
+	// 其他地区
+	{ label: '日本', value: 'JP', flag: '🇯🇵' },
+	{ label: '澳大利亚', value: 'AU', flag: '🇦🇺' },
+	{ label: '新加坡', value: 'SG', flag: '🇸🇬' },
+	{ label: '沙特阿拉伯', value: 'SA', flag: '🇸🇦' },
+	{ label: '阿联酋', value: 'AE', flag: '🇦🇪' }
+];
+
+
+// 表单验证规则
+const formRules = {
+	accountName: [
+		{ required: true, message: '请输入店铺名称', trigger: 'blur' },
+		{ min: 2, max: 50, message: '店铺名称长度在 2 到 50 个字符', trigger: 'blur' },
+	],
+	platformCode: [
+		{ required: true, message: '请输入平台编号', trigger: 'blur' },
+		{ pattern: /^[A-Za-z0-9-_]+$/, message: '平台编号只能包含字母、数字、短划线和下划线', trigger: 'blur' },
+	],
+	countryCode: [{ required: true, message: '请选择店铺区域', trigger: 'change' }],
+};
+
+// 生成授权链接
+const genAuthLink = async () => {
+	const form = formRef.value;
+	if (!form) return;
+
+	try {
+		const valid = await form.validate();
+		if (!valid) return;
+
+		isGenerating.value = true;
+
+		if (authType.value === 'sp') {
+			const resp = await genSpAuthLink(formData.value);
+			authLink.value = resp.data;
+
+			ElMessage({
+				message: '授权链接生成成功!',
+				type: 'success',
+			});
+		}
+	} catch (error) {
+		console.error('生成授权链接失败:', error);
+		ElMessage({
+			message: error.message || '生成授权链接失败,请重试',
+			type: 'error',
+		});
+	} finally {
+		isGenerating.value = false;
+	}
+};
+
+// 复制链接
+const copyLink = async () => {
+	if (!authLink.value) return;
+
+	try {
+		isCopying.value = true;
+		await toClipboard(authLink.value);
+
+		ElMessage({
+			message: '链接复制成功!',
+			type: 'success',
+			duration: 2000,
+		});
+	} catch (error) {
+		console.error('复制失败:', error);
+		ElMessage({
+			message: '复制失败,请手动复制',
+			type: 'error',
+		});
+	} finally {
+		isCopying.value = false;
+	}
+};
+
+const formRef = ref(null);
+
+// 重置表单
+const resetForm = () => {
+	authLink.value = '';
+	isGenerating.value = false;
+	isCopying.value = false;
+	formRef.value?.resetFields();
+};
+
+// 表单是否有效
+const isFormValid = computed(() => {
+	return formData.value.accountName.trim() !== '' && formData.value.platformCode.trim() !== '' && formData.value.countryCode;
+});
+
+// 在浏览器中打开链接
+const openInBrowser = () => {
+	if (authLink.value) {
+		window.open(authLink.value, '_blank');
+	}
+};
+</script>
+
+<template>
+	<el-dialog
+		v-model="authorizeDialogVisible"
+		width="520px"
+		:destroy-on-close="true"
+		@closed="resetForm"
+		center
+		class="auth-dialog"
+		:close-on-click-modal="false"
+	>
+		<template #title>
+			<div class="dialog-header">
+				<span class="title-text">
+					{{ authType === 'sp' ? '亚马逊店铺SP授权' : '广告授权' }}
+				</span>
+			</div>
+		</template>
+
+		<div class="dialog-content">
+			<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" class="auth-form" @submit.prevent>
+				<el-form-item label="店铺名称" prop="accountName">
+					<el-input v-model="formData.accountName" placeholder="请输入您的亚马逊店铺名称,例如:Amazon-ASJ-AE" clearable maxlength="50" show-word-limit>
+						<template #prefix>
+							<el-icon>
+								<Shop />
+							</el-icon>
+						</template>
+					</el-input>
+				</el-form-item>
+
+				<el-form-item label="平台编号" prop="platformCode">
+					<el-input v-model="formData.platformCode" placeholder="请输入平台分配的唯一编号,例如:ZS86" clearable maxlength="30">
+						<template #prefix>
+							<el-icon>
+								<Key />
+							</el-icon>
+						</template>
+					</el-input>
+				</el-form-item>
+
+				<el-form-item label="店铺区域" prop="countryCode">
+					<el-select v-model="formData.countryCode" placeholder="请选择店铺所在区域" style="width: 100%" clearable>
+						<el-option v-for="option in countryCodeOptions" :key="option.value" :label="option.label" :value="option.value">
+							<span class="option-content">
+								<span class="flag">{{ option.flag }}</span>
+								<span>{{ option.label }}</span>
+							</span>
+						</el-option>
+					</el-select>
+				</el-form-item>
+			</el-form>
+
+			<div class="action-area">
+				<el-button type="primary" @click="genAuthLink" :disabled="!isFormValid" :loading="isGenerating" class="generate-btn" size="large">
+					<el-icon v-if="!isGenerating">
+						<Link />
+					</el-icon>
+					{{ isGenerating ? '生成中...' : '生成授权链接' }}
+				</el-button>
+
+				<div v-show="authLink" class="link-area">
+					<el-divider>
+						<el-icon color="#67c23a">
+							<Check />
+						</el-icon>
+					</el-divider>
+
+					<div class="success-tip">
+						<el-icon color="#67c23a" size="20">
+							<CircleCheck />
+						</el-icon>
+						<span>授权链接已生成!请在常用浏览器中打开进行授权</span>
+					</div>
+
+					<div class="link-container">
+						<el-input :model-value="authLink" readonly class="link-input">
+							<template #prefix>
+								<el-icon>
+									<Link />
+								</el-icon>
+							</template>
+						</el-input>
+
+						<div class="link-actions">
+							<el-button type="success" @click="copyLink" :loading="isCopying" icon="DocumentCopy" class="action-btn">
+								{{ isCopying ? '复制中...' : '复制' }}
+							</el-button>
+
+							<el-button type="primary" @click="openInBrowser" icon="TopRight" class="action-btn"> 打开 </el-button>
+						</div>
+					</div>
+
+					<div class="warning-tip">
+						<el-icon color="#e6a23c">
+							<Warning />
+						</el-icon>
+						<span>请确保在安全的网络环境下进行授权操作</span>
+					</div>
+				</div>
+			</div>
+		</div>
+	</el-dialog>
+</template>
+
+<style scoped>
+.auth-dialog {
+	border-radius: 12px;
+	box-shadow: 0 12px 32px 4px rgba(0, 0, 0, 0.12);
+}
+
+.auth-dialog :deep(.el-dialog__header) {
+	padding: 24px 24px 0;
+	border-bottom: none;
+}
+
+.auth-dialog :deep(.el-dialog__body) {
+	padding: 0 24px 24px;
+}
+
+.dialog-header {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	gap: 12px;
+}
+
+.header-icon {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 40px;
+	height: 40px;
+	background: linear-gradient(135deg, #409eff, #67c23a);
+	border-radius: 50%;
+	color: white;
+}
+
+.title-text {
+	font-size: 18px;
+	font-weight: 600;
+	color: #303133;
+}
+
+.dialog-content {
+	margin-top: 20px;
+}
+
+.auth-form {
+	margin-bottom: 24px;
+}
+
+.auth-form :deep(.el-form-item__label) {
+	font-weight: 600;
+	color: #606266;
+	margin-bottom: 8px;
+}
+
+.auth-form :deep(.el-form-item) {
+	margin-bottom: 24px;
+}
+
+.auth-form :deep(.el-input__wrapper) {
+	border-radius: 8px;
+	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+	transition: all 0.3s ease;
+}
+
+.auth-form :deep(.el-input__wrapper:hover) {
+	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.auth-form :deep(.el-select .el-input__wrapper) {
+	border-radius: 8px;
+}
+
+.option-content {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+}
+
+.flag {
+	font-size: 16px;
+}
+
+.action-area {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+}
+
+.generate-btn {
+	width: 100%;
+	height: 48px;
+	font-size: 16px;
+	font-weight: 600;
+	border-radius: 8px;
+	background: linear-gradient(135deg, #409eff, #67c23a);
+	border: none;
+	box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+	transition: all 0.3s ease;
+}
+
+.generate-btn:hover:not(:disabled) {
+	transform: translateY(-2px);
+	box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
+}
+
+.generate-btn:disabled {
+	background: #c0c4cc;
+	box-shadow: none;
+	cursor: not-allowed;
+}
+
+.link-area {
+	width: 100%;
+	margin-top: 24px;
+	animation: fadeInUp 0.5s ease;
+}
+
+@keyframes fadeInUp {
+	from {
+		opacity: 0;
+		transform: translateY(20px);
+	}
+	to {
+		opacity: 1;
+		transform: translateY(0);
+	}
+}
+
+.success-tip {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	gap: 8px;
+	margin-bottom: 16px;
+	padding: 12px;
+	background: linear-gradient(135deg, #f0f9ff, #e6f7ff);
+	border-radius: 8px;
+	color: #52c41a;
+	font-weight: 500;
+	border: 1px solid #b7eb8f;
+}
+
+.link-container {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+	margin-bottom: 16px;
+}
+
+.link-input :deep(.el-input__wrapper) {
+	background-color: #f8fafc;
+	border: 1px dashed #d1d5db;
+	border-radius: 8px;
+}
+
+.link-actions {
+	display: flex;
+	gap: 8px;
+	justify-content: center;
+}
+
+.action-btn {
+	flex: 1;
+	height: 40px;
+	border-radius: 8px;
+	font-weight: 500;
+	transition: all 0.3s ease;
+}
+
+.action-btn:hover {
+	transform: translateY(-1px);
+	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.warning-tip {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	gap: 6px;
+	padding: 8px 12px;
+	background: #fdf6ec;
+	border-radius: 6px;
+	color: #e6a23c;
+	font-size: 13px;
+	border: 1px solid #f5dab1;
+}
+
+.el-divider {
+	margin: 20px 0 16px;
+}
+
+.el-divider :deep(.el-divider__text) {
+	background: white;
+	padding: 0 12px;
+}
+
+/* 响应式设计 */
+@media (max-width: 480px) {
+	.auth-dialog {
+		width: 90vw !important;
+		margin: 0;
+	}
+
+	.link-actions {
+		flex-direction: column;
+	}
+
+	.action-btn {
+		width: 100%;
+	}
+}
+
+/* 暗色模式支持 */
+@media (prefers-color-scheme: dark) {
+	.dialog-header .title-text {
+		color: #e5eaf3;
+	}
+
+	.success-tip {
+		background: linear-gradient(135deg, #0c1420, #1a2332);
+		border-color: #2d5a27;
+		color: #73d13d;
+	}
+
+	.warning-tip {
+		background: #2d1b12;
+		border-color: #594214;
+		color: #faad14;
+	}
+
+	.link-input :deep(.el-input__wrapper) {
+		background-color: #1d2329;
+		border-color: #4c5155;
+	}
+}
+</style>