WanGxC 7 miesięcy temu
rodzic
commit
23faf9d99b

+ 2 - 0
src/auto-imports.d.ts

@@ -7,6 +7,8 @@
 export {}
 declare global {
   const EffectScope: typeof import('vue')['EffectScope']
+  const ElMessage: typeof import('element-plus/es')['ElMessage']
+  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
   const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
   const computed: typeof import('vue')['computed']
   const createApp: typeof import('vue')['createApp']

+ 72 - 55
src/components/ImportButton/index.vue

@@ -6,21 +6,26 @@
  */
 import { ButtonProps, genFileId, UploadInstance, UploadRawFile } from 'element-plus';
 import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
-
+import { uploadFile } from '/@/views/product-manage/product-list/api';
 
 const { data } = BtnPermissionStore();
 
 const attrs = useAttrs() as any;
+const refreshView = inject('refreshView');
 
-const props = defineProps<Partial<Omit<ButtonProps, 'disabled' | 'loading' | 'color'>>>();
+const props = defineProps<
+	{
+		uploadFunction: (url: 'URL') => Promise<any>;
+	} & Partial<Omit<ButtonProps, 'disabled' | 'loading' | 'color'>>
+>();
 
 function hasPermission(permissions: string | string[]): boolean {
-  if (typeof permissions === 'string') {
-    return data.includes(permissions);
-  } else if (Array.isArray(permissions)) {
-    return permissions.every(permission => data.includes(permission));
-  }
-  return false;
+	if (typeof permissions === 'string') {
+		return data.includes(permissions);
+	} else if (Array.isArray(permissions)) {
+		return permissions.every((permission) => data.includes(permission));
+	}
+	return false;
 }
 
 const upload = ref<UploadInstance>();
@@ -31,11 +36,11 @@ const upBtnLoading = ref(false);
  * @param files 文件列表
  */
 function handleExceed(files: any) {
-  upload.value!.clearFiles();
-  const file = files[0] as UploadRawFile;
-  file.uid = genFileId();
-  upload.value!.handleStart(file);
-  upload.value!.submit();
+	upload.value!.clearFiles();
+	const file = files[0] as UploadRawFile;
+	file.uid = genFileId();
+	upload.value!.handleStart(file);
+	upload.value!.submit();
 }
 
 /**
@@ -43,19 +48,24 @@ function handleExceed(files: any) {
  * @param uploadRequest 上传请求
  */
 async function handleCustomUpload(uploadRequest: any) {
-  upBtnLoading.value = true;
-  try {
-    const { file } = uploadRequest;
-    // const response = await api.uploadFile(file);
-    // handleResponse(response);
-    // processResponseData(response.data);
-    // uploadRequest.onSuccess(response); // 通知 el-upload 上传成功
-  } catch (error) {
-    console.log('==Error==', error);
-    uploadRequest.onError(error);
-  } finally {
-    upBtnLoading.value = false;
-  }
+	upBtnLoading.value = true;
+	try {
+		const { file } = uploadRequest;
+		const formData = new FormData();
+		formData.append('file', file);
+		const resp = await uploadFile(formData);
+		const fileUrl = resp.data.url;
+		const response = await props.uploadFunction({ url: fileUrl });
+		handleResponse(response);
+		// processResponseData(response.data);
+		uploadRequest.onSuccess(response); // 通知 el-upload 上传成功
+
+	} catch (error) {
+		console.log('==Error==', error);
+		uploadRequest.onError(error);
+	} finally {
+		upBtnLoading.value = false;
+	}
 }
 
 /**
@@ -63,38 +73,45 @@ async function handleCustomUpload(uploadRequest: any) {
  * @param response 后端返回的响应
  */
 function handleResponse(response: any) {
-  // if (response.code === SUCCESS_CODE) {
-  //   ElMessage.success({ message: response.msg, plain: true });
-  // } else if (response.code === WARNING_CODE) {
-  //   ElMessage.warning({ message: response.msg, plain: true });
-  // } else {
-  //   ElMessage.error({ message: response.msg, plain: true });
-  // }
+	if (response.code === 2000) {
+		ElMessage.success({ message: response.msg, plain: true });
+
+		// 上传成功后调用刷新父组件的方法
+		if (refreshView) {
+			refreshView();
+		}
+	} else {
+		ElMessage.error({ message: response.msg, plain: true });
+	}
 }
 </script>
 
 <template>
-  <div>
-    <el-upload
-        ref="upload"
-        action="#"
-        :limit="1"
-        :show-file-list="false"
-        :auto-upload="true"
-        :on-exceed="handleExceed"
-        :http-request="handleCustomUpload">
-      <template #trigger>
-        <el-button v-if="attrs.show ? hasPermission(attrs.show) : true"
-                   :disabled="attrs.permissions ? !hasPermission(attrs.permissions) : false"
-                   :loading="upBtnLoading" :color="attrs.myColor" v-bind="props">
-                   <!--:loading="upBtnLoading" color="#6366f1" v-bind="props">-->
-          <slot></slot>
-        </el-button>
-      </template>
-    </el-upload>
-  </div>
+	<div>
+		<el-upload
+			ref="upload"
+			action="#"
+			:limit="1"
+			:show-file-list="false"
+			:auto-upload="true"
+			:on-exceed="handleExceed"
+			:http-request="handleCustomUpload"
+			accept=".xls,.xlsx"
+		>
+			<template #trigger>
+				<el-button
+					v-if="attrs.show ? hasPermission(attrs.show) : true"
+					:disabled="attrs.permissions ? !hasPermission(attrs.permissions) : false"
+					:loading="upBtnLoading"
+					:color="attrs.myColor"
+					v-bind="props"
+				>
+					<!--:loading="upBtnLoading" color="#6366f1" v-bind="props">-->
+					<slot></slot>
+				</el-button>
+			</template>
+		</el-upload>
+	</div>
 </template>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 1 - 1
src/utils/service.ts

@@ -173,7 +173,7 @@ function createRequestFunction(service: any) {
 			headers: {
 				'Content-Type': get(config, 'headers.Content-Type', 'application/json'),
 			},
-			timeout: 5000,
+			timeout: 120000,
 			baseURL: getBaseURL(),
 			data: {},
 		};

+ 16 - 0
src/views/product-manage/competitor-monitor/index.vue

@@ -0,0 +1,16 @@
+<script setup lang="ts">
+/**
+ * @Name: index.vue
+ * @Description:
+ * @Author: xinyan
+ */
+
+</script>
+
+<template>
+
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 46 - 0
src/views/product-manage/product-list/api.ts

@@ -79,3 +79,49 @@ export function updateShopDetail(body: any) {
     data: body.formData
   });
 }
+
+export function uploadFile(body: any){
+  return request({
+    url: 'api/system/file/',
+    method: 'POST',
+    data: body,
+    headers: { 'Content-Type':'multipart/form-data' },
+  });
+}
+
+// 商品变更通知导入
+export function uploadChangeNotice(body: any){
+  return request({
+    url: '/api/choice/goods/alarm/import_data/',
+    method: 'POST',
+    data: body,
+  });
+}
+
+// 商品导入
+export function uploadProducts(body: any){
+  return request({
+    url: '/api/choice/goods/import_data/',
+    method: 'POST',
+    data: body,
+  });
+}
+
+// 指导价格导入
+export function uploadPrice(body: any){
+  return request({
+    url: '/api/choice/goods/GuidancePrice/import_data/',
+    method: 'POST',
+    data: body,
+  });
+}
+
+// 导出
+export function exportData(query) {
+  return request({
+    url: '/api/choice/goods/export_data/',
+    method: 'GET',
+    params: query,
+    responseType: 'blob'
+  });
+}

+ 40 - 4
src/views/product-manage/product-list/component/DataTable.vue

@@ -105,6 +105,42 @@ function handleRefresh() {
   fetchList();
 }
 
+async function handleDownload() {
+	const confirmed = await ElMessageBox.confirm('是否确认导出当前时间内所有数据项?', '警告', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning'
+	});
+
+	if (confirmed) {
+		gridOptions.loading = true;
+		try {
+			const query = {
+				country_code: queryParameter?.country,
+				brand: queryParameter?.brand,
+				tag: queryParameter?.group,
+				status: queryParameter?.status,
+				asin: queryParameter?.asin,
+				sku: queryParameter?.sku,
+				shop_id: queryParameter?.shop
+			};
+			const response = await api.exportData(query);
+			const url = window.URL.createObjectURL(new Blob([response.data]));
+			const link = document.createElement('a');
+			link.href = url;
+			link.setAttribute('download', '商品列表数据.xlsx');
+			document.body.appendChild(link);
+			link.click();
+			ElMessage.success('数据导出成功!');
+		} catch (error) {
+			ElMessage.error('数据导出失败,请重试!');
+			console.error(error);
+		} finally {
+			gridOptions.loading = false; // 结束加载状态
+		}
+	}
+}
+
 function selectChangeEvent({ checked, row }: any) {
   if (checked) {
     checkedList.value.add(row.id); // 获取单个数据
@@ -211,12 +247,12 @@ defineExpose({ fetchList });
         </div>
         <VerticalDivider class="px-1" style="margin-left: 7px;" />
 
-        <ImportButton :icon="Message" bg text>变更通知导入</ImportButton>
-        <ImportButton bg text>
+        <ImportButton :icon="Message" bg text :uploadFunction="api.uploadChangeNotice">变更通知导入</ImportButton>
+        <ImportButton bg text :uploadFunction="api.uploadProducts">
           <i class="bi bi-box-seam mr-3"></i>
           商品导入
         </ImportButton>
-        <ImportButton :icon="Money" bg text>指导价格导入</ImportButton>
+        <ImportButton :icon="Money" bg text :uploadFunction="api.uploadPrice">指导价格导入</ImportButton>
       </div>
     </template>
     <!-- 工具栏右侧 -->
@@ -226,7 +262,7 @@ defineExpose({ fetchList });
           <Refresh />
         </el-icon>
       </el-button>
-      <el-button circle class="mr-3 toolbar-btn">
+      <el-button circle class="mr-3 toolbar-btn" @click="handleDownload">
         <el-icon>
           <Download />
         </el-icon>

+ 27 - 0
src/views/product-manage/product-monitor/api.ts

@@ -11,6 +11,14 @@ export function getTableData(query: any) {
   });
 }
 
+export function createProductMonitor(body: any) {
+  return request({
+    url: '/api/choice/reviews_monitor/',
+    method: 'POST',
+    data: body
+  });
+}
+
 export function getGroupOptions(query: any) {
   return request({
     url: apiPrefix + 'goods/tags/',
@@ -60,3 +68,22 @@ export function updateShopDetail(body: any) {
     data: body.formData
   });
 }
+
+// 导入
+export function upload(body: any){
+  return request({
+    url: '/api/choice/reviews_monitor/import_data/',
+    method: 'POST',
+    data: body,
+  });
+}
+
+// 导出
+export function exportData(query) {
+  return request({
+    url: '/api/choice/reviews_monitor/export_data/',
+    method: 'GET',
+    params: query,
+    responseType: 'blob'
+  });
+}

+ 55 - 6
src/views/product-manage/product-monitor/component/DataTable.vue

@@ -13,7 +13,7 @@ import ImportButton from '/src/components/ImportButton/index.vue';
 import VerticalDivider from '/src/components/VerticalDivider/index.vue';
 import { productColumns } from '../ColumnsTsx';
 import DataTableSlot from '/@/views/product-manage/product-monitor/component/DataTableSlot.vue';
-import { deleteRow } from '../api';
+import CreateDialog from '/@/views/product-manage/product-monitor/component/createDialog.vue';
 import { ElMessage } from 'element-plus';
 
 
@@ -74,6 +74,7 @@ const gridOptions: any = reactive({
 const checkedList = ref<Set<number>>(new Set());
 
 const editOpen = ref(false);
+const createOpen = ref(false);
 const rowData = ref({});
 
 const dialogVisible = ref(false);
@@ -113,6 +114,46 @@ function handleRefresh() {
   fetchList();
 }
 
+async function handleDownload() {
+	const confirmed = await ElMessageBox.confirm('是否确认导出当前时间内所有数据项?', '警告', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning'
+	});
+
+	if (confirmed) {
+		gridOptions.loading = true;
+		try {
+			const query = {
+				country_code: queryParameter?.country,
+				goods__brand: queryParameter?.brand,
+				goods__tag: queryParameter?.group,
+				status: queryParameter?.status,
+				shop_id: queryParameter?.shop,
+				asin: queryParameter?.asin,
+				goods__sku: queryParameter?.sku,
+				platform_number: queryParameter?.platformId,
+				goods__all_ratings: queryParameter?.scoreNumber,
+				goods__all_reviews: queryParameter?.commentNumber,
+				goods__all_score: queryParameter?.displayScore
+			};
+			const response = await api.exportData(query);
+			const url = window.URL.createObjectURL(new Blob([response.data]));
+			const link = document.createElement('a');
+			link.href = url;
+			link.setAttribute('download', '商品监控数据.xlsx');
+			document.body.appendChild(link);
+			link.click();
+			ElMessage.success('数据导出成功!');
+		} catch (error) {
+			ElMessage.error('数据导出失败,请重试!');
+			console.error(error);
+		} finally {
+			gridOptions.loading = false; // 结束加载状态
+		}
+	}
+}
+
 async function batchOpen() {
   const ids = Array.from(checkedList.value);
   await useResponse(api.updateShopDetail, { ids, status: 1 });
@@ -153,6 +194,13 @@ async function singleDelete(row: any) {
     ElMessage.success({ message: '删除成功', plain: true });
     handleRefresh();
   }
+function handleCreate() {
+	createOpen.value = true;
+}
+
+function singleDelete(row: any) {
+  // dialogVisible.value = true;
+  rowData.value = row;
 }
 
 function downloadTemplate() {
@@ -164,9 +212,9 @@ defineExpose({ fetchList });
 </script>
 
 <template>
-  <vxe-grid ref="gridRef" :auto-resize="true"
+  <vxe-grid ref="gridRef" v-bind="gridOptions"
+            :auto-resize="true"
             :sync-resize="true"
-            v-bind="gridOptions"
             @checkbox-change="selectChangeEvent"
             @checkbox-all="selectAllChangeEvent">
     <template #toolbar_buttons>
@@ -174,7 +222,7 @@ defineExpose({ fetchList });
         <PermissionButton :disabled="!checkedList.size" :icon="Delete" plain round type="danger" @click="batchOpen">
           批量删除
         </PermissionButton>
-        <PermissionButton :icon="Plus" plain round type="primary" @click="batchOpen">
+        <PermissionButton :icon="Plus" plain round type="primary" @click="handleCreate">
           新 增
         </PermissionButton>
         <div class="custom-el-input">
@@ -194,7 +242,7 @@ defineExpose({ fetchList });
           </el-select>
         </div>
         <VerticalDivider class="px-1" style="margin-left: 7px;" />
-        <ImportButton :icon="Upload" bg text>导 入</ImportButton>
+        <ImportButton :uploadFunction="api.upload" :icon="Upload" bg text>导 入</ImportButton>
       </div>
     </template>
     <template #toolbar_tools>
@@ -203,7 +251,7 @@ defineExpose({ fetchList });
           <Refresh />
         </el-icon>
       </el-button>
-      <el-button circle class="mr-3 toolbar-btn">
+      <el-button circle class="mr-3 toolbar-btn" @click="handleDownload">
         <el-icon>
           <Download />
         </el-icon>
@@ -227,6 +275,7 @@ defineExpose({ fetchList });
     </template>
   </vxe-grid>
   <EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData" @refresh="handleRefresh" />
+	<CreateDialog v-if="createOpen" v-model="createOpen" @refresh="fetchList" />
 </template>
 
 <style scoped>

+ 154 - 0
src/views/product-manage/product-monitor/component/createDialog.vue

@@ -0,0 +1,154 @@
+<script lang="ts" setup>
+/**
+ * @Name: EditDrawer.vue
+ * @Description: 商品监控-行编辑
+ * @Author: Cheney
+ */
+
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { DictionaryStore } from '/@/stores/dictionary';
+import * as api from '../api';
+
+const shopOptions = <Ref>inject('shopOptions');
+const groupOptions = <Ref>inject('groupOptions');
+const { data: staticData } = DictionaryStore();
+
+const loading = ref(false);
+const createOpen = defineModel({ default: false });
+
+const emit = defineEmits(['refresh']);
+
+interface RuleForm {
+	asin: any;
+	sku: any;
+	country: any;
+	shop: any;
+	group: any;
+	// status: any;
+	frequency: any;
+	description: any;
+}
+
+const ruleFormRef = ref<FormInstance>();
+const ruleForm = reactive<RuleForm>({
+	asin: '',
+	sku: '',
+	country: '',
+	shop: '',
+	group: '',
+	// status: '',
+	frequency: 6,
+	description: '',
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+	asin: [{ required: true, message: '请输入ASIN', trigger: 'blur' }],
+	sku: [{ required: true, message: '请输入SKU', trigger: 'blur' }],
+	country: [{ required: true, message: '请选择国家', trigger: 'change' }],
+	shop: [{ required: true, message: '请输入店铺', trigger: 'blur' }],
+	group: [{ required: true, message: '请输入分组', trigger: 'blur' }],
+	status: [{ message: '请选择状态', trigger: 'blur' }],
+	// frequency: [ { message: '请选择更新频率', trigger: 'blur' } ]
+});
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate(async (valid, fields) => {
+		if (valid) {
+			const body = {
+				asin: ruleForm.asin,
+				sku: ruleForm.sku,
+				country_code: ruleForm.country,
+				shop_id: ruleForm.shop,
+				tag: ruleForm.group,
+				goods: {
+					sku: ruleForm.sku,
+					tag: ruleForm.group,
+				},
+				freq: ruleForm.frequency,
+				description: ruleForm.description,
+			};
+			const res = await useResponse(api.createProductMonitor, body, loading);
+			if (res.code === 2000){
+				ElMessage.success('创建成功');
+				createOpen.value = false;
+				emit('refresh');
+			}
+		} else {
+			createOpen.value = false;
+			ElMessage.error('创建失败,请检查表单');
+		}
+	});
+};
+
+const resetForm = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+};
+</script>
+
+<template>
+	<el-dialog v-model="createOpen" :close-on-click-modal="false" :close-on-press-escape="false" :title="`商品监控 - 创建 `" style="width: 40%">
+		<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-position="top" label-width="auto" class="mx-2.5 mt-5" status-icon>
+			<el-row :gutter="20">
+				<el-col :span="24">
+					<el-form-item class="font-medium" label="ASIN" prop="asin">
+						<el-input v-model="ruleForm.asin" placeholder="不再支持批量创建(批量创建使用Excel导入),请填写单个asin" />
+					</el-form-item>
+				</el-col>
+
+				<el-col :span="12">
+					<el-form-item class="font-medium" label="SKU" prop="sku">
+						<el-input v-model="ruleForm.sku" placeholder="请输入SKU" />
+					</el-form-item>
+				</el-col>
+
+				<el-col :span="12">
+					<el-form-item class="font-medium" label="店 铺" prop="shop">
+						<el-select v-model="ruleForm.shop" placeholder="请选择店铺">
+							<el-option v-for="item in shopOptions" :key="item.id" :label="item.name" :value="item.id" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+
+				<el-col :span="12">
+					<el-form-item class="font-medium" label="分 组" prop="group">
+						<el-select v-model="ruleForm.group" placeholder="请选择分组">
+							<el-option v-for="item in groupOptions" :label="item.tag" :value="item.tag" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+
+				<el-col :span="12">
+					<el-form-item class="font-medium" label="国 家" prop="country">
+						<el-select v-model="ruleForm.country" placeholder="请选择国家">
+							<el-option v-for="item in staticData.country_code" :key="item.value" :label="item.label" :value="item.value" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+
+				<el-col :span="12">
+					<el-form-item class="font-medium" label="更新频率" prop="frequency">
+						<el-input-number v-model="ruleForm.frequency" min="3" />
+					</el-form-item>
+				</el-col>
+
+				<el-col :span="12">
+					<el-form-item class="font-medium" label="备注" prop="description">
+						<el-input v-model="ruleForm.description" type="textarea" placeholder="请输入备注信息" maxlength="200" show-word-limit />
+					</el-form-item>
+				</el-col>
+			</el-row>
+		</el-form>
+		<template #footer>
+			<el-button :loading="loading" type="primary" @click="submitForm(ruleFormRef)">确 定</el-button>
+			<el-button @click="resetForm(ruleFormRef)">重 置</el-button>
+		</template>
+	</el-dialog>
+</template>
+
+<style scoped>
+:deep(.el-drawer .el-drawer__header) {
+	border: none !important;
+}
+</style>

+ 4 - 0
src/views/product-manage/product-monitor/index.vue

@@ -42,6 +42,10 @@ const groupOptions: any = ref([]);
 const brandsOptions: any = ref([]);
 const shopsOptions: any = ref([]);
 
+provide('groupOptions', groupOptions);
+provide('brandsOptions', brandsOptions);
+provide('shopOptions', shopsOptions);
+
 onBeforeMount(() => {
   fetchGroupOptions();
   fetchBrandsOptions();