Prechádzať zdrojové kódy

feat: 新增商品主库功能,替换欢迎页为Dashboard首页

移除旧欢迎页(welcome)及其图表/表格组件,新增商品主库(master-library)
模块。修复菜单层级扁平化、rank排序和icon配置。增强ProductController
支持主库商品检索与加入商户库。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 týždňov pred
rodič
commit
8ece8b8cc5
41 zmenil súbory, kde vykonal 675 pridanie a 936 odobranie
  1. 6 3
      docs/database/seed_menu.sql
  2. 0 1
      haha-admin-web/locales/en.yaml
  3. 0 1
      haha-admin-web/locales/zh-CN.yaml
  4. 16 0
      haha-admin-web/src/api/product.ts
  5. 1 14
      haha-admin-web/src/layout/types.ts
  6. 1 7
      haha-admin-web/src/router/index.ts
  7. 1 1
      haha-admin-web/src/router/modules/distribution.ts
  8. 2 18
      haha-admin-web/src/router/modules/home.ts
  9. 2 2
      haha-admin-web/src/router/modules/layer-template.ts
  10. 1 1
      haha-admin-web/src/router/modules/marketing.ts
  11. 9 0
      haha-admin-web/src/router/modules/product.ts
  12. 1 1
      haha-admin-web/src/router/modules/replenishmentOrder.ts
  13. 1 1
      haha-admin-web/src/router/modules/statistics.ts
  14. 1 1
      haha-admin-web/src/router/modules/system.ts
  15. 5 2
      haha-admin-web/src/router/utils.ts
  16. 5 4
      haha-admin-web/src/views/dashboard/index.vue
  17. 145 0
      haha-admin-web/src/views/product/master-library/index.vue
  18. 187 0
      haha-admin-web/src/views/product/master-library/utils/hook.tsx
  19. 19 0
      haha-admin-web/src/views/product/master-library/utils/types.ts
  20. 0 108
      haha-admin-web/src/views/welcome/components/charts/ChartBar.vue
  21. 0 62
      haha-admin-web/src/views/welcome/components/charts/ChartLine.vue
  22. 0 73
      haha-admin-web/src/views/welcome/components/charts/ChartRound.vue
  23. 0 3
      haha-admin-web/src/views/welcome/components/charts/index.ts
  24. 0 104
      haha-admin-web/src/views/welcome/components/table/columns.tsx
  25. 0 1
      haha-admin-web/src/views/welcome/components/table/empty.svg
  26. 0 71
      haha-admin-web/src/views/welcome/components/table/index.vue
  27. 0 134
      haha-admin-web/src/views/welcome/data.ts
  28. 0 277
      haha-admin-web/src/views/welcome/index.vue
  29. 0 6
      haha-admin-web/src/views/welcome/utils.ts
  30. 6 1
      haha-admin-web/vite.config.ts
  31. 37 0
      haha-admin/src/main/java/com/haha/admin/controller/ProductController.java
  32. 10 0
      haha-admin/src/main/resources/sql/menu_add_master_library.sql
  33. 27 0
      haha-admin/src/main/resources/sql/menu_fix_ranks_and_icons.sql
  34. 16 0
      haha-admin/src/main/resources/sql/menu_flatten_single_child.sql
  35. 15 0
      haha-entity/src/main/java/com/haha/entity/dto/AddToMerchantDTO.java
  36. 32 0
      haha-entity/src/main/java/com/haha/entity/dto/MasterProductQueryDTO.java
  37. 22 0
      haha-service/src/main/java/com/haha/service/ProductService.java
  38. 46 33
      haha-service/src/main/java/com/haha/service/impl/DistributorReportServiceImpl.java
  39. 20 5
      haha-service/src/main/java/com/haha/service/impl/MenuServiceImpl.java
  40. 1 1
      haha-service/src/main/java/com/haha/service/impl/NewProductApplyServiceImpl.java
  41. 40 0
      haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

+ 6 - 3
docs/database/seed_menu.sql

@@ -11,7 +11,7 @@ DELETE FROM t_menu WHERE id IN (2001,2002,2003,2004,3001,3002,4001,4002,5001,500
 -- 1. 数据概览 (Dashboard) - rank 1
 -- =====================================================
 INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, icon, show_link, show_parent, `redirect`, keep_alive) VALUES
-(100, 0, 0, '数据概览', 'Dashboard', '/dashboard', '', 1, 'ri:dashboard-line', 1, 1, '/dashboard/index', 1);
+(100, 0, 0, '数据概览', 'Dashboard', '/dashboard', '', 1, 'ri:dashboard-line', 1, 0, '/dashboard/index', 1);
 
 INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, show_link, show_parent) VALUES
 (101, 100, 0, '数据概览', 'Dashboard', '/dashboard/index', '', 1, 1, 0);
@@ -52,6 +52,8 @@ INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `ran
 (401, 400, 0, '商品列表', 'ProductList', '/product/list', '', 1, 1, 0);
 INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, show_link, show_parent) VALUES
 (402, 400, 0, '新品申请', 'NewProductApply', '/product/new-apply', '', 2, 1, 0);
+INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, show_link, show_parent) VALUES
+(403, 400, 0, '商品总库', 'MasterLibrary', '/product/master-library', '', 3, 1, 0);
 
 -- =====================================================
 -- 5. 库存管理 (Inventory) - rank 5
@@ -70,7 +72,7 @@ INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `ran
 -- 6. 客户管理 (Customer) - rank 6
 -- =====================================================
 INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, icon, show_link, show_parent, `redirect`, keep_alive) VALUES
-(600, 0, 0, '客户管理', 'Customer', '/customer', '', 6, 'ri:user-heart-line', 1, 1, '/customer/list', 1);
+(600, 0, 0, '客户管理', 'Customer', '/customer', '', 6, 'ri:user-heart-line', 1, 0, '/customer/list', 1);
 
 INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, show_link, show_parent) VALUES
 (601, 600, 0, '客户列表', 'CustomerList', '/customer/list', '', 1, 1, 0);
@@ -182,4 +184,5 @@ INSERT INTO t_role_menu (id, role_id, menu_id) VALUES
 (90033, 1, 810), (90034, 1, 811), (90035, 1, 812), (90036, 1, 813), (90037, 1, 814),
 (90038, 1, 900), (90039, 1, 901), (90040, 1, 902), (90041, 1, 903), (90042, 1, 904),
 (90043, 1, 1000), (90044, 1, 1001), (90045, 1, 1002), (90046, 1, 1003), (90047, 1, 1004), (90048, 1, 1005), (90049, 1, 1006), (90050, 1, 1007), (90051, 1, 1008),
-(90052, 1, 1100);
+(90052, 1, 1100),
+(90053, 1, 403);

+ 0 - 1
haha-admin-web/locales/en.yaml

@@ -62,7 +62,6 @@ panel:
   pureHiddenFooter: Hidden Footer
   pureMultiTagsCache: MultiTags Cache
 menus:
-  pureHome: Home
   pureLogin: Login
   pureEmpty: Empty Page
   pureTable: Table

+ 0 - 1
haha-admin-web/locales/zh-CN.yaml

@@ -62,7 +62,6 @@ panel:
   pureHiddenFooter: 隐藏页脚
   pureMultiTagsCache: 页签持久化
 menus:
-  pureHome: 首页
   pureLogin: 登录
   pureEmpty: 无Layout页
   pureTable: 表格

+ 16 - 0
haha-admin-web/src/api/product.ts

@@ -51,3 +51,19 @@ export const deleteProduct = (id: number) => {
 export const syncProduct = (id: number) => {
   return http.request<Result>("post", `/products/${id}/sync`);
 };
+
+// 商品总库
+export const getMasterProductList = (params: {
+  page?: number;
+  pageSize?: number;
+  barcode?: string;
+  code?: string;
+  productName?: string;
+  deviceAdapt?: string;
+}) => {
+  return http.request<ResultTable>("get", "/products/master/list", { params });
+};
+
+export const addToMerchantProducts = (data: { codeList: string }) => {
+  return http.request<Result>("post", "/products/master/add-to-merchant", { data });
+};

+ 1 - 14
haha-admin-web/src/layout/types.ts

@@ -1,19 +1,6 @@
 import type { FunctionalComponent } from "vue";
-const { VITE_HIDE_HOME } = import.meta.env;
 
-export const routerArrays: Array<RouteConfigs> =
-  VITE_HIDE_HOME === "false"
-    ? [
-        {
-          path: "/welcome",
-          name: "Welcome",
-          meta: {
-            title: "menus.pureHome",
-            icon: "ep/home-filled"
-          }
-        }
-      ]
-    : [];
+export const routerArrays: Array<RouteConfigs> = [];
 
 export type routeMetaType = {
   title?: string;

+ 1 - 7
haha-admin-web/src/router/index.ts

@@ -118,8 +118,6 @@ export function resetRouter() {
 /** 路由白名单 */
 const whiteList = ["/login"];
 
-const { VITE_HIDE_HOME } = import.meta.env;
-
 router.beforeEach((to: ToRouteType, _from) => {
   to.meta.loaded = loadedPaths.has(to.path);
 
@@ -154,10 +152,6 @@ router.beforeEach((to: ToRouteType, _from) => {
     if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
       return { path: "/error/403" };
     }
-    // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
-    if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") {
-      return { path: "/error/404" };
-    }
     if (_from?.name) {
       // name为超链接
       if (externalLink) {
@@ -178,7 +172,7 @@ router.beforeEach((to: ToRouteType, _from) => {
             const { path } = to;
             const route = findRouteByPath(
               path,
-              router.options.routes[0].children
+              router.options.routes[0]?.children ?? []
             );
             getTopMenu(true);
             // query、params模式路由传参数的标签页不在此处处理

+ 1 - 1
haha-admin-web/src/router/modules/distribution.ts

@@ -20,7 +20,7 @@ export default {
   meta: {
     icon: "ep:share",
     title: "分销管理",
-    rank: 6,
+    rank: 7,
     roles: ["admin"]
   },
   children: [

+ 2 - 18
haha-admin-web/src/router/modules/home.ts

@@ -1,27 +1,11 @@
-import { $t } from "@/plugins/i18n";
 import { home } from "@/router/enums";
-const { VITE_HIDE_HOME } = import.meta.env;
 const Layout = () => import("@/layout/index.vue");
 
 export default {
   path: "/",
-  name: "Home",
   component: Layout,
-  redirect: "/welcome",
+  redirect: "/dashboard/index",
   meta: {
-    icon: "ep/home-filled",
-    title: $t("menus.pureHome"),
     rank: home
-  },
-  children: [
-    {
-      path: "/welcome",
-      name: "Welcome",
-      component: () => import("@/views/welcome/index.vue"),
-      meta: {
-        title: $t("menus.pureHome"),
-        showLink: VITE_HIDE_HOME === "true" ? false : true
-      }
-    }
-  ]
+  }
 } satisfies RouteConfigsTable;

+ 2 - 2
haha-admin-web/src/router/modules/layer-template.ts

@@ -4,8 +4,8 @@ export default {
   name: "LayerTemplate",
   meta: {
     title: "设备层模版管理",
-    icon: "ri:stack-line",
-    rank: 8,
+    icon: "ri:layout-grid-line",
+    rank: 11,
     roles: ["admin"]
   }
 } satisfies RouteConfigsTable;

+ 1 - 1
haha-admin-web/src/router/modules/marketing.ts

@@ -16,7 +16,7 @@ export default {
   meta: {
     icon: "ri:megaphone-line",
     title: "营销中心",
-    rank: 7,
+    rank: 8,
     roles: ["admin", "operator"]
   },
   children: [

+ 9 - 0
haha-admin-web/src/router/modules/product.ts

@@ -25,6 +25,15 @@ export default {
         icon: "ri:add-box-line",
         title: "新品申请"
       }
+    },
+    {
+      path: "/product/master-library",
+      name: "MasterLibrary",
+      component: () => import("@/views/product/master-library/index.vue"),
+      meta: {
+        icon: "ri:archive-line",
+        title: "商品总库"
+      }
     }
   ]
 } satisfies RouteConfigsTable;

+ 1 - 1
haha-admin-web/src/router/modules/replenishmentOrder.ts

@@ -6,7 +6,7 @@ export default {
   meta: {
     icon: "ep:shopping-cart",
     title: "设备补货单",
-    rank: 9,
+    rank: 12,
     roles: ["admin"]
   },
   children: [

+ 1 - 1
haha-admin-web/src/router/modules/statistics.ts

@@ -4,7 +4,7 @@ export default {
   meta: {
     icon: "ri:bar-chart-grouped-line",
     title: "统计报表",
-    rank: 8,
+    rank: 9,
     roles: ["admin", "operator", "viewer"]
   },
   children: [

+ 1 - 1
haha-admin-web/src/router/modules/system.ts

@@ -4,7 +4,7 @@ export default {
   meta: {
     icon: "ri:settings-3-line",
     title: "系统设置",
-    rank: 8,
+    rank: 10,
     roles: ["admin"]
   },
   children: [

+ 5 - 2
haha-admin-web/src/router/utils.ts

@@ -384,6 +384,7 @@ function hasAuth(value: string | Array<string>): boolean {
 }
 
 function handleTopMenu(route) {
+  if (!route) return route;
   if (route?.children && route.children.length > 1) {
     if (route.redirect) {
       return route.children.filter(cur => cur.path === route.redirect)[0];
@@ -398,9 +399,11 @@ function handleTopMenu(route) {
 /** 获取所有菜单中的第一个菜单(顶级菜单)*/
 function getTopMenu(tag = false): menuType {
   const topMenu = handleTopMenu(
-    usePermissionStoreHook().wholeMenus[0]?.children[0]
+    usePermissionStoreHook().wholeMenus[0]?.children?.[0]
   );
-  tag && useMultiTagsStoreHook().handleTags("push", topMenu);
+  if (tag && topMenu) {
+    useMultiTagsStoreHook().handleTags("push", topMenu);
+  }
   return topMenu;
 }
 

+ 5 - 4
haha-admin-web/src/views/dashboard/index.vue

@@ -94,14 +94,15 @@ const deviceOfflineCount = computed(() => {
 });
 
 const formatMoney = (value: number) => {
-  if (value >= 10000) {
-    return (value / 10000).toFixed(2) + "万";
+  const num = Number(value) || 0;
+  if (num >= 10000) {
+    return (num / 10000).toFixed(2) + "万";
   }
-  return value.toFixed(2);
+  return num.toFixed(2);
 };
 
 const getValue = (key: string, isMoney: boolean = false) => {
-  const value = overviewData.value?.[key] || 0;
+  const value = overviewData.value?.[key] ?? 0;
   return isMoney ? formatMoney(value) : value;
 };
 

+ 145 - 0
haha-admin-web/src/views/product/master-library/index.vue

@@ -0,0 +1,145 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useMasterLibrary } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import Refresh from "~icons/ep/refresh";
+import AddFill from "~icons/ri/add-circle-line";
+
+defineOptions({
+  name: "MasterLibrary"
+});
+
+const formRef = ref();
+const tableRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  deviceAdaptOptions,
+  onSearch,
+  resetForm,
+  handleSizeChange,
+  handleCurrentChange,
+  handleSelectionChange,
+  handleAddToMerchant
+} = useMasterLibrary(tableRef);
+</script>
+
+<template>
+  <div class="main">
+    <el-form
+      ref="formRef"
+      :inline="true"
+      :model="form"
+      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
+    >
+      <el-form-item label="商品名称:" prop="productName">
+        <el-input
+          v-model="form.productName"
+          placeholder="请输入商品名称"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="条码:" prop="barcode">
+        <el-input
+          v-model="form.barcode"
+          placeholder="请输入条码"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="商品编码:" prop="code">
+        <el-input
+          v-model="form.code"
+          placeholder="请输入商品编码"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="适用设备:" prop="deviceAdapt">
+        <el-select
+          v-model="form.deviceAdapt"
+          placeholder="请选择适用设备"
+          clearable
+          class="w-[180px]!"
+        >
+          <el-option
+            v-for="item in deviceAdaptOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon('ri/search-line')"
+          :loading="loading"
+          @click="onSearch"
+        >
+          搜索
+        </el-button>
+        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <PureTableBar
+      title="商品总库"
+      :columns="columns"
+      @refresh="onSearch"
+    >
+      <template #buttons>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="handleAddToMerchant"
+        >
+          添加到商家商品库
+        </el-button>
+      </template>
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          row-key="code"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :pagination="{ ...pagination, size }"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @page-size-change="handleSizeChange"
+          @page-current-change="handleCurrentChange"
+          @selection-change="handleSelectionChange"
+        >
+        </pure-table>
+      </template>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 187 - 0
haha-admin-web/src/views/product/master-library/utils/hook.tsx

@@ -0,0 +1,187 @@
+import { message } from "@/utils/message";
+import type { PaginationProps } from "@pureadmin/table";
+import EnhancedImage from "../../components/EnhancedImage.vue";
+import { getMasterProductList, addToMerchantProducts } from "@/api/product";
+import { type Ref, ref, reactive, onMounted } from "vue";
+import { ElMessageBox } from "element-plus";
+import type { MasterProductItem, MasterProductSearchForm } from "./types";
+
+export function useMasterLibrary(tableRef: Ref) {
+  const form = reactive<MasterProductSearchForm>({
+    productName: "",
+    barcode: "",
+    code: "",
+    deviceAdapt: ""
+  });
+  const formRef = ref();
+  const dataList = ref<MasterProductItem[]>([]);
+  const loading = ref(true);
+  const selectedRows = ref<MasterProductItem[]>([]);
+  const pagination = reactive<PaginationProps>({
+    total: 0,
+    pageSize: 10,
+    currentPage: 1,
+    background: true,
+    pageSizes: [10, 20, 50, 100],
+    layout: "total, sizes, prev, pager, next, jumper"
+  });
+
+  const deviceAdaptOptions = [
+    { label: "全部", value: "" },
+    { label: "静态柜", value: "STATIC" },
+    { label: "动态柜", value: "DYNAMIC" }
+  ];
+
+  const columns: TableColumnList = [
+    {
+      type: "selection",
+      width: 50
+    },
+    {
+      label: "商品图片",
+      prop: "pic",
+      width: 80,
+      cellRenderer: ({ row }) => (
+        <EnhancedImage
+          src={row.pic || "/img/placeholder.png"}
+          alt={row.name}
+          width={50}
+          height={50}
+          fit="cover"
+          preview={true}
+          lazy={true}
+        />
+      )
+    },
+    {
+      label: "商品编码",
+      prop: "code",
+      minWidth: 100
+    },
+    {
+      label: "商品名称",
+      prop: "name",
+      minWidth: 120
+    },
+    {
+      label: "条码",
+      prop: "barcode",
+      minWidth: 100
+    },
+    {
+      label: "适用设备",
+      prop: "deviceAdapt",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        const parts: string[] = [];
+        if (row.static_status === "1") parts.push("静态柜");
+        if (row.dynamic_status === "1") parts.push("动态柜");
+        return <span>{parts.join("、") || "-"}</span>;
+      }
+    },
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    selectedRows.value = [];
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+      if (form.productName) searchParams.productName = form.productName;
+      if (form.barcode) searchParams.barcode = form.barcode;
+      if (form.code) searchParams.code = form.code;
+      if (form.deviceAdapt) searchParams.deviceAdapt = form.deviceAdapt;
+
+      const { data } = await getMasterProductList(searchParams);
+      dataList.value = (data.list || []) as MasterProductItem[];
+      pagination.total = Number(data.total ?? 0);
+    } catch (error) {
+      console.error("获取商品总库列表失败:", error);
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    }
+  }
+
+  function resetForm(formEl: any) {
+    if (!formEl) return;
+    formEl.resetFields();
+    pagination.currentPage = 1;
+    onSearch();
+  }
+
+  function handleSizeChange(val: number) {
+    pagination.pageSize = val;
+    onSearch();
+  }
+
+  function handleCurrentChange(val: number) {
+    pagination.currentPage = val;
+    onSearch();
+  }
+
+  function handleSelectionChange(rows: MasterProductItem[]) {
+    selectedRows.value = rows;
+  }
+
+  async function handleAddToMerchant() {
+    if (selectedRows.value.length === 0) {
+      message("请先选择要添加的商品", { type: "warning" });
+      return;
+    }
+
+    const codes = selectedRows.value.map(r => r.code);
+    try {
+      await ElMessageBox.confirm(
+        `确认将以下 ${codes.length} 个商品添加到商家商品库?`,
+        "系统提示",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }
+      );
+
+      const res = await addToMerchantProducts({
+        codeList: codes.join(",")
+      });
+      if (res.code === 0) {
+        message(`成功添加 ${codes.length} 个商品到商家商品库`, {
+          type: "success"
+        });
+        onSearch();
+      } else {
+        message(res.message || "添加失败", { type: "error" });
+      }
+    } catch (error) {
+      // 用户取消或请求失败
+      if (error !== "cancel") {
+        message("添加失败", { type: "error" });
+      }
+    }
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    formRef,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    selectedRows,
+    deviceAdaptOptions,
+    onSearch,
+    resetForm,
+    handleSizeChange,
+    handleCurrentChange,
+    handleSelectionChange,
+    handleAddToMerchant
+  };
+}

+ 19 - 0
haha-admin-web/src/views/product/master-library/utils/types.ts

@@ -0,0 +1,19 @@
+// 商品总库列表项(字段名与哈哈平台API返回的snake_case保持一致)
+export interface MasterProductItem {
+  code: string;
+  name: string;
+  barcode: string;
+  pic: string;
+  planogram?: string;
+  static_status?: string;
+  dynamic_status?: string;
+  is_open_client?: string;
+}
+
+// 搜索表单
+export interface MasterProductSearchForm {
+  productName: string;
+  barcode: string;
+  code: string;
+  deviceAdapt: string;
+}

+ 0 - 108
haha-admin-web/src/views/welcome/components/charts/ChartBar.vue

@@ -1,108 +0,0 @@
-<script setup lang="ts">
-import { useDark, useECharts } from "@pureadmin/utils";
-import { type PropType, ref, computed, watch, nextTick } from "vue";
-
-const props = defineProps({
-  requireData: {
-    type: Array as PropType<Array<number>>,
-    default: () => []
-  },
-  questionData: {
-    type: Array as PropType<Array<number>>,
-    default: () => []
-  }
-});
-
-const { isDark } = useDark();
-
-const theme = computed(() => (isDark.value ? "dark" : "light"));
-
-const chartRef = ref();
-const { setOptions } = useECharts(chartRef, {
-  theme
-});
-
-watch(
-  () => props,
-  async () => {
-    await nextTick(); // 确保DOM更新完成后再执行
-    setOptions({
-      container: ".bar-card",
-      color: ["#41b6ff", "#e85f33"],
-      tooltip: {
-        trigger: "axis",
-        axisPointer: {
-          type: "none"
-        }
-      },
-      grid: {
-        top: "20px",
-        left: "50px",
-        right: 0
-      },
-      legend: {
-        data: ["需求人数", "提问数量"],
-        textStyle: {
-          color: "#606266",
-          fontSize: "0.875rem"
-        },
-        bottom: 0
-      },
-      xAxis: [
-        {
-          type: "category",
-          data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
-          axisLabel: {
-            fontSize: "0.875rem"
-          },
-          axisPointer: {
-            type: "shadow"
-          }
-        }
-      ],
-      yAxis: [
-        {
-          type: "value",
-          axisLabel: {
-            fontSize: "0.875rem"
-          },
-          splitLine: {
-            show: false // 去网格线
-          }
-          // name: "单位: 个"
-        }
-      ],
-      series: [
-        {
-          name: "需求人数",
-          type: "bar",
-          barWidth: 10,
-          itemStyle: {
-            color: "#41b6ff",
-            borderRadius: [10, 10, 0, 0]
-          },
-          data: props.requireData
-        },
-        {
-          name: "提问数量",
-          type: "bar",
-          barWidth: 10,
-          itemStyle: {
-            color: "#e86033ce",
-            borderRadius: [10, 10, 0, 0]
-          },
-          data: props.questionData
-        }
-      ]
-    });
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-);
-</script>
-
-<template>
-  <div ref="chartRef" style="width: 100%; height: 365px" />
-</template>

+ 0 - 62
haha-admin-web/src/views/welcome/components/charts/ChartLine.vue

@@ -1,62 +0,0 @@
-<script setup lang="ts">
-import { type PropType, ref, computed } from "vue";
-import { useDark, useECharts } from "@pureadmin/utils";
-
-const props = defineProps({
-  data: {
-    type: Array as PropType<Array<number>>,
-    default: () => []
-  },
-  color: {
-    type: String,
-    default: "#41b6ff"
-  }
-});
-
-const { isDark } = useDark();
-
-const theme = computed(() => (isDark.value ? "dark" : "light"));
-
-const chartRef = ref();
-const { setOptions } = useECharts(chartRef, {
-  theme,
-  renderer: "svg"
-});
-
-setOptions({
-  container: ".line-card",
-  xAxis: {
-    type: "category",
-    show: false,
-    data: props.data
-  },
-  grid: {
-    top: "15px",
-    bottom: 0,
-    left: 0,
-    right: 0
-  },
-  yAxis: {
-    show: false,
-    type: "value"
-  },
-  series: [
-    {
-      data: props.data,
-      type: "line",
-      symbol: "none",
-      smooth: true,
-      color: props.color,
-      lineStyle: {
-        shadowOffsetY: 3,
-        shadowBlur: 7,
-        shadowColor: props.color
-      }
-    }
-  ]
-});
-</script>
-
-<template>
-  <div ref="chartRef" style="width: 100%; height: 60px" />
-</template>

+ 0 - 73
haha-admin-web/src/views/welcome/components/charts/ChartRound.vue

@@ -1,73 +0,0 @@
-<script setup lang="ts">
-import { ref, computed } from "vue";
-import { useDark, useECharts } from "@pureadmin/utils";
-
-const { isDark } = useDark();
-
-const theme = computed(() => (isDark.value ? "dark" : "light"));
-
-const chartRef = ref();
-const { setOptions } = useECharts(chartRef, {
-  theme,
-  renderer: "svg"
-});
-
-setOptions({
-  container: ".line-card",
-  title: {
-    text: "100%",
-    left: "47%",
-    top: "30%",
-    textAlign: "center",
-    textStyle: {
-      fontSize: "16",
-      fontWeight: 600
-    }
-  },
-  polar: {
-    radius: ["100%", "90%"],
-    center: ["50%", "50%"]
-  },
-  angleAxis: {
-    max: 100,
-    show: false
-  },
-  radiusAxis: {
-    type: "category",
-    show: true,
-    axisLabel: {
-      show: false
-    },
-    axisLine: {
-      show: false
-    },
-    axisTick: {
-      show: false
-    }
-  },
-  series: [
-    {
-      type: "bar",
-      roundCap: true,
-      barWidth: 2,
-      showBackground: true,
-      backgroundStyle: {
-        color: "#dfe7ef"
-      },
-      data: [100],
-      coordinateSystem: "polar",
-      color: "#7846e5",
-      itemStyle: {
-        shadowBlur: 2,
-        shadowColor: "#7846e5",
-        shadowOffsetX: 0,
-        shadowOffsetY: 0
-      }
-    }
-  ]
-});
-</script>
-
-<template>
-  <div ref="chartRef" style="width: 100%; height: 60px" />
-</template>

+ 0 - 3
haha-admin-web/src/views/welcome/components/charts/index.ts

@@ -1,3 +0,0 @@
-export { default as ChartBar } from "./ChartBar.vue";
-export { default as ChartLine } from "./ChartLine.vue";
-export { default as ChartRound } from "./ChartRound.vue";

+ 0 - 104
haha-admin-web/src/views/welcome/components/table/columns.tsx

@@ -1,104 +0,0 @@
-import { tableData } from "../../data";
-import { delay } from "@pureadmin/utils";
-import { ref, onMounted, reactive } from "vue";
-import type { PaginationProps } from "@pureadmin/table";
-import ThumbUp from "~icons/ri/thumb-up-line";
-import Hearts from "~icons/ri/hearts-line";
-import Empty from "./empty.svg?component";
-
-export function useColumns() {
-  const dataList = ref([]);
-  const loading = ref(true);
-  const columns: TableColumnList = [
-    {
-      sortable: true,
-      label: "序号",
-      prop: "id"
-    },
-    {
-      sortable: true,
-      label: "需求人数",
-      prop: "requiredNumber",
-      filterMultiple: false,
-      filterClassName: "pure-table-filter",
-      filters: [
-        { text: "≥16000", value: "more" },
-        { text: "<16000", value: "less" }
-      ],
-      filterMethod: (value, { requiredNumber }) => {
-        return value === "more"
-          ? requiredNumber >= 16000
-          : requiredNumber < 16000;
-      }
-    },
-    {
-      sortable: true,
-      label: "提问数量",
-      prop: "questionNumber"
-    },
-    {
-      sortable: true,
-      label: "解决数量",
-      prop: "resolveNumber"
-    },
-    {
-      sortable: true,
-      label: "用户满意度",
-      minWidth: 100,
-      prop: "satisfaction",
-      cellRenderer: ({ row }) => (
-        <div class="flex justify-center w-full">
-          <span class="flex items-center w-[60px]">
-            <span class="ml-auto mr-2">{row.satisfaction}%</span>
-            <iconifyIconOffline
-              icon={row.satisfaction > 98 ? Hearts : ThumbUp}
-              color="#e85f33"
-            />
-          </span>
-        </div>
-      )
-    },
-    {
-      sortable: true,
-      label: "统计日期",
-      prop: "date"
-    },
-    {
-      label: "操作",
-      fixed: "right",
-      slot: "operation"
-    }
-  ];
-
-  /** 分页配置 */
-  const pagination = reactive<PaginationProps>({
-    pageSize: 10,
-    currentPage: 1,
-    layout: "prev, pager, next",
-    total: 0,
-    align: "center"
-  });
-
-  function onCurrentChange(page: number) {
-    console.log("onCurrentChange", page);
-    loading.value = true;
-    delay(300).then(() => {
-      loading.value = false;
-    });
-  }
-
-  onMounted(() => {
-    dataList.value = tableData;
-    pagination.total = dataList.value.length;
-    loading.value = false;
-  });
-
-  return {
-    Empty,
-    loading,
-    columns,
-    dataList,
-    pagination,
-    onCurrentChange
-  };
-}

+ 0 - 1
haha-admin-web/src/views/welcome/components/table/empty.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" class="empty-icon" viewBox="0 0 1024 1024"><path d="M855.6 427.2H168.5c-12.7 0-24.4 6.9-30.6 18L4.4 684.7C1.5 689.9 0 695.8 0 701.8v287.1c0 19.4 15.7 35.1 35.1 35.1H989c19.4 0 35.1-15.7 35.1-35.1V701.8c0-6-1.5-11.8-4.4-17.1L886.2 445.2c-6.2-11.1-17.9-18-30.6-18M673.4 695.6c-16.5 0-30.8 11.5-34.3 27.7-12.7 58.5-64.8 102.3-127.2 102.3s-114.5-43.8-127.2-102.3c-3.5-16.1-17.8-27.7-34.3-27.7H119c-26.4 0-43.3-28-31.1-51.4l81.7-155.8c6.1-11.6 18-18.8 31.1-18.8h622.4c13 0 25 7.2 31.1 18.8l81.7 155.8c12.2 23.4-4.7 51.4-31.1 51.4zm146.5-486.1c-1-1.8-2.1-3.7-3.2-5.5-9.8-16.6-31.1-22.2-47.8-12.6L648.5 261c-17 9.8-22.7 31.6-12.6 48.4.9 1.4 1.7 2.9 2.5 4.4 9.5 17 31.2 22.8 48 13L807 257.3c16.7-9.7 22.4-31 12.9-47.8m-444.5 51.6L255 191.6c-16.7-9.6-38-4-47.8 12.6-1.1 1.8-2.1 3.6-3.2 5.5-9.5 16.8-3.8 38.1 12.9 47.8L337.3 327c16.9 9.7 38.6 4 48-13.1.8-1.5 1.7-2.9 2.5-4.4 10.2-16.8 4.5-38.6-12.4-48.4M512 239.3h2.5c19.5.3 35.5-15.5 35.5-35.1v-139c0-19.3-15.6-34.9-34.8-35.1h-6.4C489.6 30.3 474 46 474 65.2v139c0 19.5 15.9 35.4 35.5 35.1z"/></svg>

+ 0 - 71
haha-admin-web/src/views/welcome/components/table/index.vue

@@ -1,71 +0,0 @@
-<script setup lang="ts">
-import { useColumns } from "./columns";
-import { useRenderIcon } from "@/components/ReIcon/src/hooks";
-
-const { loading, columns, dataList, pagination, Empty, onCurrentChange } =
-  useColumns();
-</script>
-
-<template>
-  <pure-table
-    row-key="id"
-    alignWhole="center"
-    showOverflowTooltip
-    :loading="loading"
-    :loading-config="{ background: 'transparent' }"
-    :data="
-      dataList.slice(
-        (pagination.currentPage - 1) * pagination.pageSize,
-        pagination.currentPage * pagination.pageSize
-      )
-    "
-    :columns="columns"
-    :pagination="pagination"
-    @page-current-change="onCurrentChange"
-  >
-    <template #empty>
-      <el-empty description="暂无数据" :image-size="60">
-        <template #image>
-          <Empty />
-        </template>
-      </el-empty>
-    </template>
-    <template #operation="{ row }">
-      <el-button
-        plain
-        circle
-        size="small"
-        :title="`查看序号为${row.id}的详情`"
-        :icon="useRenderIcon('ri:search-line')"
-      />
-    </template>
-  </pure-table>
-</template>
-
-<style lang="scss">
-.pure-table-filter {
-  .el-table-filter__list {
-    min-width: 80px;
-    padding: 0;
-
-    li {
-      line-height: 28px;
-    }
-  }
-}
-</style>
-
-<style lang="scss" scoped>
-:deep(.el-table) {
-  --el-table-border: none;
-  --el-table-border-color: transparent;
-
-  .el-empty__description {
-    margin: 0;
-  }
-
-  .el-scrollbar__bar {
-    display: none;
-  }
-}
-</style>

+ 0 - 134
haha-admin-web/src/views/welcome/data.ts

@@ -1,134 +0,0 @@
-import { dayjs, cloneDeep, getRandomIntBetween } from "./utils";
-import GroupLine from "~icons/ri/group-line";
-import Question from "~icons/ri/question-answer-line";
-import CheckLine from "~icons/ri/chat-check-line";
-import Smile from "~icons/ri/star-smile-line";
-
-const days = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
-
-/** 需求人数、提问数量、解决数量、用户满意度 */
-const chartData = [
-  {
-    icon: GroupLine,
-    bgColor: "#effaff",
-    color: "#41b6ff",
-    duration: 2200,
-    name: "需求人数",
-    value: 36000,
-    percent: "+88%",
-    data: [2101, 5288, 4239, 4962, 6752, 5208, 7450] // 平滑折线图数据
-  },
-  {
-    icon: Question,
-    bgColor: "#fff5f4",
-    color: "#e85f33",
-    duration: 1600,
-    name: "提问数量",
-    value: 16580,
-    percent: "+70%",
-    data: [2216, 1148, 1255, 788, 4821, 1973, 4379]
-  },
-  {
-    icon: CheckLine,
-    bgColor: "#eff8f4",
-    color: "#26ce83",
-    duration: 1500,
-    name: "解决数量",
-    value: 16499,
-    percent: "+99%",
-    data: [861, 1002, 3195, 1715, 3666, 2415, 3645]
-  },
-  {
-    icon: Smile,
-    bgColor: "#f6f4fe",
-    color: "#7846e5",
-    duration: 100,
-    name: "用户满意度",
-    value: 100,
-    percent: "+100%",
-    data: [100]
-  }
-];
-
-/** 分析概览 */
-const barChartData = [
-  {
-    requireData: [2101, 5288, 4239, 4962, 6752, 5208, 7450],
-    questionData: [2216, 1148, 1255, 1788, 4821, 1973, 4379]
-  },
-  {
-    requireData: [2101, 3280, 4400, 4962, 5752, 6889, 7600],
-    questionData: [2116, 3148, 3255, 3788, 4821, 4970, 5390]
-  }
-];
-
-/** 解决概率 */
-const progressData = [
-  {
-    week: "周一",
-    percentage: 85,
-    duration: 110,
-    color: "#41b6ff"
-  },
-  {
-    week: "周二",
-    percentage: 86,
-    duration: 105,
-    color: "#41b6ff"
-  },
-  {
-    week: "周三",
-    percentage: 88,
-    duration: 100,
-    color: "#41b6ff"
-  },
-  {
-    week: "周四",
-    percentage: 89,
-    duration: 95,
-    color: "#41b6ff"
-  },
-  {
-    week: "周五",
-    percentage: 94,
-    duration: 90,
-    color: "#26ce83"
-  },
-  {
-    week: "周六",
-    percentage: 96,
-    duration: 85,
-    color: "#26ce83"
-  },
-  {
-    week: "周日",
-    percentage: 100,
-    duration: 80,
-    color: "#26ce83"
-  }
-].reverse();
-
-/** 数据统计 */
-const tableData = Array.from({ length: 30 }).map((_, index) => {
-  return {
-    id: index + 1,
-    requiredNumber: getRandomIntBetween(13500, 19999),
-    questionNumber: getRandomIntBetween(12600, 16999),
-    resolveNumber: getRandomIntBetween(13500, 17999),
-    satisfaction: getRandomIntBetween(95, 100),
-    date: dayjs().subtract(index, "day").format("YYYY-MM-DD")
-  };
-});
-
-/** 最新动态 */
-const latestNewsData = cloneDeep(tableData)
-  .slice(0, 14)
-  .map((item, index) => {
-    return Object.assign(item, {
-      date: `${dayjs().subtract(index, "day").format("YYYY-MM-DD")} ${
-        days[dayjs().subtract(index, "day").day()]
-      }`
-    });
-  });
-
-export { chartData, barChartData, progressData, tableData, latestNewsData };

+ 0 - 277
haha-admin-web/src/views/welcome/index.vue

@@ -1,277 +0,0 @@
-<script setup lang="ts">
-import { ref, markRaw } from "vue";
-import ReCol from "@/components/ReCol";
-import { useDark, randomGradient } from "./utils";
-import WelcomeTable from "./components/table/index.vue";
-import { ReNormalCountTo } from "@/components/ReCountTo";
-import { useRenderFlicker } from "@/components/ReFlicker";
-import { ChartBar, ChartLine, ChartRound } from "./components/charts";
-import Segmented, { type OptionsType } from "@/components/ReSegmented";
-import { chartData, barChartData, progressData, latestNewsData } from "./data";
-
-defineOptions({
-  name: "Welcome"
-});
-
-const { isDark } = useDark();
-
-let curWeek = ref(1); // 0上周、1本周
-const optionsBasis: Array<OptionsType> = [
-  {
-    label: "上周"
-  },
-  {
-    label: "本周"
-  }
-];
-</script>
-
-<template>
-  <div>
-    <el-row :gutter="24" justify="space-around">
-      <re-col
-        v-for="(item, index) in chartData"
-        :key="index"
-        v-motion
-        class="mb-[18px]"
-        :value="6"
-        :md="12"
-        :sm="12"
-        :xs="24"
-        :initial="{
-          opacity: 0,
-          y: 100
-        }"
-        :enter="{
-          opacity: 1,
-          y: 0,
-          transition: {
-            delay: 80 * (index + 1)
-          }
-        }"
-      >
-        <el-card class="line-card" shadow="never">
-          <div class="flex justify-between">
-            <span class="text-md font-medium">
-              {{ item.name }}
-            </span>
-            <div
-              class="w-8 h-8 flex justify-center items-center rounded-md"
-              :style="{
-                backgroundColor: isDark ? 'transparent' : item.bgColor
-              }"
-            >
-              <IconifyIconOffline
-                :icon="item.icon"
-                :color="item.color"
-                width="18"
-                height="18"
-              />
-            </div>
-          </div>
-          <div class="flex justify-between items-start mt-3">
-            <div class="w-1/2">
-              <ReNormalCountTo
-                :duration="item.duration"
-                :fontSize="'1.6em'"
-                :startVal="100"
-                :endVal="item.value"
-              />
-              <p class="font-medium text-green-500">{{ item.percent }}</p>
-            </div>
-            <ChartLine
-              v-if="item.data.length > 1"
-              class="w-1/2!"
-              :color="item.color"
-              :data="item.data"
-            />
-            <ChartRound v-else class="w-1/2!" />
-          </div>
-        </el-card>
-      </re-col>
-
-      <re-col
-        v-motion
-        class="mb-[18px]"
-        :value="18"
-        :xs="24"
-        :initial="{
-          opacity: 0,
-          y: 100
-        }"
-        :enter="{
-          opacity: 1,
-          y: 0,
-          transition: {
-            delay: 400
-          }
-        }"
-      >
-        <el-card class="bar-card" shadow="never">
-          <div class="flex justify-between">
-            <span class="text-md font-medium">分析概览</span>
-            <Segmented v-model="curWeek" :options="optionsBasis" />
-          </div>
-          <div class="flex justify-between items-start mt-3">
-            <ChartBar
-              :requireData="barChartData[curWeek].requireData"
-              :questionData="barChartData[curWeek].questionData"
-            />
-          </div>
-        </el-card>
-      </re-col>
-
-      <re-col
-        v-motion
-        class="mb-[18px]"
-        :value="6"
-        :xs="24"
-        :initial="{
-          opacity: 0,
-          y: 100
-        }"
-        :enter="{
-          opacity: 1,
-          y: 0,
-          transition: {
-            delay: 480
-          }
-        }"
-      >
-        <el-card shadow="never">
-          <div class="flex justify-between">
-            <span class="text-md font-medium">解决概率</span>
-          </div>
-          <div
-            v-for="(item, index) in progressData"
-            :key="index"
-            :class="[
-              'flex',
-              'justify-between',
-              'items-start',
-              index === 0 ? 'mt-8' : 'mt-[2.15rem]'
-            ]"
-          >
-            <el-progress
-              :text-inside="true"
-              :percentage="item.percentage"
-              :stroke-width="21"
-              :color="item.color"
-              striped
-              striped-flow
-              :duration="item.duration"
-            />
-            <span class="text-nowrap ml-2 text-text_color_regular text-sm">
-              {{ item.week }}
-            </span>
-          </div>
-        </el-card>
-      </re-col>
-
-      <re-col
-        v-motion
-        class="mb-[18px]"
-        :value="18"
-        :xs="24"
-        :initial="{
-          opacity: 0,
-          y: 100
-        }"
-        :enter="{
-          opacity: 1,
-          y: 0,
-          transition: {
-            delay: 560
-          }
-        }"
-      >
-        <el-card shadow="never" class="h-[580px]">
-          <div class="flex justify-between">
-            <span class="text-md font-medium">数据统计</span>
-          </div>
-          <WelcomeTable class="mt-3" />
-        </el-card>
-      </re-col>
-
-      <re-col
-        v-motion
-        class="mb-[18px]"
-        :value="6"
-        :xs="24"
-        :initial="{
-          opacity: 0,
-          y: 100
-        }"
-        :enter="{
-          opacity: 1,
-          y: 0,
-          transition: {
-            delay: 640
-          }
-        }"
-      >
-        <el-card shadow="never">
-          <div class="flex justify-between">
-            <span class="text-md font-medium">最新动态</span>
-          </div>
-          <el-scrollbar max-height="504" class="mt-3">
-            <el-timeline>
-              <el-timeline-item
-                v-for="(item, index) in latestNewsData"
-                :key="index"
-                center
-                placement="top"
-                :icon="
-                  markRaw(
-                    useRenderFlicker({
-                      background: randomGradient({
-                        randomizeHue: true
-                      })
-                    })
-                  )
-                "
-                :timestamp="item.date"
-              >
-                <p class="text-text_color_regular text-sm">
-                  {{
-                    `新增 ${item.requiredNumber} 条问题,${item.resolveNumber} 条已解决`
-                  }}
-                </p>
-              </el-timeline-item>
-            </el-timeline>
-          </el-scrollbar>
-        </el-card>
-      </re-col>
-    </el-row>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-:deep(.el-card) {
-  --el-card-border-color: none;
-
-  /* 解决概率进度条宽度 */
-  .el-progress--line {
-    width: 85%;
-  }
-
-  /* 解决概率进度条字体大小 */
-  .el-progress-bar__innerText {
-    font-size: 15px;
-  }
-
-  /* 隐藏 el-scrollbar 滚动条 */
-  .el-scrollbar__bar {
-    display: none;
-  }
-
-  /* el-timeline 每一项上下、左右边距 */
-  .el-timeline-item {
-    margin: 0 6px;
-  }
-}
-
-.main-content {
-  margin: 20px 20px 0 !important;
-}
-</style>

+ 0 - 6
haha-admin-web/src/views/welcome/utils.ts

@@ -1,6 +0,0 @@
-export { default as dayjs } from "dayjs";
-export { useDark, cloneDeep, randomGradient } from "@pureadmin/utils";
-
-export function getRandomIntBetween(min: number, max: number) {
-  return Math.floor(Math.random() * (max - min + 1)) + min;
-}

+ 6 - 1
haha-admin-web/vite.config.ts

@@ -50,7 +50,12 @@ export default async ({ mode }: ConfigEnv): Promise<UserConfigExport> => {
     proxyConfig[path] = {
       target: API_BASE_URL,
       changeOrigin: true,
-      rewrite: (p: string) => p.replace(new RegExp(`^${path}`), path)
+      rewrite: (p: string) => p.replace(new RegExp(`^${path}`), path),
+      bypass: (req: any) => {
+        if (req.headers?.accept?.includes('text/html')) {
+          return '/index.html';
+        }
+      }
     };
   });
   

+ 37 - 0
haha-admin/src/main/java/com/haha/admin/controller/ProductController.java

@@ -2,6 +2,8 @@ package com.haha.admin.controller;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.haha.admin.annotation.RequirePermission;
+import com.haha.entity.dto.AddToMerchantDTO;
+import com.haha.entity.dto.MasterProductQueryDTO;
 import com.haha.entity.dto.ProductQueryDTO;
 import com.haha.common.annotation.Log;
 import com.haha.common.enums.OperationType;
@@ -134,4 +136,39 @@ public class ProductController {
         boolean success = productService.syncToHaha(id);
         return success ? Result.success("同步成功", null) : Result.error(500, "同步失败");
     }
+
+    /**
+     * 查询商品总库列表(调用哈哈平台API)
+     * @param queryDTO 查询参数
+     * @return 商品总库分页数据
+     */
+    @RequirePermission("product:read")
+    @GetMapping("/master/list")
+    public Result<Map<String, Object>> masterList(MasterProductQueryDTO queryDTO) {
+        queryDTO.validate();
+
+        Map<String, Object> result = productService.getMasterProductList(
+                queryDTO.getPage(),
+                queryDTO.getPageSize(),
+                queryDTO.getBarcode(),
+                queryDTO.getCode(),
+                queryDTO.getProductName(),
+                queryDTO.getDeviceAdapt()
+        );
+
+        return Result.success("查询成功", result);
+    }
+
+    /**
+     * 添加商品到商家商品库(调用哈哈平台API)
+     * @param dto 请求参数
+     * @return 添加结果
+     */
+    @RequirePermission("product:create")
+    @Log(module = "商品管理", operation = OperationType.INSERT, summary = "添加商品到商家商品库")
+    @PostMapping("/master/add-to-merchant")
+    public Result<Void> addToMerchant(@RequestBody AddToMerchantDTO dto) {
+        boolean success = productService.addToMerchant(dto.getCodeList());
+        return success ? Result.success("添加成功", null) : Result.error(500, "添加失败");
+    }
 }

+ 10 - 0
haha-admin/src/main/resources/sql/menu_add_master_library.sql

@@ -0,0 +1,10 @@
+-- =====================================================
+-- 增量迁移:商品管理下新增"商品总库"菜单
+-- 执行前确认 403 未被占用
+-- =====================================================
+INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, show_link, show_parent)
+VALUES (403, 400, 0, '商品总库', 'MasterLibrary', '/product/master-library', '', 3, 1, 0);
+
+-- admin 角色(role_id=1)关联新菜单
+INSERT INTO t_role_menu (id, role_id, menu_id)
+VALUES (90053, 1, 403);

+ 27 - 0
haha-admin/src/main/resources/sql/menu_fix_ranks_and_icons.sql

@@ -0,0 +1,27 @@
+-- =====================================================
+-- 修复菜单排序冲突 + 图标重复
+-- 执行日期:2026-06-25
+-- =====================================================
+
+-- 1. 解决一级菜单 rank 冲突
+UPDATE t_menu SET `rank` = 7  WHERE id = 700 AND deleted = 0;  -- 分销管理: 6→7
+UPDATE t_menu SET `rank` = 8  WHERE id = 800 AND deleted = 0;  -- 营销中心: 7→8
+UPDATE t_menu SET `rank` = 9  WHERE id = 900 AND deleted = 0;  -- 统计报表: 8→9
+UPDATE t_menu SET `rank` = 10 WHERE id = 1000 AND deleted = 0; -- 系统设置: 8→10
+UPDATE t_menu SET `rank` = 11 WHERE id = 1100 AND deleted = 0; -- 设备层模版管理: 8→11
+
+-- 2. 修复设备层模版图标与库存管理重复 (ri:stack-line → ri:layout-grid-line)
+UPDATE t_menu SET icon = 'ri:layout-grid-line' WHERE id = 1100 AND deleted = 0;
+
+-- 最终一级菜单排序:
+--  1: 数据概览
+--  2: 订单管理
+--  3: 运营管理
+--  4: 商品管理
+--  5: 库存管理
+--  6: 客户管理
+--  7: 分销管理
+--  8: 营销中心
+--  9: 统计报表
+-- 10: 系统设置
+-- 11: 设备层模版管理

+ 16 - 0
haha-admin/src/main/resources/sql/menu_flatten_single_child.sql

@@ -0,0 +1,16 @@
+-- =====================================================
+-- 将只有一个子菜单的父菜单扁平化为一级菜单
+-- (去掉下拉箭头,直接显示为可点击的菜单项)
+-- =====================================================
+UPDATE t_menu
+SET show_parent = 0
+WHERE id IN (
+    SELECT parent_id FROM (
+        SELECT parent_id, COUNT(*) AS cnt
+        FROM t_menu
+        WHERE deleted = 0 AND parent_id > 0
+        GROUP BY parent_id
+    ) t
+    WHERE t.cnt = 1
+)
+AND deleted = 0;

+ 15 - 0
haha-entity/src/main/java/com/haha/entity/dto/AddToMerchantDTO.java

@@ -0,0 +1,15 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+
+/**
+ * 添加到商家商品库请求DTO
+ */
+@Data
+public class AddToMerchantDTO {
+
+    /**
+     * 商品编码列表,多个用逗号隔开
+     */
+    private String codeList;
+}

+ 32 - 0
haha-entity/src/main/java/com/haha/entity/dto/MasterProductQueryDTO.java

@@ -0,0 +1,32 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 商品总库查询DTO
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MasterProductQueryDTO extends PageQueryDTO {
+
+    /**
+     * 条形码
+     */
+    private String barcode;
+
+    /**
+     * 商品编码
+     */
+    private String code;
+
+    /**
+     * 商品名称
+     */
+    private String productName;
+
+    /**
+     * 适用设备: STATIC-静态柜, DYNAMIC-动态柜
+     */
+    private String deviceAdapt;
+}

+ 22 - 0
haha-service/src/main/java/com/haha/service/ProductService.java

@@ -100,4 +100,26 @@ public interface ProductService extends IService<Product> {
      * 更新同步状态
      */
     boolean updateSyncStatus(Long productId, Integer syncStatus);
+
+    /**
+     * 查询商品总库列表(调用哈哈平台API)
+     *
+     * @param page        页码
+     * @param pageSize    每页条数
+     * @param barcode     条形码(可选)
+     * @param code        商品编码(可选)
+     * @param productName 商品名称(可选)
+     * @param deviceAdapt 适用设备(可选)
+     * @return 商品总库分页数据
+     */
+    Map<String, Object> getMasterProductList(int page, int pageSize, String barcode,
+                                              String code, String productName, String deviceAdapt);
+
+    /**
+     * 添加商品到商家商品库(调用哈哈平台API)
+     *
+     * @param codeList 商品编码列表,多个用逗号隔开
+     * @return 是否成功
+     */
+    boolean addToMerchant(String codeList);
 }

+ 46 - 33
haha-service/src/main/java/com/haha/service/impl/DistributorReportServiceImpl.java

@@ -158,46 +158,59 @@ public class DistributorReportServiceImpl extends ServiceImpl<DistributorMonthly
 
     @Override
     public DistributorDashboardVO getDashboard() {
-        Long totalDistributors = distributorMapper.selectCount(
-                new LambdaQueryWrapper<Distributor>().eq(Distributor::getDeleted, 0));
+        try {
+            Long totalDistributors = distributorMapper.selectCount(
+                    new LambdaQueryWrapper<Distributor>().eq(Distributor::getDeleted, 0));
 
-        Long activeDistributors = distributorMapper.selectCount(
-                new LambdaQueryWrapper<Distributor>().eq(Distributor::getStatus, 1).eq(Distributor::getDeleted, 0));
+            Long activeDistributors = distributorMapper.selectCount(
+                    new LambdaQueryWrapper<Distributor>().eq(Distributor::getStatus, 1).eq(Distributor::getDeleted, 0));
 
-        List<Distributor> allDistributors = distributorMapper.selectList(
-                new LambdaQueryWrapper<Distributor>().eq(Distributor::getDeleted, 0));
+            List<Distributor> allDistributors = distributorMapper.selectList(
+                    new LambdaQueryWrapper<Distributor>().eq(Distributor::getDeleted, 0));
 
-        Long totalReferrals = allDistributors.stream()
-                .mapToLong(d -> d.getTotalReferrals() != null ? d.getTotalReferrals() : 0)
-                .sum();
+            Long totalReferrals = allDistributors.stream()
+                    .mapToLong(d -> d.getTotalReferrals() != null ? d.getTotalReferrals() : 0)
+                    .sum();
 
-        Long validReferrals = allDistributors.stream()
-                .mapToLong(d -> d.getValidReferrals() != null ? d.getValidReferrals() : 0)
-                .sum();
+            Long validReferrals = allDistributors.stream()
+                    .mapToLong(d -> d.getValidReferrals() != null ? d.getValidReferrals() : 0)
+                    .sum();
 
-        BigDecimal totalCommission = allDistributors.stream()
-                .map(d -> d.getTotalCommission() != null ? d.getTotalCommission() : BigDecimal.ZERO)
-                .reduce(BigDecimal.ZERO, BigDecimal::add);
+            BigDecimal totalCommission = allDistributors.stream()
+                    .map(d -> d.getTotalCommission() != null ? d.getTotalCommission() : BigDecimal.ZERO)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
 
-        String currentMonth = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
-        LambdaQueryWrapper<DistributorCommission> monthCommissionWrapper = new LambdaQueryWrapper<>();
-        monthCommissionWrapper.apply("DATE_FORMAT(create_time, '%Y-%m') = {0}", currentMonth);
-        List<DistributorCommission> monthCommissions = commissionMapper.selectList(monthCommissionWrapper);
-        BigDecimal monthlyCommission = monthCommissions.stream()
-                .map(c -> c.getAmount() != null ? c.getAmount() : BigDecimal.ZERO)
-                .reduce(BigDecimal.ZERO, BigDecimal::add);
+            String currentMonth = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
+            LambdaQueryWrapper<DistributorCommission> monthCommissionWrapper = new LambdaQueryWrapper<>();
+            monthCommissionWrapper.apply("DATE_FORMAT(create_time, '%Y-%m') = {0}", currentMonth);
+            List<DistributorCommission> monthCommissions = commissionMapper.selectList(monthCommissionWrapper);
+            BigDecimal monthlyCommission = monthCommissions.stream()
+                    .map(c -> c.getAmount() != null ? c.getAmount() : BigDecimal.ZERO)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
 
-        List<MonthlyTrendVO> trends = buildMonthlyTrends();
-
-        return DistributorDashboardVO.builder()
-                .totalDistributors(totalDistributors)
-                .activeDistributors(activeDistributors)
-                .totalReferrals(totalReferrals)
-                .validReferrals(validReferrals)
-                .totalCommission(totalCommission)
-                .monthlyCommission(monthlyCommission)
-                .trends(trends)
-                .build();
+            List<MonthlyTrendVO> trends = buildMonthlyTrends();
+
+            return DistributorDashboardVO.builder()
+                    .totalDistributors(totalDistributors)
+                    .activeDistributors(activeDistributors)
+                    .totalReferrals(totalReferrals)
+                    .validReferrals(validReferrals)
+                    .totalCommission(totalCommission)
+                    .monthlyCommission(monthlyCommission)
+                    .trends(trends)
+                    .build();
+        } catch (Exception e) {
+            log.error("获取分销仪表盘数据失败", e);
+            return DistributorDashboardVO.builder()
+                    .totalDistributors(0L)
+                    .activeDistributors(0L)
+                    .totalReferrals(0L)
+                    .validReferrals(0L)
+                    .totalCommission(BigDecimal.ZERO)
+                    .monthlyCommission(BigDecimal.ZERO)
+                    .trends(new ArrayList<>())
+                    .build();
+        }
     }
 
     @Override

+ 20 - 5
haha-service/src/main/java/com/haha/service/impl/MenuServiceImpl.java

@@ -154,18 +154,33 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
             // 递归构建子节点
             List<Map<String, Object>> childNodes = buildMenuTree(menu.getId(), allMenus);
             if (!childNodes.isEmpty()) {
-                node.put("children", childNodes);
-                if (menu.getRedirect() != null && !menu.getRedirect().isEmpty()) {
-                    node.put("redirect", menu.getRedirect());
+                // showParent为false且只有一个子节点时,提升子节点为一级菜单
+                boolean showParent = menu.getShowParent() == null || menu.getShowParent();
+                if (!showParent && childNodes.size() == 1) {
+                    Map<String, Object> childNode = childNodes.get(0);
+                    Map<String, Object> childMeta = (Map<String, Object>) childNode.get("meta");
+                    // 合并父级 icon(子级优先)
+                    if ((childMeta.get("icon") == null || childMeta.get("icon").toString().isEmpty())
+                            && menu.getIcon() != null && !menu.getIcon().isEmpty()) {
+                        childMeta.put("icon", menu.getIcon());
+                    }
+                    // 使用父级 title 覆盖子级(更概括)
+                    childMeta.put("title", menu.getTitle());
+                    tree.add(childNode);
+                } else {
+                    node.put("children", childNodes);
+                    if (menu.getRedirect() != null && !menu.getRedirect().isEmpty()) {
+                        node.put("redirect", menu.getRedirect());
+                    }
+                    tree.add(node);
                 }
             } else {
                 // 叶子节点需要 component
                 if (menu.getComponent() != null && !menu.getComponent().isEmpty()) {
                     node.put("component", menu.getComponent());
                 }
+                tree.add(node);
             }
-
-            tree.add(node);
         }
 
         return tree;

+ 1 - 1
haha-service/src/main/java/com/haha/service/impl/NewProductApplyServiceImpl.java

@@ -222,7 +222,7 @@ public class NewProductApplyServiceImpl extends ServiceImpl<NewProductApplyMappe
             Map<String, Object> result = hahaClient.getGoodsApi().getTotalList(1, 1, barcode, null, null, null);
             
             if (result != null) {
-                List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("list");
+                List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("data");
                 if (list != null && !list.isEmpty()) {
                     return list.get(0);
                 }

+ 40 - 0
haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

@@ -10,6 +10,8 @@ import com.haha.common.exception.BusinessException;
 import com.haha.common.utils.EntityLabelUtils;
 import com.haha.entity.Product;
 import com.haha.mapper.ProductMapper;
+import com.haha.sdk.HahaClient;
+import com.haha.sdk.exception.HahaException;
 import com.haha.service.ProductService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -25,6 +27,8 @@ import java.util.Map;
 @RequiredArgsConstructor
 public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
 
+    private final HahaClient hahaClient;
+
     @Override
     public IPage<Product> getPage(int page, int pageSize, String name, String barcode, 
                                    String category, Integer syncStatus) {
@@ -165,6 +169,42 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
                 .update();
     }
 
+    @Override
+    public Map<String, Object> getMasterProductList(int page, int pageSize, String barcode,
+                                                     String code, String productName, String deviceAdapt) {
+        try {
+            Map<String, Object> result = hahaClient.getGoodsApi()
+                    .getTotalList(page, pageSize, barcode, code, productName, deviceAdapt);
+
+            Map<String, Object> response = new HashMap<>();
+            if (result != null) {
+                response.put("list", result.getOrDefault("data", List.of()));
+                response.put("total", result.getOrDefault("total", 0L));
+                response.put("pageSize", pageSize);
+                response.put("currentPage", page);
+            } else {
+                response.put("list", List.of());
+                response.put("total", 0L);
+                response.put("pageSize", pageSize);
+                response.put("currentPage", page);
+            }
+            return response;
+        } catch (HahaException e) {
+            log.error("查询商品总库失败: {}", e.getMessage());
+            throw new BusinessException(500, "查询商品总库失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean addToMerchant(String codeList) {
+        try {
+            return hahaClient.getGoodsApi().addToMerchant(codeList);
+        } catch (HahaException e) {
+            log.error("添加商品到商家商品库失败: {}", e.getMessage());
+            throw new BusinessException(500, "添加到商家商品库失败: " + e.getMessage());
+        }
+    }
+
     /**
      * 填充商品标签字段
      */