Переглянути джерело

```
feat(upload): 增加文件大小限制功能并优化上传组件

- 为 `z-upload` 组件新增 `FileSize` 属性,用于限制上传图片的大小
- 在上传前校验文件尺寸,超出限制时提示用户并阻止上传
- 添加了对 props 变化的监听日志(开发调试用途)

feat(router): 新增运营管理相关路由配置

- 添加「广告管理」和「热门搜索」两个新模块的路由映射
- 完成国际化语言包中对应菜单项的占位翻译字段
- 注册新的页面视图组件路径到优雅路由系统

feat(api): 创建运营模块下的广告与热搜接口服务

- 实现广告管理相关的 CRUD API 调用函数
- 实现热门搜索内容的分页查询、添加、更新及删除方法
- 扩展全局类型声明以支持新增的数据结构定义

feat(view): 构建广告位与热门搜索管理界面

- 开发广告位管理表格页面,包含状态切换、表单弹窗等交互逻辑
- 支持上传广告图和背景图,并限制图片大小不超过 3M
- 构建热门搜索标签的基础维护页面,具备基本的增删改查能力
```

zhangtao 3 тижнів тому
батько
коміт
234f1a7e8d

+ 4 - 0
src/components/zt/upload/index.ts

@@ -15,5 +15,9 @@ export interface zuploadProps extends /* @vue-ignore */ UploadProps {
    * 绑定的值
    */
   value?: string;
+  /**
+   * 图片大小
+   */
+  FileSize?: number;
 }
 export { zUpload };

+ 8 - 0
src/components/zt/upload/z-upload.vue

@@ -29,6 +29,12 @@ function handleBeforeUpload(data: { file: UploadFileInfo; fileList: UploadFileIn
       };
     });
   }
+  if (props.FileSize && data.file.file) {
+    if (data.file?.file.size > props.FileSize * 1024 * 1024) {
+      window.$message?.error(`请上传小于${props.FileSize}M的图片`);
+      return Promise.resolve(false);
+    }
+  }
   // 扩展 beforeUpload 验证
   if (typeof props.onBeforeUpload === 'function') {
     return Promise.resolve(props.onBeforeUpload(data as any)).then(res => res ?? true);
@@ -60,6 +66,8 @@ watch(
   () => props,
   () => {
     if (props.value) {
+      console.log('props.value', props.value);
+
       fileListData.value = props.value
         .split(',')
         .map(it => ({ url: it, status: 'finished', id: dayjs().valueOf() + it, name: it }));

+ 4 - 1
src/locales/langs/en-us.ts

@@ -293,7 +293,10 @@ const local: App.I18n.Schema = {
     'store-management': '',
     'store-management_store-info': '',
     'user-management': '',
-    'user-management_user-list': ''
+    'user-management_user-list': '',
+    operation: '',
+    operation_advertisement: '',
+    operation_search: ''
   },
   page: {
     login: {

+ 4 - 1
src/locales/langs/zh-cn.ts

@@ -290,7 +290,10 @@ const local: App.I18n.Schema = {
     'store-management': '',
     'store-management_store-info': '',
     'user-management': '',
-    'user-management_user-list': ''
+    'user-management_user-list': '',
+    operation: '',
+    operation_advertisement: '',
+    operation_search: ''
   },
   page: {
     login: {

+ 2 - 0
src/router/elegant/imports.ts

@@ -35,6 +35,8 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
   manage_role: () => import("@/views/manage/role/index.vue"),
   manage_schedule: () => import("@/views/manage/schedule/index.vue"),
   manage_user: () => import("@/views/manage/user/index.vue"),
+  operation_advertisement: () => import("@/views/operation/advertisement/index.vue"),
+  operation_search: () => import("@/views/operation/search/index.vue"),
   plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
   plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
   plugin_charts_echarts: () => import("@/views/plugin/charts/echarts/index.vue"),

+ 29 - 0
src/router/elegant/routes.ts

@@ -249,6 +249,35 @@ export const generatedRoutes: GeneratedRoute[] = [
       }
     ]
   },
+  {
+    name: 'operation',
+    path: '/operation',
+    component: 'layout.base',
+    meta: {
+      title: 'operation',
+      i18nKey: 'route.operation'
+    },
+    children: [
+      {
+        name: 'operation_advertisement',
+        path: '/operation/advertisement',
+        component: 'view.operation_advertisement',
+        meta: {
+          title: 'operation_advertisement',
+          i18nKey: 'route.operation_advertisement'
+        }
+      },
+      {
+        name: 'operation_search',
+        path: '/operation/search',
+        component: 'view.operation_search',
+        meta: {
+          title: 'operation_search',
+          i18nKey: 'route.operation_search'
+        }
+      }
+    ]
+  },
   {
     name: 'plugin',
     path: '/plugin',

+ 3 - 0
src/router/elegant/transform.ts

@@ -201,6 +201,9 @@ const routeMap: RouteMap = {
   "manage_role": "/manage/role",
   "manage_schedule": "/manage/schedule",
   "manage_user": "/manage/user",
+  "operation": "/operation",
+  "operation_advertisement": "/operation/advertisement",
+  "operation_search": "/operation/search",
   "plugin": "/plugin",
   "plugin_barcode": "/plugin/barcode",
   "plugin_charts": "/plugin/charts",

+ 52 - 0
src/service/api/operation/advertisement/index.ts

@@ -0,0 +1,52 @@
+import { request } from '@/service/request';
+
+/**
+ * 分页获取广告列表
+ * @param params
+ * @returns
+ */
+export function fetchGetadvertInfoList(params: any) {
+  return request<Api.operation.AdvertInfo[]>({
+    url: '/platform/advertInfo/page',
+    method: 'get',
+    params
+  });
+}
+/**
+ * 添加广告
+ * @param params
+ * @returns
+ */
+
+export function fetchAddAdvertInfo(params: any) {
+  return request({
+    url: '/platform/advertInfo',
+    method: 'post',
+    data: params
+  });
+}
+
+/**
+ * 删除广告
+ * @param id
+ */
+export function fetchDeleteAdvertInfo(id: any) {
+  return request({
+    url: `/platform/advertInfo/${id}`,
+    method: 'delete'
+  });
+}
+
+/**
+ * 更新广告
+ * @param params
+ * @returns
+
+ */
+export function fetchUpdateAdvertInfo(params: any) {
+  return request({
+    url: '/platform/advertInfo',
+    method: 'put',
+    data: params
+  });
+}

+ 53 - 0
src/service/api/operation/search/index.ts

@@ -0,0 +1,53 @@
+import { request } from '@/service/request';
+
+/**
+ * 分页获取热门搜索
+ * @param params
+ * @returns
+ */
+export function fetchGethotSearch(params: any) {
+  return request<Api.operation.HotSearch[]>({
+    url: '/platform/hotSearch/page',
+    method: 'get',
+    params
+  });
+}
+/**
+ * 新增热门搜索
+ * @param params
+ * @returns
+ */
+
+export function fetchAddHotSearch(params: any) {
+  return request({
+    url: '/platform/hotSearch',
+    method: 'post',
+    data: params
+  });
+}
+
+/**
+ * 删除热门搜索
+
+*/
+export function fetchDeleteHotSearch(params: any) {
+  return request({
+    url: '/platform/hotSearch',
+    method: 'delete',
+    params
+  });
+}
+
+/**
+ * 修改热门搜索
+ * @param params
+ * @returns
+
+ */
+export function fetchUpdateHotSearch(params: any) {
+  return request({
+    url: '/platform/hotSearch',
+    method: 'put',
+    data: params
+  });
+}

+ 66 - 0
src/typings/api.d.ts

@@ -687,4 +687,70 @@ declare namespace Api {
       [property: string]: any;
     }
   }
+  namespace operation {
+    interface HotSearch {
+      /**
+       * 内容
+       */
+      content?: string;
+      /**
+       * 主键
+       */
+      hotSearchId?: number;
+      /**
+       * 录入时间
+       */
+      recDate?: string;
+      /**
+       * 顺序
+       */
+      seq?: number;
+      /**
+       * 店铺id
+       */
+      shopId?: number;
+      /**
+       * 状态 默认是1,0为下线
+       */
+      status?: number;
+      /**
+       * 标题
+       */
+      title?: string;
+      [property: string]: any;
+    }
+    /**
+     * 广告
+     */
+    interface AdvertInfo {
+      /**
+       * 背景图
+       */
+      advertBack?: string;
+      /**
+       * 广告图片
+       */
+      advertImg?: string;
+      /**
+       * 广告名称
+       */
+      advertName?: string;
+      /**
+       * 排序
+       */
+      advertSort?: number;
+      createTime?: string;
+      deleted?: number;
+      id?: number;
+      /**
+       * 跳转路径
+       */
+      jumpUrl?: string;
+      /**
+       * 状态 1正常 2删除
+       */
+      status?: Api.Common.commonStatus;
+      [property: string]: any;
+    }
+  }
 }

+ 6 - 0
src/typings/elegant-router.d.ts

@@ -55,6 +55,9 @@ declare module "@elegant-router/types" {
     "manage_role": "/manage/role";
     "manage_schedule": "/manage/schedule";
     "manage_user": "/manage/user";
+    "operation": "/operation";
+    "operation_advertisement": "/operation/advertisement";
+    "operation_search": "/operation/search";
     "plugin": "/plugin";
     "plugin_barcode": "/plugin/barcode";
     "plugin_charts": "/plugin/charts";
@@ -148,6 +151,7 @@ declare module "@elegant-router/types" {
     | "iframe-page"
     | "login"
     | "manage"
+    | "operation"
     | "plugin"
     | "pro-naive"
     | "store-management"
@@ -191,6 +195,8 @@ declare module "@elegant-router/types" {
     | "manage_role"
     | "manage_schedule"
     | "manage_user"
+    | "operation_advertisement"
+    | "operation_search"
     | "plugin_barcode"
     | "plugin_charts_antv"
     | "plugin_charts_echarts"

+ 214 - 0
src/views/operation/advertisement/index.vue

@@ -0,0 +1,214 @@
+<script setup lang="tsx">
+import { NButton, NImage, NPopconfirm, NSwitch } from 'naive-ui';
+import {
+  fetchAddAdvertInfo,
+  fetchDeleteAdvertInfo,
+  fetchGetadvertInfoList,
+  fetchUpdateAdvertInfo
+} from '@/service/api/operation/advertisement';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { useModalFrom } from '@/components/zt/ModalForm/hooks/useModalForm';
+
+const columns: NaiveUI.TableColumn<Api.operation.AdvertInfo>[] = [
+  {
+    key: 'advertName',
+    title: '广告位名称',
+    align: 'center'
+  },
+  {
+    key: 'advertImg',
+    title: '广告图',
+    align: 'center',
+    render: row => {
+      return <NImage src={row.advertImg} class={'h90px w180px'}></NImage>;
+    }
+  },
+  {
+    key: 'advertBack',
+    title: '背景图',
+    align: 'center',
+    render: row => {
+      return <NImage src={row.advertBack} class={'h90px w180px'}></NImage>;
+    }
+  },
+  {
+    key: 'jumpUrl',
+    title: '跳转链接',
+    align: 'center'
+  },
+  {
+    key: 'advertSort',
+    title: '排序',
+    align: 'center'
+  },
+  {
+    key: 'status',
+    title: '状态',
+    align: 'center',
+    render: row => {
+      return (
+        <NSwitch
+          uncheckedValue={0}
+          checkedValue={1}
+          value={row.status}
+          onUpdate:value={val => {
+            row.status = val;
+            fetchUpdateAdvertInfo(row);
+          }}
+        ></NSwitch>
+      );
+    }
+  }
+];
+
+const [registerTable, { refresh, setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'advertName',
+        label: '广告位名称',
+        component: 'NInput'
+      },
+      {
+        field: 'status',
+        label: '状态',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            {
+              label: '启用',
+              value: 1
+            },
+            {
+              label: '禁用',
+              value: 0
+            }
+          ]
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '广告位列表',
+    showAddButton: true
+  }
+});
+
+async function handleDelete(row: Recordable) {
+  setTableLoading(true);
+  await fetchDeleteAdvertInfo(row.id);
+  refresh();
+}
+const [registerModalForm, { openModal, closeModal, getFieldsValue, setFieldsValue }] = useModalFrom({
+  modalConfig: {
+    title: '广告位',
+    width: 800,
+    isShowHeaderText: true,
+    height: 600
+  },
+  formConfig: {
+    schemas: [
+      {
+        label: '',
+        field: 'id',
+        component: 'NInput',
+        show: false
+      },
+      {
+        field: 'advertName',
+        label: '广告位名称',
+        component: 'NInput',
+        required: true
+      },
+      {
+        field: 'advertImg',
+        label: '广告图',
+        component: 'zUpload',
+        required: true,
+        componentProps: {
+          tipText: '单张,仅支持png,jpg,jpeg格式,建议尺寸比例 16:9;图片大小不能超过3m。',
+          max: 1,
+          FileSize: 3
+        }
+      },
+      {
+        field: 'advertBack',
+        label: '背景图',
+        component: 'zUpload',
+        required: true,
+        componentProps: {
+          tipText: '单张,仅支持png,jpg,jpeg格式,建议尺寸比例 16:9;图片大小不能超过3m。',
+          max: 1,
+          FileSize: 3
+        }
+      },
+      {
+        field: 'jumpUrl',
+        label: '跳转链接',
+        component: 'NInput',
+        required: true
+      },
+      {
+        field: 'advertSort',
+        label: '排序',
+        component: 'NInputNumber',
+        required: true
+      },
+      {
+        field: 'status',
+        label: '状态',
+        component: 'NSwitch',
+        required: true,
+        componentProps: {
+          checkedValue: 1,
+          uncheckedValue: 0
+        },
+        defaultValue: 1
+      }
+    ],
+    gridProps: {
+      cols: '1'
+    },
+    labelWidth: 120
+  }
+});
+async function handleSubmit() {
+  const form = await getFieldsValue();
+  if (form.id) {
+    await fetchUpdateAdvertInfo(form);
+  } else {
+    await fetchAddAdvertInfo(form);
+  }
+  closeModal();
+  refresh();
+}
+
+async function edit(row: Recordable) {
+  openModal(row);
+  setFieldsValue(row);
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :api="fetchGetadvertInfoList" @register="registerTable" @add="openModal">
+      <template #op="{ row }">
+        <NButton size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
+        <NPopconfirm @positive-click="handleDelete(row)">
+          <template #trigger>
+            <NButton size="small" type="error" ghost>删除</NButton>
+          </template>
+          确定删除吗?
+        </NPopconfirm>
+      </template>
+    </ZTable>
+    <BasicModelForm @register-modal-form="registerModalForm" @submit-form="handleSubmit"></BasicModelForm>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 114 - 0
src/views/operation/search/index.vue

@@ -0,0 +1,114 @@
+<script setup lang="tsx">
+import { NButton, NPopconfirm } from 'naive-ui';
+import type { InternalRowData } from 'naive-ui/es/data-table/src/interface';
+import {
+  fetchAddHotSearch,
+  fetchDeleteHotSearch,
+  fetchGethotSearch,
+  fetchUpdateHotSearch
+} from '@/service/api/operation/search';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { useModalFrom } from '@/components/zt/ModalForm/hooks/useModalForm';
+
+const columns: NaiveUI.TableColumn<InternalRowData>[] = [
+  {
+    key: 'name',
+    title: '名称',
+    align: 'center'
+  },
+  {
+    key: 'name',
+    title: '类型',
+    align: 'center'
+  }
+];
+
+const [registerTable, { refresh, setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'name',
+        label: '标签名称',
+        component: 'NInput'
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '标签列表',
+    showAddButton: true
+  }
+});
+
+async function handleDelete(row: Recordable) {
+  setTableLoading(true);
+  await fetchDeleteHotSearch([row.id]);
+  refresh();
+}
+const [registerModalForm, { openModal, closeModal, getFieldsValue, setFieldsValue }] = useModalFrom({
+  modalConfig: {
+    title: '标签 ',
+    width: 400,
+    isShowHeaderText: true,
+    height: 100
+  },
+  formConfig: {
+    schemas: [
+      {
+        label: '',
+        field: 'id',
+        component: 'NInput',
+        show: false
+      },
+      {
+        field: 'name',
+        label: '标签名称',
+        component: 'NInput',
+        required: true
+      }
+    ],
+    gridProps: {
+      cols: '1'
+    },
+    labelWidth: 120
+  }
+});
+async function handleSubmit() {
+  const form = await getFieldsValue();
+  if (form.id) {
+    await fetchUpdateHotSearch(form);
+  } else {
+    await fetchAddHotSearch(form);
+  }
+  closeModal();
+  refresh();
+}
+
+async function edit(row: Recordable) {
+  openModal(row);
+  setFieldsValue(row);
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :api="fetchGethotSearch" @register="registerTable" @add="openModal">
+      <template #op="{ row }">
+        <NButton size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
+        <NPopconfirm @positive-click="handleDelete(row)">
+          <template #trigger>
+            <NButton size="small" type="error" ghost>删除</NButton>
+          </template>
+          确定删除吗?
+        </NPopconfirm>
+      </template>
+    </ZTable>
+    <BasicModelForm @register-modal-form="registerModalForm" @submit-form="handleSubmit"></BasicModelForm>
+  </LayoutTable>
+</template>
+
+<style scoped></style>