瀏覽代碼

新增商品列表功能和相关组件- 添加商品列表页面及其相关组件
- 实现商品列表的查询、编辑和导入功能
- 新增权限按钮、垂直分割线等通用组件
- 优化用户界面和交互逻辑

WanGxC 7 月之前
父節點
當前提交
e6cd6acc23

+ 3 - 0
components.d.ts

@@ -17,14 +17,17 @@ declare module 'vue' {
     Editor: typeof import('./src/components/editor/index.vue')['default']
     ForeignKey: typeof import('./src/components/foreignKey/index.vue')['default']
     IconSelector: typeof import('./src/components/iconSelector/index.vue')['default']
+    ImportButton: typeof import('./src/components/ImportButton/index.vue')['default']
     ImportExcel: typeof import('./src/components/importExcel/index.vue')['default']
     List: typeof import('./src/components/iconSelector/list.vue')['default']
     ManyToMany: typeof import('./src/components/manyToMany/index.vue')['default']
     NoticeBar: typeof import('./src/components/noticeBar/index.vue')['default']
+    PermissionButton: typeof import('./src/components/PermissionButton/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SvgIcon: typeof import('./src/components/svgIcon/index.vue')['default']
     Table: typeof import('./src/components/table/index.vue')['default']
     TableSelector: typeof import('./src/components/tableSelector/index.vue')['default']
+    VerticalDivider: typeof import('./src/components/VerticalDivider/index.vue')['default']
   }
 }

+ 102 - 0
src/components/ImportButton/index.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts">
+/**
+ * @Name: index.vue
+ * @Description: 文件上传
+ * @Author: Cheney
+ */
+import { ButtonProps, genFileId, UploadInstance, UploadRawFile } from 'element-plus';
+import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
+
+
+const { data } = BtnPermissionStore();
+
+const attrs = useAttrs() as any;
+
+console.log('attrs=> ', attrs);
+
+const props = defineProps<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;
+}
+
+const upload = ref<UploadInstance>();
+const upBtnLoading = ref(false);
+
+/**
+ * @description 替换文件并上传
+ * @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();
+}
+
+/**
+ * 上传文件
+ * @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;
+  }
+}
+
+/**
+ * 统一处理响应
+ * @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 });
+  // }
+}
+</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>
+</template>
+
+<style scoped>
+
+</style>

+ 36 - 0
src/components/PermissionButton/index.vue

@@ -0,0 +1,36 @@
+<script lang="ts" setup>/**
+ * @Name: index.vue
+ * @Description: 权限按钮
+ * @Author: Cheney
+ */
+import { ButtonProps } from 'element-plus';
+import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
+
+const { data } = BtnPermissionStore();
+
+const attrs = useAttrs() as any;
+
+const props = defineProps<Partial<Omit<ButtonProps, ''>>>();
+
+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;
+}
+</script>
+
+<template>
+  <div>
+    <el-button v-if="attrs.permissions ? hasPermission(attrs.permissions) : true"
+               v-bind="props">
+      <slot></slot>
+    </el-button>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 24 - 0
src/components/VerticalDivider/index.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+/**
+ * @Name: index.vue
+ * @Description:
+ * @Author: Cheney
+ */
+
+</script>
+
+<template>
+  <div class="arco-divider-vertical"></div>
+</template>
+
+<style scoped>
+.arco-divider-vertical {
+  display: inline-block;
+  min-width: 1px;
+  max-width: 1px;
+  min-height: 1em;
+  margin: 0 0 0 12px;
+  vertical-align: middle;
+  border-left: 1px solid rgb(229, 230, 235);
+}
+</style>

+ 1 - 1
src/components/importExcel/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div style="display: inline-block">
-    <el-button size="default" type="success" @click="handleImport()">
+    <el-button bg round text v-bind="props" @click="handleImport()">
       <slot>导入</slot>
     </el-button>
     <el-dialog :title="props.upload.title" v-model="uploadShow" width="400px" append-to-body>

+ 2 - 1
src/layout/component/main.vue

@@ -4,7 +4,7 @@
 		<el-scrollbar ref="layoutMainScrollbarRef" class="layout-main-scroll layout-backtop-header-fixed"
 			wrap-class="layout-main-scroll" view-class="layout-main-scroll">
 			<LayoutParentView />
-      <LayoutFooter v-if="isFooter" />
+      <!--<LayoutFooter v-if="isFooter" />-->
 		</el-scrollbar>
 		<el-backtop :target="setBacktopClass" />
 	</el-main>
@@ -36,6 +36,7 @@ const isFooter = computed(() => {
 });
 // 设置 header 固定
 const isFixedHeader = computed(() => {
+  console.log('themeConfig.value.isFixedHeader;=> ', themeConfig.value.isFixedHeader);
 	return themeConfig.value.isFixedHeader;
 });
 // 设置 Backtop 回到顶部

+ 16 - 2
src/layout/navMenu/subItem.vue

@@ -11,12 +11,26 @@
       <el-menu-item :key="val.path" :index="val.path">
         <div class="menu-hover rounded-md absolute left-4 px-10" style="width: 85%;">
           <template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
-            <SvgIcon :name="val.meta.icon"/>
+            <SvgIcon :name="val.meta.icon" :style="[
+                  val.meta.icon.startsWith('bi') ? {
+                    'vertical-align': 'middle',
+                    'margin-right': '5px',
+                    'width': '24px',
+                    'text-align': 'center',
+                  } : {}
+                ]"/>
             <span>{{ $t(val.meta.title) }}</span>
           </template>
           <template v-else>
             <a class="w100" @click.prevent="onALinkClick(val)">
-              <SvgIcon :name="val.meta.icon"/>
+              <SvgIcon :name="val.meta.icon" :style="[
+                  val.meta.icon.startsWith('bi') ? {
+                    'vertical-align': 'middle',
+                    'margin-right': '5px',
+                    'width': '24px',
+                    'text-align': 'center',
+                  } : {}
+                ]"/>
               {{ $t(val.meta.title) }}
             </a>
           </template>

+ 9 - 5
src/layout/navMenu/vertical.vue

@@ -12,12 +12,19 @@
       <!-- 可展开菜单 -->
       <el-sub-menu v-if="val.children && val.children.length > 0" :key="val.path" :index="val.path" class="custom-menu">
         <template #title>
-          <SvgIcon :name="val.meta.icon"/>
+          <SvgIcon :name="val.meta.icon" :style="[
+                  val.meta.icon.startsWith('bi') ? {
+                    'vertical-align': 'middle',
+                    'margin-right': '5px',
+                    'width': '24px',
+                    'text-align': 'center',
+                  } : {}
+                ]"/>
           <span>{{ $t(val.meta.title) }}</span>
         </template>
         <SubItem :chil="val.children"/>
       </el-sub-menu>
-      
+
       <template v-else>
         <div class="menu-hover rounded-md">
           <el-menu-item :key="val.path" :index="val.path" class="">
@@ -52,8 +59,6 @@ import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import other from '/@/utils/other';
 
-
-
 // 引入组件
 const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
 
@@ -80,7 +85,6 @@ const state = reactive({
 const menuLists = computed(() => {
   return <RouteItems>props.menuList;
 });
-console.log('menuLists=> ', menuLists);
 // 获取布局配置信息
 const getThemeConfig = computed(() => {
   return themeConfig.value;

+ 0 - 2
src/layout/routerView/parent.vue

@@ -3,9 +3,7 @@
 		<router-view v-slot="{ Component }">
 			<transition :name="setTransitionName" mode="out-in">
 				<keep-alive :include="getKeepAliveNames" v-if="showView">
-          <div class="flex flex-1"> <!-- flex-1 避免 layout main 的内容不能全屏 -->
 						<component :is="Component" :key="state.refreshRouterViewKey" class="w100" v-show="!isIframePage" />
-          </div>
 				</keep-alive>
 			</transition>
 		</router-view>

+ 41 - 19
src/stores/btnPermission.ts

@@ -2,25 +2,47 @@ import {defineStore} from "pinia";
 import {DictionaryStates} from "/@/stores/interface";
 import {request} from "/@/utils/service";
 
-export const BtnPermissionStore = defineStore('BtnPermission', {
-    state: (): DictionaryStates => ({
-        data: []
-    }),
-    actions: {
-        async getBtnPermissionStore() {
-            request({
+// export const BtnPermissionStore = defineStore('BtnPermission', {
+//     state: (): DictionaryStates => ({
+//         data: []
+//     }),
+//     actions: {
+//         async getBtnPermissionStore() {
+//             request({
+//                 url: '/api/system/menu_button/menu_button_all_permission/',
+//                 method: 'get',
+//             }).then((ret: {
+//                 data: []
+//             }) => {
+//                 // 转换数据格式并保存到pinia
+//                 let dataList = ret.data
+//                 this.data=dataList
+//             })
+//         },
+//     },
+//     persist: {
+//         enabled: true,
+//     },
+// });
+
+
+export const BtnPermissionStore = defineStore('BtnPermission', () => {
+    const data = ref<any[]>([]);
+
+    const getBtnPermissionStore = async () => {
+        try {
+            const response = await request({
                 url: '/api/system/menu_button/menu_button_all_permission/',
                 method: 'get',
-            }).then((ret: {
-                data: []
-            }) => {
-                // 转换数据格式并保存到pinia
-                let dataList = ret.data
-                this.data=dataList
-            })
-        },
-    },
-    persist: {
-        enabled: true,
-    },
+            });
+            data.value = response.data;
+        } catch (error) {
+            console.error('Error fetching button permissions:', error);
+        }
+    };
+
+    return {
+        data,
+        getBtnPermissionStore,
+    };
 });

+ 3 - 3
src/stores/themeConfig.ts

@@ -70,7 +70,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 			// 是否开启菜单手风琴效果
 			isUniqueOpened: true,
 			// 是否开启固定 Header
-			isFixedHeader: false,
+			isFixedHeader: true,
 			// 初始化变量,用于更新菜单 el-scrollbar 的高度,请勿删除
 			isFixedHeaderChange: false,
 			// 是否开启经典布局分割菜单(仅经典布局生效)
@@ -90,7 +90,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 			// 是否开启 Breadcrumb,强制经典、横向布局不显示
 			isBreadcrumb: true,
 			// 是否开启 Tagsview
-			isTagsview: true,
+			isTagsview: false,
 			// 是否开启 Breadcrumb 图标
 			isBreadcrumbIcon: true,
 			// 是否开启 Tagsview 图标
@@ -131,7 +131,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 			 * 中的 `initSetLayoutChange(设置布局切换,重置主题样式)` 方法
 			 */
 			// 布局切换:可选值"<defaults|classic|transverse|columns>",默认 defaults
-			layout: 'classic',
+			layout: 'defaults',
 
 			/**
 			 * 后端控制路由

+ 9 - 2
src/theme/app.scss

@@ -31,7 +31,8 @@ body,
 	padding: 0;
 	width: 100%;
 	height: 100%;
-	font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
+	color: rgba(0, 0, 0, 0.88);
+	font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
 	font-weight: 400;
 	-webkit-font-smoothing: antialiased;
 	-webkit-tap-highlight-color: transparent;
@@ -39,6 +40,7 @@ body,
 	font-size: 14px;
 	overflow: hidden;
 	position: relative;
+	--vxe-ui-font-primary-color: #165DFF;
 }
 
 /* 主布局样式
@@ -49,6 +51,9 @@ body,
 	.layout-pd {
 		padding: 15px !important;
 	}
+	.layout-screen-height {
+		height: calc(100vh - 300px);
+	}
 	.layout-flex {
 		display: flex;
 		flex-direction: column;
@@ -77,13 +82,13 @@ body,
 		overflow: hidden;
 		width: 100%;
 		//background-color: var(--next-bg-main-color);
-		//background-color: #f5f5f5;
 		background-color: #F4F5FC;
 		display: flex;
 		flex-direction: column;
 		// 内层 el-scrollbar样式,用于界面高度自适应(main.vue)
 		.layout-main-scroll {
 			@extend .layout-flex;
+			
 			.layout-parent {
 				@extend .layout-flex;
 				position: relative;
@@ -99,10 +104,12 @@ body,
 		height: 100%;
 		overflow: hidden;
 		@extend .layout-flex;
+		
 		&-auto {
 			height: inherit;
 			@extend .layout-flex;
 		}
+		
 		&-view {
 			background: var(--el-color-white);
 			width: 100%;

+ 1 - 1
src/utils/usePagination.ts

@@ -7,7 +7,7 @@ export function usePagination(getData: Function) {
     data: [],
     total: 0,
     page: 1,
-    limit: 10,
+    limit: 15,
     loading: false
   })
   /**

+ 1 - 1
src/utils/useResponse.ts

@@ -5,8 +5,8 @@
  * @param loadingOrOptions ref-loading | vxe-loading (ref | obj)
  */
 export async function useResponse(
-    parameter: any = {},
     requestApi: any,
+    parameter: any = {},
     loadingOrOptions?: Ref<boolean> | { loading?: boolean, value?: { loading: boolean } } | any
 ) {
   const setLoading = (value: boolean) => {

+ 82 - 0
src/views/product-list/api.ts

@@ -0,0 +1,82 @@
+import { request } from '/@/utils/service';
+
+
+const apiPrefix = '/api/assets/shop/';
+
+export function getCardData(query: any) {
+  return request({
+    url: apiPrefix + 'card/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getTableData(query: any) {
+  return request({
+    url: apiPrefix,
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getPlatformDetailOverview(query: any) {
+  return request({
+    url: apiPrefix + 'platform/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getShopDetailOverview(query: any) {
+  return request({
+    url: apiPrefix + 'detail/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getCurrentInfo(query: any) {
+  return request({
+    url: apiPrefix + 'current/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getHistoryInfo(query: any) {
+  return request({
+    url: apiPrefix + 'past/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getComputerInfo(query: any) {
+  return request({
+    url: apiPrefix + 'computer/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getShopSelect() {
+  return request({
+    url: apiPrefix + 'box/',
+    method: 'GET',
+  });
+}
+export function getCompanySelect() {
+  return request({
+    url: '/api/assets/company/box/',
+    method: 'GET',
+  });
+}
+
+export function updateShopDetail(body: any) {
+  return request({
+    url: apiPrefix + `${body.id}/`,
+    method: 'POST',
+    params: { partial: body.partial },
+    data: body.formData,
+  });
+}

+ 30 - 0
src/views/product-list/colDefine.tsx

@@ -0,0 +1,30 @@
+import { useCountryInfoStore } from '/@/stores/countryInfo';
+
+const countryInfoStore = useCountryInfoStore();
+
+export const productColumns: any = [
+  { type: 'checkbox', width: 50, align: 'center', fixed: 'left' },
+  { type: 'seq', width: 60, align: 'center' },
+  {
+    field: 'is_monitor', title: '监控管理', width: 90, align: 'center',
+    slots: {
+      default({ row }: any) {
+        return <el-switch v-model={ row.is_monitor }></el-switch>;
+      }
+    }
+  },
+  {
+    field: 'country', title: '国 家', minWidth: 'auto', align: 'center',
+    slots: {
+      default({ row }: any) {
+        const country = countryInfoStore.countries.find(c => c.name === row.country);
+        const color = country ? country.color : '#3875F6';
+        return <el-tag effect="plain" round
+                       style={ { color: color, borderColor: color } }>{ row.country ? row.country : '--' }</el-tag>;
+      }
+    }
+  },
+  { 
+    field: 'operate', title: '操 作', width: 100, align: 'center', fixed: 'right',
+    slots: { default: 'operate' }},
+];

+ 241 - 0
src/views/product-list/component/DataTable.vue

@@ -0,0 +1,241 @@
+<script lang="ts" setup>
+/**
+ * @Name: Table.vue
+ * @Description:
+ * @Author: Cheney
+ */
+
+import { ArrowDown, Download, Message, Money, Open, Operation, Refresh } from '@element-plus/icons-vue';
+import { usePagination } from '/@/utils/usePagination';
+import { useTableData } from '/@/utils/useTableData';
+import * as api from '/@/views/product-list/api';
+import PermissionButton from '/@/components/PermissionButton/index.vue';
+import { productColumns } from '/@/views/product-list/colDefine';
+import { useResponse } from '/@/utils/useResponse';
+import EditDrawer from '/@/views/product-list/component/EditDrawer.vue';
+import NoticeDialog from '/@/views/product-list/component/NoticeDialog.vue';
+import ImportButton from '/@/components/ImportButton/index.vue';
+import VerticalDivider from '/@/components/VerticalDivider/index.vue';
+import { Icon } from '@iconify/vue';
+
+
+const { tableOptions, handlePageChange } = usePagination(fetchList);
+
+const gridRef = ref();
+const gridOptions: any = reactive({
+  border: false,
+  round: true,
+  stripe: true,
+  currentRowHighLight: true,
+  height: 'auto',
+  toolbarConfig: {
+    custom: true,
+    slots: {
+      buttons: 'toolbar_buttons',
+      tools: 'toolbar_tools'
+    }
+  },
+  rowConfig: {
+    isHover: true
+  },
+  columnConfig: {
+    resizable: true
+  },
+  pagerConfig: {
+    total: tableOptions.value.total,
+    page: tableOptions.value.page,
+    limit: tableOptions.value.limit
+  },
+  loading: false,
+  loadingConfig: {
+    icon: 'vxe-icon-indicator roll',
+    text: '正在拼命加载中...'
+  },
+  columns: productColumns,
+  data: ''
+});
+
+const checkedList = ref<Set<number>>(new Set());
+
+const editOpen = ref(false);
+const rowData = ref({});
+
+const dialogVisible = ref(false);
+
+onBeforeMount(() => {
+  fetchList();
+});
+
+async function fetchList() {
+  const query = {
+    page: gridOptions.pagerConfig.page,
+    limit: gridOptions.pagerConfig.limit
+  };
+  await useTableData(api.getTableData, query, gridOptions);
+}
+
+function handleRefresh() {
+  gridOptions.pagerConfig.page = 1;
+  gridOptions.pagerConfig.limit = 15;
+  fetchList();
+}
+
+async function batchOpen() {
+  const ids = Array.from(checkedList.value);
+  await useResponse(api.updateShopDetail, { ids, status: 1 });
+  await fetchList();
+}
+
+function selectChangeEvent({ checked, row }: any) {
+  if (checked) {
+    checkedList.value.add(row.id); // 获取单个数据
+  } else {
+    checkedList.value.delete(row.id);
+  }
+}
+
+function selectAllChangeEvent({ checked }: any) {
+  const $grid = gridRef.value;
+  if ($grid) {
+    const records = $grid.getData(); // 获取所有数据
+    if (checked) {
+      records.forEach((item: any) => {
+        checkedList.value.add(item.id);
+      });
+    } else {
+      checkedList.value.clear();
+    }
+  }
+}
+
+function handleEdit(row: any) {
+  editOpen.value = true;
+  rowData.value = row;
+}
+
+function handleNotice(row: any) {
+  dialogVisible.value = true;
+  rowData.value = row;
+}
+
+const value = ref();
+
+function downloadTemplate() {
+  console.log('111=> ');
+}
+</script>
+
+<template>
+  <div class="layout-screen-height">
+    <vxe-grid ref="gridRef" v-bind="gridOptions"
+              @checkbox-change="selectChangeEvent"
+              @checkbox-all="selectAllChangeEvent">
+      <template #toolbar_buttons>
+        <div class="flex gap-2">
+          <PermissionButton :icon="Open" plain round type="primary" :disabled="!checkedList.size" @click="batchOpen">
+            批量开启
+          </PermissionButton>
+          <VerticalDivider class="px-1" style="margin-left: 7px;"/>
+          <div class="custom-el-input">
+            <el-select
+                v-model="value"
+                placeholder="Select"
+                style="width: 190px"
+            >
+              <template #prefix>
+                <div class="flex items-center">
+                  <el-button link type="success" style="margin-left: 0px; font-size: 14px"
+                             @click.stop="downloadTemplate">下载
+                  </el-button>
+                  <VerticalDivider />
+                </div>
+              </template>
+              <el-option
+                  label="商品通知模板"
+                  value="item1"
+              />
+              <el-option
+                  label="商品模板"
+                  value="item2"
+              />
+              <el-option
+                  label="指导价格模板"
+                  value="item3"
+              />
+            </el-select>
+          </div>
+          <ImportButton :icon="Message" text bg>
+            变更通知导入
+          </ImportButton>
+          <ImportButton  text bg>
+            <i class="bi bi-box-seam mr-3"></i>
+            商品导入
+          </ImportButton>
+          <ImportButton :icon="Money" text bg>
+            指导价格导入
+          </ImportButton>
+        </div>
+      </template>
+      <!-- 工具栏右侧插槽 -->
+      <template #toolbar_tools>
+        <el-button circle class="toolbar-btn" @click="handleRefresh">
+          <el-icon>
+            <Refresh/>
+          </el-icon>
+        </el-button>
+        <el-button circle class="mr-3 toolbar-btn">
+          <el-icon>
+            <Download/>
+          </el-icon>
+        </el-button>
+      </template>
+      <template #top>
+        <div class="mb-2"></div>
+      </template>
+      <!-- 分页插槽 -->
+      <template #pager>
+        <vxe-pager
+            v-model:currentPage="gridOptions.pagerConfig.page"
+            v-model:pageSize="gridOptions.pagerConfig.limit"
+            :total="gridOptions.pagerConfig.total"
+            @page-change="handlePageChange"
+        >
+        </vxe-pager>
+      </template>
+      <!-- 表格内容插槽 -->
+      <template #operate="{ row }">
+        <div class="flex justify-between">
+          <PermissionButton circle plain type="warning" @click="handleEdit(row)">
+            <el-icon>
+              <Operation/>
+            </el-icon>
+          </PermissionButton>
+          <PermissionButton circle plain type="info" @click="handleNotice(row)">
+            <el-icon>
+              <Message/>
+            </el-icon>
+          </PermissionButton>
+        </div>
+      </template>
+    </vxe-grid>
+
+    <EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData"/>
+    <NoticeDialog v-if="dialogVisible" v-model="dialogVisible" :row-data="rowData"/>
+  </div>
+</template>
+
+<style scoped>
+.toolbar-btn {
+  width: 34px;
+  height: 34px;
+  font-size: 18px
+}
+
+:deep(.custom-el-input .el-select__wrapper) {
+  border-radius: 20px;
+}
+
+/* .screen-height {
+  height: calc(100vh - 300px);
+} */
+</style>

+ 113 - 0
src/views/product-list/component/EditDrawer.vue

@@ -0,0 +1,113 @@
+<script lang="ts" setup>
+/**
+ * @Name: EditDrawer.vue
+ * @Description: 店铺编辑
+ * @Author: Cheney
+ */
+
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { useResponse } from '/@/utils/useResponse';
+import * as api from '/@/views/shop-information/api';
+
+
+const loading = ref(false);
+const editOpen = defineModel({ default: false });
+const { rowData } = defineProps<{
+  rowData: object;
+}>();
+
+const emit = defineEmits([ 'refresh' ]);
+
+onBeforeMount(() => {
+  // replaceCol();
+  console.log('rowData=> ', rowData);
+});
+
+interface RuleForm {
+  operatorName: any,
+  country: any
+}
+
+const ruleFormRef = ref<FormInstance>();
+const ruleForm = reactive<RuleForm>({
+  operatorName: '',
+  country: ''
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+  operatorName: [
+    { message: 'Please input operator name', trigger: 'blur' }
+  ]
+
+});
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      // await useResponse({ id: gridOptions.data[0].id, partial: 1, formData: ruleForm }, api.updateShopDetail, loading);
+      editOpen.value = false;
+      ElMessage.success('编辑成功');
+      emit('refresh');
+    } else {
+      console.log('error submit!', fields);
+    }
+  });
+};
+
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  formEl.resetFields();
+};
+
+// function replaceCol() {
+//   const result = Object.keys(ruleForm).reduce((acc, key) => {
+//     if (key in gridOptions.data[0]) {
+//       acc[key] = gridOptions.data[0][key];
+//     }
+//     return acc;
+//   }, {} as { [key: string]: any });
+//   Object.assign(ruleForm, result);
+// }
+</script>
+
+<template>
+  <el-drawer v-model="editOpen"
+             :close-on-click-modal="false"
+             :close-on-press-escape="false"
+             :title="`店铺编辑 - `"
+             size="30%">
+    <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        class="mx-2.5 mt-2.5"
+        label-width="auto"
+        status-icon>
+      <el-form-item label="运营" prop="operatorName">
+        <el-input v-model="ruleForm.operatorName"/>
+      </el-form-item>
+
+      <el-form-item label="国家" prop="country">
+        <!--<el-select v-model="ruleForm.country" placeholder="请选择线路">-->
+        <!--  <el-option-->
+        <!--      v-for="item in rowData.country"-->
+        <!--      :key="item"-->
+        <!--      :label="item"-->
+        <!--      :value="item">-->
+        <!--  </el-option>-->
+        <!--</el-select>-->
+      </el-form-item>
+      <el-form-item>
+        <div class="flex flex-1 justify-center">
+          <el-button :loading="loading" type="primary" @click="submitForm(ruleFormRef)">确 定</el-button>
+          <el-button @click="resetForm(ruleFormRef)">重 置</el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+  </el-drawer>
+</template>
+
+<style scoped>
+
+</style>

+ 135 - 0
src/views/product-list/component/NoticeDialog.vue

@@ -0,0 +1,135 @@
+<script lang="ts" setup>/**
+ * @Name: NoticeDialog.vue
+ * @Description:
+ * @Author: Cheney
+ */
+import { ElMessage } from 'element-plus';
+
+
+const { rowData } = defineProps<{
+  rowData: object;
+}>();
+// console.log('rowData=> ', rowData);
+const dialogVisible = defineModel({ default: false });
+
+const staffSelect = ref('');
+const staffOptions: any = ref([]);
+const staffTags: any = ref([]);
+const staffLoading = ref(false);
+const currentRow: any = ref(null);
+
+function handleClose(done: any) {
+  staffSelect.value = '';
+  staffTags.value = [];
+  done();
+}
+
+function addStaffChange() {
+  const selectedOption: any = staffOptions.value.find((option: any) => option.id === staffSelect.value);
+  if (selectedOption && !staffTags.value.some((tag: any) => tag.id === selectedOption.id)) {
+    // 如果选中的项不在 staffTags 中,则添加到 staffTags
+    staffTags.value.push({
+      id: selectedOption.id,
+      username: selectedOption.username
+    });
+  }
+}
+
+function isOptionDisabled(id: any) {
+  return staffTags.value.some((tag: any) => tag.id === id);
+}
+
+function removeTag(tag: any) {
+  staffTags.value = staffTags.value.filter((t: any) => t.id !== tag.id);
+}
+
+async function fetchExistingStaff(row: any) {
+  const query = {
+    id: row.value.id ? row.value.id : row.value.rowid
+  };
+  // const resp = await api.getExistingStaffs(query);
+  // staffTags.value = resp.data;
+}
+
+async function addStaffs() {
+  staffLoading.value = true;
+  const body = {
+    id: currentRow.id,
+    user_ids: staffTags.map((tag: any) => tag.id)
+  };
+  try {
+    // const resp = await api.postStaffs(body);
+    // if (resp.code === 2000) {
+    //   ElMessage.error('编辑成功!');
+    //   await fetchExistingStaff(currentRow);
+    // }
+  } catch (error) {
+    ElMessage.error('编辑失败!');
+  } finally {
+    staffSelect.value = '';
+    staffLoading.value = false;
+  }
+}
+
+function cancelDialog() {
+  handleClose(() => {
+    dialogVisible.value = false;
+  });
+}
+</script>
+
+<template>
+  <div>
+    <el-dialog
+        v-model="dialogVisible"
+        :before-close="handleClose"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        title="变更通知"
+        width="35%"
+    >
+      <el-row class="mb-2">
+        <el-col>
+          <span class="mr-2">人员选择</span>
+          <el-select v-model="staffSelect" filterable placeholder="输入搜索" style="width: 200px;"
+                     @change="addStaffChange">
+            <el-option
+                v-for="item in staffOptions"
+                :key="item.id"
+                :disabled="isOptionDisabled(item.id)"
+                :label="item.username"
+                :value="item.id">
+            </el-option>
+          </el-select>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="2">
+
+        </el-col>
+        <el-col :span="20" class="ml-2.5">
+          <i class="bi bi-info-circle"></i>
+          <span class="ml-1" style="color: #909399">仅可添加已绑定邮箱的用户</span>
+        </el-col>
+      </el-row>
+      <el-divider style="margin: 12px 0 20px 0"></el-divider>
+      <div class="flex flex-wrap gap-1.5">
+        <el-tag
+            v-for="tag in staffTags"
+            :key="tag.id"
+            closable
+            @close="removeTag(tag)">
+          {{ tag.username }}
+        </el-tag>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="cancelDialog">取 消</el-button>
+        <el-button :loading="staffLoading" type="primary" @click="addStaffs">确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 104 - 0
src/views/product-list/index.vue

@@ -0,0 +1,104 @@
+<script lang="ts" setup>
+/**
+ * @Name: index.vue
+ * @Description: 商品列表
+ * @Author: Cheney
+ */
+
+import VerticalDivider from '/@/components/VerticalDivider/index.vue';
+import { RefreshRight, Search } from '@element-plus/icons-vue';
+import DataTable from './component/DataTable.vue';
+
+
+const loading = ref(false);
+
+const formInline = reactive({
+  user: '',
+  region: '',
+  date: ''
+});
+
+function onClick() {
+  loading.value = true;
+  setTimeout(() => {
+    loading.value = false;
+  }, 2000);
+}
+</script>
+
+<template>
+  <div class="p-5">
+    <el-card style="color: rgba(0, 0, 0, 0.88);">
+      <div class="text-xl font-semibold pb-7">商品列表</div>
+      <!-- 查询条件 -->
+      <div class="flex justify-between">
+        <div class="flex flex-1">
+          <div class="w-full whitespace-nowrap">
+            <el-row :gutter="20" style="margin-bottom: 16px;">
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">国 家</span>
+                  <el-select v-model="formInline.date" clearable placeholder="请选择国家"/>
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">品 牌</span>
+                  <el-select v-model="formInline.date" clearable placeholder="请选择品牌"/>
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">分 组</span>
+                  <el-select v-model="formInline.date" clearable placeholder="请选择分组"/>
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">状 态</span>
+                  <el-select v-model="formInline.date" clearable placeholder="请选择状态"/>
+                </div>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">ASIN</span>
+                  <el-input v-model="formInline.region" clearable placeholder="请输入ASIN"></el-input>
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">SKU</span>
+                  <el-input v-model="formInline.region" clearable placeholder="请输入SKU"></el-input>
+                </div>
+              </el-col>
+              <el-col :span="6" class="flex">
+                <div class="flex items-center">
+                  <span class="mr-2">店 铺</span>
+                  <el-input v-model="formInline.region" clearable placeholder="请输入店铺"></el-input>
+                </div>
+              </el-col>
+            </el-row>
+          </div>
+        </div>
+        <VerticalDivider />
+        <div class="flex flex-col gap-1.5 items-end">
+          <el-button :icon="Search" :loading="loading" class="mb-4" type="primary" @click="onClick">
+            查 询
+          </el-button>
+          <el-button :icon="RefreshRight" style="width: 88px; color: #3c3c3c;" color="#ECECF1C9" >
+            重 置
+          </el-button>
+        </div>
+      </div>
+      <el-divider style="margin: 20px 0 12px 0"/>
+        
+      <DataTable></DataTable>
+
+    </el-card>
+  </div>
+</template>
+
+<style scoped>
+</style>

+ 0 - 16
src/views/product-management/index.vue

@@ -1,16 +0,0 @@
-<script setup lang="ts">
-/**
- * @Name: index.vue
- * @Description: 商品管理
- * @Author: Cheney
- */
-
-</script>
-
-<template>
-
-</template>
-
-<style scoped>
-
-</style>

+ 0 - 3
src/views/shop-information/useColumns.tsx

@@ -1,13 +1,10 @@
 import { useCountryInfoStore } from '/@/stores/countryInfo';
-import { useResponse } from '/@/utils/useResponse';
 import * as api from '/@/views/shop-information/api';
 
 
 const countryInfoStore = useCountryInfoStore();
 
 export const companySelect: Ref<any[]> = ref([]);
-// const ret = await useResponse({}, api.getCompanySelect);
-// companySelect.value = ret.data;
 
 async function main() {
   const result = await api.getCompanySelect();

+ 26 - 24
src/views/system/user/index.vue

@@ -1,37 +1,38 @@
 <template>
   <fs-page>
     <el-row class="mx-2">
-      <el-col xs="24" :sm="8" :md="6" :lg="4" :xl="4" class="p-1">
+      <el-col :lg="4" :md="6" :sm="8" :xl="4" class="p-1" xs="24">
         <el-card :body-style="{ height: '100%' }">
           <p class="font-mono font-black text-center text-xl pb-5">
             部门列表
-            <el-tooltip effect="dark" :content="content" placement="right">
+            <el-tooltip :content="content" effect="dark" placement="right">
               <el-icon>
                 <QuestionFilled/>
               </el-icon>
             </el-tooltip>
           </p>
           <el-input v-model="filterText" :placeholder="placeholder"/>
-          <el-tree ref="treeRef" class="font-mono font-bold leading-6 text-7xl" :data="data" :props="treeProps"
-                   :filter-node-method="filterNode" icon="ArrowRightBold" :indent="38" highlight-current @node-click="onTreeNodeClick">
+          <el-tree ref="treeRef" :data="data" :filter-node-method="filterNode" :indent="38"
+                   :props="treeProps" class="font-mono font-bold leading-6 text-7xl" highlight-current icon="ArrowRightBold"
+                   @node-click="onTreeNodeClick">
             <template #default="{ node, data }">
-              <element-tree-line :node="node" :showLabelLine="false" :indent="32">
+              <element-tree-line :indent="32" :node="node" :showLabelLine="false">
 					<span v-if="data.status" class="text-center font-black font-normal">
-						<SvgIcon name="iconfont icon-shouye" color="var(--el-color-primary)"/>&nbsp;{{ node.label }}
+						<SvgIcon color="var(--el-color-primary)" name="iconfont icon-shouye"/>&nbsp;{{ node.label }}
 					</span>
                 <span v-else color="var(--el-color-primary)"> <SvgIcon name="iconfont icon-shouye"/>&nbsp;{{
                     node.label
-                  }} </span>
+                                                                                                    }} </span>
               </element-tree-line>
             </template>
           </el-tree>
         </el-card>
       </el-col>
-      <el-col xs="24" :sm="16" :md="18" :lg="20" :xl="20" class="p-1">
+      <el-col :lg="20" :md="18" :sm="16" :xl="20" class="p-1" xs="24">
         <el-card :body-style="{ height: '100%' }">
           <fs-crud ref="crudRef" v-bind="crudBinding">
             <template #actionbar-right>
-              <importExcel api="api/system/user/" v-auth="'user:Import'">导入</importExcel>
+              <importExcel v-auth="'user:Import'" api="api/system/user/">导入</importExcel>
             </template>
           </fs-crud>
         </el-card>
@@ -41,15 +42,16 @@
   </fs-page>
 </template>
 
-<script lang="ts" setup name="user">
-import {useExpose, useCrud} from '@fast-crud/fast-crud';
-import {createCrudOptions} from './crud';
+<script lang="ts" name="user" setup>
+import { useExpose, useCrud } from '@fast-crud/fast-crud';
+import { createCrudOptions } from './crud';
 import * as api from './api';
-import {ElTree} from 'element-plus';
-import {ref, onMounted, watch, toRaw, h} from 'vue';
+import { ElTree } from 'element-plus';
+import { ref, onMounted, watch, toRaw, h } from 'vue';
 import XEUtils from 'xe-utils';
-import {getElementLabelLine} from 'element-tree-line';
-import importExcel from '/@/components/importExcel/index.vue'
+import { getElementLabelLine } from 'element-tree-line';
+import importExcel from '/@/components/importExcel/index.vue';
+
 
 const ElementTreeLine = getElementLabelLine(h);
 
@@ -74,7 +76,7 @@ const treeRef = ref<InstanceType<typeof ElTree>>();
 const treeProps = {
   children: 'children',
   label: 'name',
-  icon: 'icon',
+  icon: 'icon'
 };
 
 watch(filterText, (val) => {
@@ -98,7 +100,7 @@ const getData = () => {
     const result = XEUtils.toArrayTree(responseData, {
       parentKey: 'parent',
       children: 'children',
-      strict: true,
+      strict: true
     });
 
     data.value = result;
@@ -107,8 +109,8 @@ const getData = () => {
 
 //树形点击事件
 const onTreeNodeClick = (node: any) => {
-  const {id} = node;
-  crudExpose.doSearch({form: {dept: id}});
+  const { id } = node;
+  crudExpose.doSearch({ form: { dept: id } });
 };
 
 // 页面打开后获取列表数据
@@ -121,11 +123,11 @@ const crudRef = ref();
 // crud 配置的ref
 const crudBinding = ref();
 // 暴露的方法
-const {crudExpose} = useExpose({crudRef, crudBinding});
+const { crudExpose } = useExpose({ crudRef, crudBinding });
 // 你的crud配置
-const {crudOptions} = createCrudOptions({crudExpose});
+const { crudOptions } = createCrudOptions({ crudExpose });
 // 初始化crud配置
-const {resetCrudOptions} = useCrud({crudExpose, crudOptions});
+const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
 
 // 页面打开后获取列表数据
 onMounted(() => {
@@ -147,6 +149,6 @@ onMounted(() => {
 }
 
 .font-normal {
-  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
 }
 </style>

+ 3 - 5
src/views/test/index.vue

@@ -12,9 +12,7 @@
 </script>
 
 <template>
-  <!--<el-button @click="handleOpen">确定</el-button>-->
-  
-  <!--&lt;!&ndash; 传递 isOpen 给子组件 &ndash;&gt;-->
-  <!--&lt;!&ndash;<PropsTest :text="text" v-model="isOpen" />&ndash;&gt;-->
-  <!--<el-tag type="warning"></el-tag>-->
+<div style="background-color: #3c3c3c;">
+  asd
+</div>
 </template>