Sfoglia il codice sorgente

Merge branch 'master' of https://gogs.storlead.com/storlead/storlead-sasa-platform

183207892172 6 giorni fa
parent
commit
a8ef7ae6ae

+ 13 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/SopServiceImpl.java

@@ -21,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -157,6 +158,18 @@ public class SopServiceImpl extends ServiceImpl<SopMapper, SopEntity> implements
         }).collect(Collectors.toList());
         responsePage.setRecords(records);
 
+        List<Long> sopIds = records.stream().map(SopResponseDTO::getId).collect(Collectors.toList());
+        List<SopDetailEntity> details = sopDetailService.list(new LambdaQueryWrapper<SopDetailEntity>()
+                .in(SopDetailEntity::getSopId, sopIds)
+                .eq(SopDetailEntity::getIsDelete, 0));
+        List<SopDetailResponseDTO> detailResponses = details.stream().map(detail -> {
+            SopDetailResponseDTO detailDto = new SopDetailResponseDTO();
+            BeanUtils.copyProperties(detail, detailDto);
+            return detailDto;
+        }).collect(Collectors.toList());
+        Map<Long, List<SopDetailResponseDTO>> detailMap = detailResponses.stream().collect(Collectors.groupingBy(SopDetailResponseDTO::getSopId));
+        records.forEach(sopDetailDto -> sopDetailDto.setDetails(detailMap.get(sopDetailDto.getId())));
+
         return responsePage;
     }
 }

+ 99 - 19
java/storlead-system/storlead-system-api/src/main/java/com/storlead/system/controller/PermissionResApiController.java

@@ -1,14 +1,18 @@
 package com.storlead.system.controller;
 
 import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.storlead.framework.common.result.Result;
 import com.storlead.framework.common.util.PingyinUtil;
 import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.system.pojo.entity.MenuEntity;
+import com.storlead.system.pojo.entity.SysAppEntity;
+import com.storlead.system.pojo.vo.AppRouterVO;
 import com.storlead.system.pojo.vo.MetaVo;
 import com.storlead.system.pojo.vo.RouterVo;
 import com.storlead.system.pojo.vo.UserMenuVO;
 import com.storlead.system.service.IMenuService;
+import com.storlead.system.service.SysAppService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.log4j.Log4j2;
@@ -21,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * @program: sp-sales-platform
@@ -37,6 +42,10 @@ public class PermissionResApiController {
 
     @Resource
     private IMenuService menuService;
+
+    @Resource
+    private SysAppService sysAppService;
+
     private static final String PARENT_VIEW = "ParentView";
     private static final String LAYOUT = "Layout";
 
@@ -47,25 +56,14 @@ public class PermissionResApiController {
             Map<String,Object> map = new HashMap();
             Long currentUserId = LoginUserUtil.getCurrentUserId();
             List<MenuEntity> userMenuList = menuService.getUserMenuList(currentUserId,Arrays.asList(0),Arrays.asList(1,2));
-            List<UserMenuVO> menuls = new ArrayList();
-
-            userMenuList.forEach(menu -> {
-                UserMenuVO menuVo = new UserMenuVO();
-                BeanUtils.copyProperties(menu,menuVo);
-                menuls.add(menuVo);
-            });
-            if (!CollectionUtils.isEmpty(menuls)) {
-                menuls.forEach(e -> {
-                    if (StrUtil.isNotBlank(e.getMenuName())) {
-                        String pyIcon = PingyinUtil.getLowFullSpell(e.getMenuName());
-                        e.setIcon(pyIcon);
-                    }
-                });
-            }
-            List<UserMenuVO>  menuRes = createTreeMenus(Long.valueOf(0),menuls);
-            List<RouterVo> treeRouter = new ArrayList<>();
-            menuToRouter(menuRes, null, treeRouter);
-            map.put("routerVo",treeRouter);
+            List<UserMenuVO> menuls = toUserMenuVoList(userMenuList);
+            List<AppRouterVO> appRouterList = buildAppRouterList(menuls);
+            List<RouterVo> treeRouter = appRouterList.stream()
+                    .filter(app -> !CollectionUtils.isEmpty(app.getChildren()))
+                    .flatMap(app -> app.getChildren().stream())
+                    .collect(Collectors.toList());
+            map.put("appRouterVo", appRouterList);
+//            map.put("routerVo", treeRouter);
             return Result.result(map);
         }catch (Exception e) {
             e.printStackTrace();
@@ -106,6 +104,88 @@ public class PermissionResApiController {
             return Result.error("获取菜单信息错误:"+e.getMessage());
         }
     }
+    /**
+     * 按应用分组构建菜单路由:应用 -> 菜单树。
+     */
+    private List<AppRouterVO> buildAppRouterList(List<UserMenuVO> menuls) {
+        if (CollectionUtils.isEmpty(menuls)) {
+            return Collections.emptyList();
+        }
+        Map<Long, List<UserMenuVO>> menusByApp = menuls.stream()
+                .collect(Collectors.groupingBy(menu -> menu.getAppId() == null ? 0L : menu.getAppId()));
+
+        Set<Long> appIds = menusByApp.keySet();
+        List<SysAppEntity> apps = sysAppService.list(new LambdaQueryWrapper<SysAppEntity>()
+                .in(SysAppEntity::getId, appIds)
+                .eq(SysAppEntity::getIsDelete, 0)
+                .eq(SysAppEntity::getEnabled, 1)
+                .orderByAsc(SysAppEntity::getSort)
+                .orderByAsc(SysAppEntity::getId));
+
+        List<AppRouterVO> appRouterList = new ArrayList<>();
+        Set<Long> handledAppIds = new HashSet<>();
+
+        for (SysAppEntity app : apps) {
+            List<UserMenuVO> appMenus = menusByApp.get(app.getId());
+            if (CollectionUtils.isEmpty(appMenus)) {
+                continue;
+            }
+            handledAppIds.add(app.getId());
+            appRouterList.add(buildOneAppRouter(app, appMenus));
+        }
+
+        for (Long appId : appIds) {
+            if (handledAppIds.contains(appId)) {
+                continue;
+            }
+            List<UserMenuVO> appMenus = menusByApp.get(appId);
+            if (CollectionUtils.isEmpty(appMenus)) {
+                continue;
+            }
+            AppRouterVO appRouter = new AppRouterVO();
+            appRouter.setAppId(appId);
+            appRouter.setAppName("应用" + appId);
+            appRouter.setChildren(buildMenuRouters(appMenus));
+            appRouterList.add(appRouter);
+        }
+        return appRouterList;
+    }
+
+    private AppRouterVO buildOneAppRouter(SysAppEntity app, List<UserMenuVO> appMenus) {
+        AppRouterVO appRouter = new AppRouterVO();
+        appRouter.setAppId(app.getId());
+        appRouter.setAppCode(app.getAppCode());
+        appRouter.setAppName(app.getAppName());
+        appRouter.setAppTitle(app.getAppTitle());
+        appRouter.setIcon(app.getIcon());
+        appRouter.setHomePath(app.getHomePath());
+        appRouter.setChildren(buildMenuRouters(appMenus));
+        return appRouter;
+    }
+
+    private List<RouterVo> buildMenuRouters(List<UserMenuVO> appMenus) {
+        List<UserMenuVO> menuTree = createTreeMenus(0L, appMenus);
+        List<RouterVo> routers = new ArrayList<>();
+        menuToRouter(menuTree, null, routers);
+        return routers;
+    }
+
+    private List<UserMenuVO> toUserMenuVoList(List<MenuEntity> userMenuList) {
+        if (CollectionUtils.isEmpty(userMenuList)) {
+            return Collections.emptyList();
+        }
+        List<UserMenuVO> menuls = new ArrayList<>();
+        userMenuList.forEach(menu -> {
+            UserMenuVO menuVo = new UserMenuVO();
+            BeanUtils.copyProperties(menu, menuVo);
+            if (StrUtil.isNotBlank(menuVo.getMenuName())) {
+                menuVo.setIcon(PingyinUtil.getLowFullSpell(menuVo.getMenuName()));
+            }
+            menuls.add(menuVo);
+        });
+        return menuls;
+    }
+
     /**
      * 是否为parent_view组件
      */

+ 12 - 1
java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/service/impl/MenuServiceImpl.java

@@ -6,6 +6,7 @@ import com.storlead.system.mapper.MenuMapper;
 import com.storlead.system.service.IMenuService;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
@@ -23,7 +24,7 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, MenuEntity> impleme
 
     @Override
     public List<MenuEntity> getUserMenuList(Long userId,Long appId,List<Integer> modeTypes,List<Integer> menuTypes) {
-        return this.baseMapper.selectUserMenuList(userId, appId, modeTypes ,menuTypes);
+        return this.baseMapper.selectUserMenuList(userId, appId, toArrayList(modeTypes), toArrayList(menuTypes));
     }
 
     @Override
@@ -42,4 +43,14 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, MenuEntity> impleme
         return this.baseMapper.selectMenuCreateTree();
     }
 
+    /**
+     * MyBatis foreach 在 Java 17+ 下无法反射访问 Arrays$ArrayList,需转为 ArrayList。
+     */
+    private static <T> List<T> toArrayList(List<T> source) {
+        if (source == null || source.isEmpty()) {
+            return source;
+        }
+        return source instanceof ArrayList ? source : new ArrayList<>(source);
+    }
+
 }

+ 36 - 0
java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/pojo/vo/AppRouterVO.java

@@ -0,0 +1,36 @@
+package com.storlead.system.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 用户可访问的应用及其菜单路由。
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public class AppRouterVO {
+
+    @ApiModelProperty(value = "应用 ID,对应 sys_app.id")
+    private Long appId;
+
+    @ApiModelProperty(value = "应用编码")
+    private String appCode;
+
+    @ApiModelProperty(value = "应用名称")
+    private String appName;
+
+    @ApiModelProperty(value = "前台标题")
+    private String appTitle;
+
+    @ApiModelProperty(value = "应用图标")
+    private String icon;
+
+    @ApiModelProperty(value = "默认首页路由")
+    private String homePath;
+
+    @ApiModelProperty(value = "应用下的菜单路由树")
+    private List<RouterVo> children;
+}

+ 1 - 1
ui/smarttrade-platform/src/api/customer/index.js

@@ -1,3 +1,3 @@
 import { get, post, put, remove, upload } from '@/utils/request'
 
-export const getCustomerList = (params = {}) => post('/router/rest/customer/list', params)
+export const getCustomerList = (data = {}) => post('/router/rest/customer/list', data)

+ 7 - 0
ui/smarttrade-platform/src/api/knowledge/index.js

@@ -0,0 +1,7 @@
+import { get, post, put, remove, upload } from '@/utils/request'
+
+export const knowledgeDatasets = (params = {}) => get('/router/rest/knowledge/base/datasets', params)
+export const knowledgeDocuments = (params = {}) => {
+    const { dataset_id, ...query } = params
+    return get(`/router/rest/knowledge/document/${dataset_id}/documents`, params)
+}

+ 825 - 0
ui/smarttrade-platform/src/utils/region.ts

@@ -0,0 +1,825 @@
+
+const region =  [
+      {
+        value: "亚洲",
+        label: "亚洲",
+        children: [
+          {
+            value: "中国",
+            label: "中国",
+          },
+          {
+            value: "蒙古",
+            label: "蒙古",
+          },
+          {
+            value: "朝鲜",
+            label: "朝鲜",
+          },
+          {
+            value: "韩国",
+            label: "韩国",
+          },
+          {
+            value: "日本",
+            label: "日本",
+          },
+          {
+            value: "菲律宾",
+            label: "菲律宾",
+          },
+          {
+            value: "越南",
+            label: "越南",
+          },
+          {
+            value: "老挝",
+            label: "老挝",
+          },
+          {
+            value: "柬埔寨",
+            label: "柬埔寨",
+          },
+          {
+            value: "缅甸",
+            label: "缅甸",
+          },
+          {
+            value: "泰国",
+            label: "泰国",
+          },
+          {
+            value: "马来西亚",
+            label: "马来西亚",
+          },
+          {
+            value: "文莱",
+            label: "文莱",
+          },
+          {
+            value: "新加坡",
+            label: "新加坡",
+          },
+          {
+            value: "印度尼西亚",
+            label: "印度尼西亚",
+          },
+          {
+            value: "东帝汶",
+            label: "东帝汶",
+          },
+          {
+            value: "尼泊尔",
+            label: "尼泊尔",
+          },
+          {
+            value: "不丹",
+            label: "不丹",
+          },
+          {
+            value: "孟加拉国",
+            label: "孟加拉国",
+          },
+          {
+            value: "印度",
+            label: "印度",
+          },
+          {
+            value: "巴基斯坦",
+            label: "巴基斯坦",
+          },
+          {
+            value: "斯里兰卡",
+            label: "斯里兰卡",
+          },
+          {
+            value: "马尔代夫",
+            label: "马尔代夫",
+          },
+          {
+            value: "哈萨克斯坦",
+            label: "哈萨克斯坦",
+          },
+          {
+            value: "吉尔吉斯斯坦",
+            label: "吉尔吉斯斯坦",
+          },
+          {
+            value: "塔吉克斯坦",
+            label: "塔吉克斯坦",
+          },
+          {
+            value: "乌兹别克斯坦",
+            label: "乌兹别克斯坦",
+          },
+          {
+            value: "土库曼斯坦",
+            label: "土库曼斯坦",
+          },
+          {
+            value: "阿富汗",
+            label: "阿富汗",
+          },
+          {
+            value: "伊拉克",
+            label: "伊拉克",
+          },
+          {
+            value: "伊朗",
+            label: "伊朗",
+          },
+   
+          {
+            value: "叙利亚",
+            label: "叙利亚",
+          },
+          {
+            value: "约旦",
+            label: "约旦",
+          },
+          {
+            value: "黎巴嫩",
+            label: "黎巴嫩",
+          },
+          {
+            value: "以色列",
+            label: "以色列",
+          },
+          {
+            value: "巴勒斯坦",
+            label: "巴勒斯坦",
+          },
+          {
+            value: "沙特阿拉伯",
+            label: "沙特阿拉伯",
+          },
+          {
+            value: "巴林",
+            label: "巴林",
+          },
+          {
+            value: "卡塔尔",
+            label: "卡塔尔",
+          },
+   
+          {
+            value: "科威特",
+            label: "科威特",
+          },
+          {
+            value: "阿拉伯联合酋长国",
+            label: "阿拉伯联合酋长国",
+          },
+          {
+            value: "阿曼",
+            label: "阿曼",
+          },
+          {
+            value: "也门",
+            label: "也门",
+          },
+          {
+            value: "格鲁吉亚",
+            label: "格鲁吉亚",
+          },
+          {
+            value: "亚美尼亚",
+            label: "亚美尼亚",
+          },
+          {
+            value: "阿塞拜疆",
+            label: "阿塞拜疆",
+          },
+          {
+            value: "土耳其",
+            label: "土耳其",
+          },
+          {
+            value: "塞浦路斯",
+            label: "塞浦路斯",
+          },
+        ],
+      },
+      {
+        value: "欧洲",
+        label: "欧洲",
+        children: [
+          {
+            value: "芬兰",
+            label: "芬兰",
+          },
+          {
+            value: "瑞典",
+            label: "瑞典",
+          },
+          {
+            value: "挪威",
+            label: "挪威",
+          },
+          {
+            value: "冰岛",
+            label: "冰岛",
+          },
+          {
+            value: "爱沙尼亚",
+            label: "爱沙尼亚",
+          },
+          {
+            value: "拉脱维亚",
+            label: "拉脱维亚",
+          },
+          {
+            value: "立陶宛",
+            label: "立陶宛",
+          },
+          {
+            value: "摩尔多瓦",
+            label: "摩尔多瓦",
+          },
+          {
+            value: "白俄罗斯",
+            label: "白俄罗斯",
+          },
+          {
+            value: "俄罗斯",
+            label: "俄罗斯",
+          },
+          {
+            value: "乌克兰",
+            label: "乌克兰",
+          },
+          {
+            value: "波兰",
+            label: "波兰",
+          },
+          {
+            value: "捷克共和国",
+            label: "捷克共和国",
+          },
+          {
+            value: "斯洛伐克",
+            label: "斯洛伐克",
+          },
+          {
+            value: "匈牙利",
+            label: "匈牙利",
+          },
+          {
+            value: "德国",
+            label: "德国",
+          },
+          {
+            value: "奥地利",
+            label: "奥地利",
+          },
+          {
+            value: "瑞士",
+            label: "瑞士",
+          },
+          {
+            value: "列支敦士登",
+            label: "列支敦士登",
+          },
+          {
+            value: "英国",
+            label: "英国",
+          },
+          {
+            value: "爱尔兰",
+            label: "爱尔兰",
+          },
+          {
+            value: "荷兰",
+            label: "荷兰",
+          },
+          {
+            value: "比利时",
+            label: "比利时",
+          },
+          {
+            value: "卢森堡",
+            label: "卢森堡",
+          },
+          {
+            value: "法国",
+            label: "法国",
+          },
+          {
+            value: "摩纳哥",
+            label: "摩纳哥",
+          },
+          {
+            value: "罗马尼亚",
+            label: "罗马尼亚",
+          },
+          {
+            value: "保加利亚",
+            label: "保加利亚",
+          },
+          {
+            value: "塞尔维亚",
+            label: "塞尔维亚",
+          },
+          {
+            value: "马其顿",
+            label: "马其顿",
+          },
+          {
+            value: "斯洛文尼亚",
+            label: "斯洛文尼亚",
+          },
+          {
+            value: "克罗地亚",
+            label: "克罗地亚",
+          },
+          {
+            value: "黑山共和国",
+            label: "黑山共和国",
+          },
+          {
+            value: "波斯尼亚和黑塞哥维那",
+            label: "波斯尼亚和黑塞哥维那",
+          },
+          {
+            value: "阿尔巴尼亚",
+            label: "阿尔巴尼亚",
+          },
+          {
+            value: "希腊",
+            label: "希腊",
+          },
+          {
+            value: "意大利",
+            label: "意大利",
+          },
+          {
+            value: "马耳他",
+            label: "马耳他",
+          },
+          {
+            value: "丹麦",
+            label: "丹麦",
+          },
+          {
+            value: "梵蒂冈",
+            label: "梵蒂冈",
+          },
+          {
+            value: "圣马力诺",
+            label: "圣马力诺",
+          },
+          {
+            value: "西班牙",
+            label: "西班牙",
+          },
+          {
+            value: "葡萄牙",
+            label: "葡萄牙",
+          },
+          {
+            value: "安道尔共和国",
+            label: "安道尔共和国",
+          },
+        ],
+      },
+      {
+        value: "非洲",
+        label: "非洲",
+        children: [
+          {
+            value: "埃及",
+            label: "埃及",
+          },
+          {
+            value: "利比亚",
+            label: "利比亚",
+          },
+          {
+            value: "突尼斯",
+            label: "突尼斯",
+          },
+          {
+            value: "阿尔及利亚",
+            label: "阿尔及利亚",
+          },
+          {
+            value: "摩洛哥",
+            label: "摩洛哥",
+          },
+          {
+            value: "苏丹",
+            label: "苏丹",
+          },
+          {
+            value: "南苏丹",
+            label: "南苏丹",
+          },
+          {
+            value: "埃塞俄比亚",
+            label: "埃塞俄比亚",
+          },
+          {
+            value: "厄立特里亚",
+            label: "厄立特里亚",
+          },
+          {
+            value: "索马里",
+            label: "索马里",
+          },
+          {
+            value: "吉布提",
+            label: "吉布提",
+          },
+          {
+            value: "肯尼亚",
+            label: "肯尼亚",
+          },
+          {
+            value: "坦桑尼亚",
+            label: "坦桑尼亚",
+          },
+          {
+            value: "乌干达",
+            label: "乌干达",
+          },
+          {
+            value: "卢旺达",
+            label: "卢旺达",
+          },
+          {
+            value: "布隆迪",
+            label: "布隆迪",
+          },
+          {
+            value: "塞舌尔",
+            label: "塞舌尔",
+          },
+          {
+            value: "乍得共和国",
+            label: "乍得共和国",
+          },
+          {
+            value: "中非共和国",
+            label: "中非共和国",
+          },
+          {
+            value: "喀麦隆",
+            label: "喀麦隆",
+          },
+          {
+            value: "赤道几内亚",
+            label: "赤道几内亚",
+          },
+          {
+            value: "加蓬",
+            label: "加蓬",
+          },
+          {
+            value: "刚果共和国",
+            label: "刚果共和国",
+          },
+          {
+            value: "刚果民主共和国",
+            label: "刚果民主共和国",
+          },
+          {
+            value: "圣多美和普林西比",
+            label: "圣多美和普林西比",
+          },
+          {
+            value: "毛里塔尼亚",
+            label: "毛里塔尼亚",
+          },
+          {
+            value: "塞内加尔",
+            label: "塞内加尔",
+          },
+          {
+            value: "冈比亚",
+            label: "冈比亚",
+          },
+          {
+            value: "马里共和国",
+            label: "马里共和国",
+          },
+          {
+            value: "布基纳法索",
+            label: "布基纳法索",
+          },
+          {
+            value: "几内亚",
+            label: "几内亚",
+          },
+          {
+            value: "几内亚比绍",
+            label: "几内亚比绍",
+          },
+          {
+            value: "佛得角",
+            label: "佛得角",
+          },
+          {
+            value: "塞拉利昂",
+            label: "塞拉利昂",
+          },
+          {
+            value: "利比里亚",
+            label: "利比里亚",
+          },
+          {
+            value: "科特迪瓦",
+            label: "科特迪瓦",
+          },
+          {
+            value: "加纳",
+            label: "加纳",
+          },
+          {
+            value: "多哥",
+            label: "多哥",
+          },
+          {
+            value: "贝宁共和国",
+            label: "贝宁共和国",
+          },
+          {
+            value: "尼日尔",
+            label: "尼日尔",
+          },
+          {
+            value: "尼日利亚",
+            label: "尼日利亚",
+          },
+          {
+            value: "赞比亚",
+            label: "赞比亚",
+          },
+          {
+            value: "安哥拉",
+            label: "安哥拉",
+          },
+          {
+            value: "津巴布韦",
+            label: "津巴布韦",
+          },
+          {
+            value: "马拉维",
+            label: "马拉维",
+          },
+          {
+            value: "莫桑比克",
+            label: "莫桑比克",
+          },
+          {
+            value: "博茨瓦纳",
+            label: "博茨瓦纳",
+          },
+          {
+            value: "纳米比亚",
+            label: "纳米比亚",
+          },
+          {
+            value: "南非",
+            label: "南非",
+          },
+          {
+            value: "斯威士兰",
+            label: "斯威士兰",
+          },
+          {
+            value: "莱索托",
+            label: "莱索托",
+          },
+          {
+            value: "马达加斯加",
+            label: "马达加斯加",
+          },
+          {
+            value: "科摩罗联盟",
+            label: "科摩罗联盟",
+          },
+          {
+            value: "毛里求斯",
+            label: "毛里求斯",
+          },
+        ],
+      },
+      {
+        value: "北美洲",
+        label: "北美洲",
+        children: [
+          {
+            value: "加拿大",
+            label: "加拿大",
+          },
+          {
+            value: "美国",
+            label: "美国",
+          },
+          {
+            value: "墨西哥",
+            label: "墨西哥",
+          },
+          {
+            value: "危地马拉",
+            label: "危地马拉",
+          },
+          {
+            value: "伯利兹",
+            label: "伯利兹",
+          },
+          {
+            value: "萨尔瓦多",
+            label: "萨尔瓦多",
+          },
+          {
+            value: "洪都拉斯",
+            label: "洪都拉斯",
+          },
+          {
+            value: "尼加拉瓜",
+            label: "尼加拉瓜",
+          },
+          {
+            value: "哥斯达黎加",
+            label: "哥斯达黎加",
+          },
+          {
+            value: "巴拿马",
+            label: "巴拿马",
+          },
+          {
+            value: "巴哈马群岛",
+            label: "巴哈马群岛",
+          },
+          {
+            value: "古巴",
+            label: "古巴",
+          },
+          {
+            value: "牙买加",
+            label: "牙买加",
+          },
+          {
+            value: "海地",
+            label: "海地",
+          },
+          {
+            value: "多米尼加共和国",
+            label: "多米尼加共和国",
+          },
+          {
+            value: "安提瓜和巴布达",
+            label: "安提瓜和巴布达",
+          },
+          {
+            value: "圣基茨和尼维斯",
+            label: "圣基茨和尼维斯",
+          },
+          {
+            value: "多米尼克",
+            label: "多米尼克",
+          },
+          {
+            value: "圣卢西亚",
+            label: "圣卢西亚",
+          },
+          {
+            value: "圣文森特和格林纳丁斯",
+            label: "圣文森特和格林纳丁斯",
+          },
+          {
+            value: "格林纳达",
+            label: "格林纳达",
+          },
+          {
+            value: "巴巴多斯",
+            label: "巴巴多斯",
+          },
+          {
+            value: "特立尼达和多巴哥",
+            label: "特立尼达和多巴哥",
+          },
+        ],
+      },
+      {
+        value: "南美洲",
+        label: "南美洲",
+        children: [
+          {
+            value: "哥伦比亚",
+            label: "哥伦比亚",
+          },
+          {
+            value: "委内瑞拉",
+            label: "委内瑞拉",
+          },
+          {
+            value: "圭亚那",
+            label: "圭亚那",
+          },
+          {
+            value: "苏里南",
+            label: "苏里南",
+          },
+          {
+            value: "厄瓜多尔",
+            label: "厄瓜多尔",
+          },
+          {
+            value: "秘鲁",
+            label: "秘鲁",
+          },
+          {
+            value: "巴西",
+            label: "巴西",
+          },
+          {
+            value: "智利",
+            label: "智利",
+          },
+          {
+            value: "阿根廷",
+            label: "阿根廷",
+          },
+          {
+            value: "乌拉圭",
+            label: "乌拉圭",
+          },
+          {
+            value: "巴拉圭",
+            label: "巴拉圭",
+          },
+        ],
+      },
+      {
+        value: "大洋洲",
+        label: "大洋洲",
+        children: [
+          {
+            value: "澳大利亚",
+            label: "澳大利亚",
+          },
+          {
+            value: "新西兰",
+            label: "新西兰",
+          },
+          {
+            value: "帕劳",
+            label: "帕劳",
+          },
+          {
+            value: "密克罗尼西亚联邦",
+            label: "密克罗尼西亚联邦",
+          },
+          {
+            value: "马绍尔群岛",
+            label: "马绍尔群岛",
+          },
+          {
+            value: "基里巴斯",
+            label: "基里巴斯",
+          },
+          {
+            value: "瑙鲁",
+            label: "瑙鲁",
+          },
+          {
+            value: "巴布亚新几内亚",
+            label: "巴布亚新几内亚",
+          },
+          {
+            value: "所罗门群岛",
+            label: "所罗门群岛",
+          },
+          {
+            value: "瓦努阿图",
+            label: "瓦努阿图",
+          },
+          {
+            value: "斐济",
+            label: "斐济",
+          },
+          {
+            value: "图瓦卢",
+            label: "图瓦卢",
+          },
+          {
+            value: "萨摩亚",
+            label: "萨摩亚",
+          },
+          {
+            value: "汤加",
+            label: "汤加",
+          },
+          {
+            value: "库克群岛",
+            label: "库克群岛",
+          },
+        ],
+      },
+    ]
+
+export {
+    region
+}

+ 33 - 0
ui/smarttrade-platform/src/utils/time.js

@@ -0,0 +1,33 @@
+/**
+ * 秒级时间戳 转 多久前/多久后
+ * @param {number} timestamp 秒时间戳
+ * @returns string
+ */
+export function formatTimeAgo(timestamp) {
+  const now = Math.floor(Date.now() / 1000) // 当前秒级时间戳
+  const diff = timestamp - now // 差值,正数=未来,负数=过去
+
+  const absDiff = Math.abs(diff)
+  const minute = 60
+  const hour = minute * 60
+  const day = hour * 24
+  const month = day * 30
+  const year = day * 365
+
+  let text = ''
+  if (absDiff < minute) {
+    text = `${absDiff}秒`
+  } else if (absDiff < hour) {
+    text = `${Math.floor(absDiff / minute)}分钟`
+  } else if (absDiff < day) {
+    text = `${Math.floor(absDiff / hour)}小时`
+  } else if (absDiff < month) {
+    text = `${Math.floor(absDiff / day)}天`
+  } else if (absDiff < year) {
+    text = `${Math.floor(absDiff / month)}个月`
+  } else {
+    text = `${Math.floor(absDiff / year)}年`
+  }
+
+  return diff > 0 ? `${text}后` : `${text}前`
+}

+ 122 - 107
ui/smarttrade-platform/src/views/CustomersView.vue

@@ -36,16 +36,11 @@
     <div style="display: flex; gap: 16px; margin-bottom: 20px;">
       <div class="search-box" style="flex: 1;">
         <i class="fas fa-search search-icon"></i>
-        <input type="text" v-model="searchQuery" class="search-input" placeholder="搜索公司名称、联系人、邮箱...">
+        <input type="text" v-model="searchQuery" class="search-input" placeholder="搜索公司名称、联系人、邮箱..." @input="getCustomer">
       </div>
       <div style="display: flex; gap: 8px;">
-        <select v-model="regionFilter" class="form-select" style="width: auto; padding: 8px 16px;">
-          <option value="">全部地区</option>
-          <option value="中东">中东</option>
-          <option value="欧洲">欧洲</option>
-          <option value="北美">北美</option>
-          <option value="东南亚">东南亚</option>
-        </select>
+        <el-cascader class="form-select" style="width: auto; padding: 0;" placeholder="区域名称" :options="region"
+          v-model="regionValue" clearable filterable @change="getRegion" />
         <select v-model="industryFilter" class="form-select" style="width: auto; padding: 8px 16px;">
           <option value="">全部行业</option>
           <option value="固态硬盘">固态硬盘</option>
@@ -81,24 +76,25 @@
         <div class="customer-flag">{{ customer.flag }}</div>
         <div class="customer-info">
           <div class="customer-header">
-            <span class="customer-name">{{ customer.name }}</span>
-            <span class="customer-status" :style="{ background: getStatusBg(customer.status), color: getStatusColor(customer.status) }">
+            <span class="customer-name">{{ customer.customerName }}</span>
+            <!-- 客户类型标识 -->
+            <!-- <span class="customer-status" :style="{ background: getStatusBg(customer.status), color: getStatusColor(customer.status) }">
               {{ getStatusLabel(customer.status) }}
-            </span>
+            </span> -->
           </div>
           <div class="customer-contact">
-            <i class="fas fa-user"></i> {{ customer.contact }} · {{ customer.role }}
+            <i class="fas fa-user"></i> {{ customer.liaison.name }}<span v-if="customer.liaison.departmentPosition"> · </span>{{ customer.liaison.departmentPosition }}
           </div>
           <div class="customer-meta">
-            <span><i class="fas fa-map-marker-alt"></i> {{ customer.location }}</span>
-            <span><i class="fas fa-industry"></i> {{ customer.industry }}</span>
-            <span><i class="fas fa-dollar-sign"></i> {{ customer.annualPurchase }}</span>
+            <span><i class="fas fa-map-marker-alt"></i> {{ customer.continent }} · {{ customer.country }}</span>
+            <span><i class="fas fa-industry"></i> {{ customer.customerCompany.industry }}</span>
+            <!-- <span><i class="fas fa-dollar-sign"></i> {{ customer.annualPurchase }}</span> -->
           </div>
         </div>
-        <div class="customer-score">
+        <!-- <div class="customer-score">
           <div class="score-circle" :class="getScoreClass(customer.score)">{{ customer.score }}</div>
           <div class="score-label">意向分</div>
-        </div>
+        </div> -->
         <div class="customer-actions" @click.stop>
           <button class="btn btn-secondary btn-sm" @click="showCustomerDetail(customer)"><i class="fas fa-eye"></i>更多详情</button>
         </div>
@@ -127,7 +123,7 @@
         </div>
         <div class="modal-body">
           <!-- Quick Stats -->
-          <div class="detail-stats">
+          <!-- <div class="detail-stats">
             <div class="detail-stat-card">
               <div class="detail-stat-value" :class="getScoreClass(selectedCustomer.score)">{{ selectedCustomer.score }}</div>
               <div class="detail-stat-label">意向分</div>
@@ -144,7 +140,7 @@
               <div class="detail-stat-value">{{ selectedCustomer.lastContact }}</div>
               <div class="detail-stat-label">最后联系</div>
             </div>
-          </div>
+          </div> -->
 
           <!-- Contact Info -->
           <div class="detail-section">
@@ -154,29 +150,29 @@
                 <i class="fas fa-user"></i>
                 <div class="contact-content">
                   <div class="contact-label">主要联系人</div>
-                  <div class="contact-value">{{ selectedCustomer.contact }}</div>
-                  <div class="contact-sub">{{ selectedCustomer.role }}</div>
+                  <div class="contact-value">{{ selectedCustomer.liaison?.name }}</div>
+                  <div class="contact-sub">{{ selectedCustomer.liaison?.departmentPosition }}</div>
                 </div>
               </div>
               <div class="contact-item">
                 <i class="fas fa-envelope"></i>
                 <div class="contact-content">
                   <div class="contact-label">邮箱</div>
-                  <div class="contact-value">{{ selectedCustomer.email }}</div>
+                  <div class="contact-value">{{ selectedCustomer.liaison?.email }}</div>
                 </div>
               </div>
               <div class="contact-item">
                 <i class="fas fa-phone"></i>
                 <div class="contact-content">
                   <div class="contact-label">电话</div>
-                  <div class="contact-value">{{ selectedCustomer.phone }}</div>
+                  <div class="contact-value">{{ selectedCustomer.liaison?.telephone }}</div>
                 </div>
               </div>
               <div class="contact-item">
                 <i class="fas fa-globe"></i>
                 <div class="contact-content">
                   <div class="contact-label">官网</div>
-                  <div class="contact-value link">{{ selectedCustomer.website }}</div>
+                  <div class="contact-value link">{{ selectedCustomer.customerCompany?.website }}</div>
                 </div>
               </div>
             </div>
@@ -185,7 +181,7 @@
           <!-- AI Insights -->
           <div class="detail-section">
             <h4><i class="fas fa-robot"></i> AI 洞察</h4>
-            <div class="ai-insight-cards">
+            <div class="ai-insight-cards" v-if="selectedCustomer.aiInsights">
               <div class="ai-insight-card" v-for="insight in selectedCustomer.aiInsights" :key="insight.type">
                 <div class="insight-icon" :class="insight.level">
                   <i :class="insight.icon"></i>
@@ -201,7 +197,7 @@
           <!-- Interaction Timeline -->
           <div class="detail-section">
             <h4><i class="fas fa-history"></i> 互动记录</h4>
-            <div class="timeline">
+            <div class="timeline" v-if="selectedCustomer.interactions">
               <div class="timeline-item" v-for="record in selectedCustomer.interactions" :key="record.id">
                 <div class="timeline-dot" :class="record.type"></div>
                 <div class="timeline-content">
@@ -231,7 +227,7 @@
         </div>
         <div class="modal-footer">
           <button class="btn btn-secondary" @click="showDetailModal = false">关闭</button>
-          <button class="btn btn-secondary"><i class="fas fa-edit"></i> 编辑</button>
+          <!-- <button class="btn btn-secondary"><i class="fas fa-edit"></i> 编辑</button> -->
           <button class="btn btn-primary" @click="">
             <i class="fas fa-rocket"></i> 客户详情
           </button>
@@ -252,6 +248,7 @@
 <script setup>
 import { ref, computed } from 'vue'
 import { getCustomerList } from '../api/customer'
+import {region} from '@/utils/region';
 // 引入添加客户组件
 import AddCustomerModal from '../components/addCustomer.vue'
 
@@ -266,89 +263,106 @@ const showToast = ref(false)
 const toastMessage = ref('')
 const toastType = ref('success')
 
-const customerStats = {
-  total: 1247,
-  prospect: 812,
-  intent: 224,
-  deal: 156,
-  silent: 55
-}
-
-const tabs = [
-  { key: 'all', label: '全部客户', count: 1247 },
-  { key: 'prospect', label: '潜在客户', count: 812 },
-  { key: 'intent', label: '意向客户', count: 224 },
-  { key: 'deal', label: '成交客户', count: 156 },
-  { key: 'silent', label: '沉默客户', count: 55 }
-]
-
-getCustomerList().then(res => {
-  console.log('res :>》》》> ', res);
+const customerStats = ref({
+  total: 0,
+  prospect: 0,
+  intent: 0,
+  deal: 0,
+  silent: 0
 })
 
-const customers = ref([
-  { 
-    id: 1, flag: '🇸🇦', name: 'Riyadh Tech Distribution', status: 'prospect', contact: 'Ahmed Al-Rashid', 
-    role: '采购总监', location: '沙特·利雅得', industry: '固态硬盘进口商', annualPurchase: '年采购$2M+', score: 92,
-    email: 'ahmed@riyahtech.sa', phone: '+966 50 123 4567', website: 'www.riyahtech.sa',
-    totalDeals: 3, totalAmount: 4560000, lastContact: '3天前',
-    aiInsights: [
-      { type: 'purchase', level: 'high', icon: 'fas fa-shopping-cart', title: '采购意愿强烈', description: '近30天内访问官网5次,下载产品手册,建议重点跟进' },
-      { type: 'timing', level: 'medium', icon: 'fas fa-clock', title: '最佳联系时间', description: '根据历史数据分析,周二上午10点联系成功率最高' },
-      { type: 'decision', level: 'high', icon: 'fas fa-user-tie', title: '决策人已识别', description: 'Ahmed Al-Rashid 为采购决策人,已验证邮箱有效性' }
-    ],
-    interactions: [
-      { id: 1, type: 'email', action: '发送报价单', detail: 'SSD采购报价已发送,等待回复', time: '3天前' },
-      { id: 2, type: 'visit', action: '官网访问', detail: '浏览了企业级SSD产品页面,停留8分钟', time: '5天前' },
-      { id: 3, type: 'call', action: '电话沟通', detail: '确认了采购需求,数量约5000片', time: '1周前' },
-      { id: 4, type: 'email', action: '首次联系', detail: '通过LinkedIn找到联系人,发送介绍邮件', time: '2周前' }
-    ],
-    opportunities: [
-      { id: 1, name: 'SSD批量采购', amount: 850000, stage: '谈判中', statusClass: 'negotiation', statusLabel: '跟进中' }
-    ]
-  },
-  { 
-    id: 2, flag: '🇦🇪', name: 'Dubai Storage Solutions', status: 'prospect', contact: 'Mohammed Khan', 
-    role: 'CEO', location: '阿联酋·迪拜', industry: '电子分销商', annualPurchase: '年采购$1.5M', score: 88,
-    email: 'mohammed@dubaistorage.ae', phone: '+971 50 987 6543', website: 'www.dubaistorage.ae',
-    totalDeals: 2, totalAmount: 2800000, lastContact: '1天前',
-    aiInsights: [
-      { type: 'purchase', level: 'medium', icon: 'fas fa-chart-line', title: '采购潜力', description: '公司规模扩张中,预计明年需求增长30%' }
-    ],
-    interactions: [
-      { id: 1, type: 'meeting', action: '视频会议', detail: '讨论Q2采购计划,对NVMe SSD感兴趣', time: '1天前' }
-    ],
-    opportunities: []
-  },
-  { 
-    id: 3, flag: '🇰🇼', name: 'Kuwait Data Systems', status: 'intent', contact: 'Faisal Al-Mutairi', 
-    role: 'IT总监', location: '科威特', industry: '数据中心集成商', annualPurchase: '年采购$2.5M', score: 85,
-    email: 'faisal@kwdata.kw', phone: '+965 99 888 777', website: 'www.kwdata.kw',
-    totalDeals: 5, totalAmount: 6800000, lastContact: '今天',
-    aiInsights: [
-      { type: 'timing', level: 'high', icon: 'fas fa-fire', title: '紧急需求', description: '数据中心扩容项目急需采购,建议立即跟进' }
-    ],
-    interactions: [
-      { id: 1, type: 'email', action: '回复询价', detail: '确认了技术规格,要求提供样品', time: '今天' }
-    ],
-    opportunities: [
-      { id: 1, name: '数据中心扩容项目', amount: 1200000, stage: '方案报价', statusClass: 'proposal', statusLabel: '待报价' }
-    ]
-  },
-  { 
-    id: 4, flag: '🇩🇪', name: 'Berlin Tech GmbH', status: 'deal', contact: 'Hans Mueller', 
-    role: '采购经理', location: '德国·柏林', industry: '企业级存储', annualPurchase: '年采购$3M+', score: 95,
-    email: 'hans@berlintech.de', phone: '+49 30 12345678', website: 'www.berlintech.de',
-    totalDeals: 8, totalAmount: 12500000, lastContact: '2天前',
-    aiInsights: [],
-    interactions: [
-      { id: 1, type: 'order', action: '订单确认', detail: 'PO-2026-0315 已确认,金额¥680,000', time: '2天前' }
-    ],
-    opportunities: [
-      { id: 1, name: 'NVMe SSD批量采购', amount: 680000, stage: '已成交', statusClass: 'won', statusLabel: '已成交' }
-    ]
-  }
+const tabs = ref([
+  { key: 'all', label: '全部客户', count: 0 },
+  // { key: 'prospect', label: '潜在客户', count: 812 },
+  // { key: 'intent', label: '意向客户', count: 224 },
+  // { key: 'deal', label: '成交客户', count: 156 },
+  // { key: 'silent', label: '沉默客户', count: 55 }
 ])
+const customers = ref([])
+const params = ref({
+  pageIndex: 1, 
+  pageSize: 10,
+  blurry:null,
+  country:null,
+})
+const getCustomer = () => {
+    params.value.blurry = searchQuery.value
+    getCustomerList(params.value).then(res => {
+    customerStats.value.total = tabs.value[0].count = res.result.total
+    customers.value = res.result.records
+  })
+}
+getCustomer()
+
+const regionValue = ref([])
+const getRegion = (value) => {
+    params.value.country = value?value[1]:''
+    getCustomer()
+}
+
+// const customers = ref([
+//   { 
+//     id: 1, flag: '🇸🇦', name: 'Riyadh Tech Distribution', status: 'prospect', contact: 'Ahmed Al-Rashid', 
+//     role: '采购总监', location: '沙特·利雅得', industry: '固态硬盘进口商', annualPurchase: '年采购$2M+', score: 92,
+//     email: 'ahmed@riyahtech.sa', phone: '+966 50 123 4567', website: 'www.riyahtech.sa',
+//     totalDeals: 3, totalAmount: 4560000, lastContact: '3天前',
+//     aiInsights: [
+//       { type: 'purchase', level: 'high', icon: 'fas fa-shopping-cart', title: '采购意愿强烈', description: '近30天内访问官网5次,下载产品手册,建议重点跟进' },
+//       { type: 'timing', level: 'medium', icon: 'fas fa-clock', title: '最佳联系时间', description: '根据历史数据分析,周二上午10点联系成功率最高' },
+//       { type: 'decision', level: 'high', icon: 'fas fa-user-tie', title: '决策人已识别', description: 'Ahmed Al-Rashid 为采购决策人,已验证邮箱有效性' }
+//     ],
+//     interactions: [
+//       { id: 1, type: 'email', action: '发送报价单', detail: 'SSD采购报价已发送,等待回复', time: '3天前' },
+//       { id: 2, type: 'visit', action: '官网访问', detail: '浏览了企业级SSD产品页面,停留8分钟', time: '5天前' },
+//       { id: 3, type: 'call', action: '电话沟通', detail: '确认了采购需求,数量约5000片', time: '1周前' },
+//       { id: 4, type: 'email', action: '首次联系', detail: '通过LinkedIn找到联系人,发送介绍邮件', time: '2周前' }
+//     ],
+//     opportunities: [
+//       { id: 1, name: 'SSD批量采购', amount: 850000, stage: '谈判中', statusClass: 'negotiation', statusLabel: '跟进中' }
+//     ]
+//   },
+//   { 
+//     id: 2, flag: '🇦🇪', name: 'Dubai Storage Solutions', status: 'prospect', contact: 'Mohammed Khan', 
+//     role: 'CEO', location: '阿联酋·迪拜', industry: '电子分销商', annualPurchase: '年采购$1.5M', score: 88,
+//     email: 'mohammed@dubaistorage.ae', phone: '+971 50 987 6543', website: 'www.dubaistorage.ae',
+//     totalDeals: 2, totalAmount: 2800000, lastContact: '1天前',
+//     aiInsights: [
+//       { type: 'purchase', level: 'medium', icon: 'fas fa-chart-line', title: '采购潜力', description: '公司规模扩张中,预计明年需求增长30%' }
+//     ],
+//     interactions: [
+//       { id: 1, type: 'meeting', action: '视频会议', detail: '讨论Q2采购计划,对NVMe SSD感兴趣', time: '1天前' }
+//     ],
+//     opportunities: []
+//   },
+//   { 
+//     id: 3, flag: '🇰🇼', name: 'Kuwait Data Systems', status: 'intent', contact: 'Faisal Al-Mutairi', 
+//     role: 'IT总监', location: '科威特', industry: '数据中心集成商', annualPurchase: '年采购$2.5M', score: 85,
+//     email: 'faisal@kwdata.kw', phone: '+965 99 888 777', website: 'www.kwdata.kw',
+//     totalDeals: 5, totalAmount: 6800000, lastContact: '今天',
+//     aiInsights: [
+//       { type: 'timing', level: 'high', icon: 'fas fa-fire', title: '紧急需求', description: '数据中心扩容项目急需采购,建议立即跟进' }
+//     ],
+//     interactions: [
+//       { id: 1, type: 'email', action: '回复询价', detail: '确认了技术规格,要求提供样品', time: '今天' }
+//     ],
+//     opportunities: [
+//       { id: 1, name: '数据中心扩容项目', amount: 1200000, stage: '方案报价', statusClass: 'proposal', statusLabel: '待报价' }
+//     ]
+//   },
+//   { 
+//     id: 4, flag: '🇩🇪', name: 'Berlin Tech GmbH', status: 'deal', contact: 'Hans Mueller', 
+//     role: '采购经理', location: '德国·柏林', industry: '企业级存储', annualPurchase: '年采购$3M+', score: 95,
+//     email: 'hans@berlintech.de', phone: '+49 30 12345678', website: 'www.berlintech.de',
+//     totalDeals: 8, totalAmount: 12500000, lastContact: '2天前',
+//     aiInsights: [],
+//     interactions: [
+//       { id: 1, type: 'order', action: '订单确认', detail: 'PO-2026-0315 已确认,金额¥680,000', time: '2天前' }
+//     ],
+//     opportunities: [
+//       { id: 1, name: 'NVMe SSD批量采购', amount: 680000, stage: '已成交', statusClass: 'won', statusLabel: '已成交' }
+//     ]
+//   }
+// ])
 
 const filteredCustomers = computed(() => {
   let result = customers.value
@@ -501,6 +515,7 @@ const handleAddCustomer = (customerData) => {
 .search-input { width: 100%; padding: 10px 14px 10px 40px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; color: var(--text-primary); font-size: 13px; }
 .search-input:focus { outline: none; border-color: var(--primary); }
 .form-select { background: var(--bg-card); border: 1px solid var(--border); color: var(--text-primary); }
+.form-select :deep(.el-input__wrapper){ background: var(--bg-card);}
 .customer-card { display: flex; align-items: center; gap: 16px; padding: 16px; background: var(--bg-card); border-radius: 12px; margin-bottom: 12px; border: 1px solid var(--border); cursor: pointer; transition: all 0.2s; }
 .customer-card:hover { border-color: var(--primary); }
 .customer-flag { font-size: 32px; }

+ 91 - 59
ui/smarttrade-platform/src/views/KnowledgeView.vue

@@ -125,23 +125,23 @@
       </div>
     </div>
 
-    <!-- Categories Tab -->
+    <!-- 知识分类 Tab -->
     <div v-if="activeTab === 'categories'" class="section">
       <div class="category-grid">
-        <div v-for="category in filteredCategories" :key="category.id"
+        <div v-for="category in categories" :key="category.id"
              class="category-card"
              @click="openCategory(category)">
-          <div class="category-header">
+          <!-- <div class="category-header">
             <div class="category-icon" :style="{ background: category.iconBg }">{{ category.icon }}</div>
             <div class="category-badge" v-if="category.trending">
               <i class="fas fa-fire"></i> 热门
             </div>
-          </div>
+          </div> -->
           <div class="category-name">{{ category.name }}</div>
           <div class="category-desc">{{ category.description }}</div>
           <div class="category-stats">
             <div class="category-stat">
-              <span class="stat-value">{{ category.count }}</span>
+              <span class="stat-value">{{ category.document_count }}</span>
               <span class="stat-label">知识点</span>
             </div>
             <div class="category-stat">
@@ -154,31 +154,31 @@
             </div>
           </div>
           <div class="category-footer">
-            <span class="update-time">更新于 {{ category.updatedAt }}</span>
+            <span class="update-time">更新于 {{ category.timeip }}</span>
             <i class="fas fa-chevron-right"></i>
           </div>
         </div>
       </div>
     </div>
 
-    <!-- All Knowledge Tab -->
+    <!-- 全部知识 Tab -->
     <div v-if="activeTab === 'all'" class="section">
       <div class="knowledge-list">
-        <div v-for="item in filteredKnowledge" :key="item.id"
+        <div v-for="item in knowledgeList" :key="item.id"
              class="knowledge-card"
              @click="showKnowledgeDetail(item)">
-          <div class="knowledge-icon" :style="{ background: item.iconBg }">{{ item.icon }}</div>
+          <!-- <div class="knowledge-icon" :style="{ background: item.iconBg }">{{ item.icon }}</div> -->
           <div class="knowledge-content">
             <div class="knowledge-header">
-              <div class="knowledge-question">{{ item.question }}</div>
-              <span class="knowledge-category" :style="{ background: item.iconBg }">{{ item.categoryName }}</span>
+              <div class="knowledge-question">{{ item.name }}</div>
+              <!-- <span class="knowledge-category" :style="{ background: item.iconBg }">{{ item.categoryName }}</span> -->
             </div>
-            <div class="knowledge-answer">{{ item.answer }}</div>
+            <!-- <div class="knowledge-answer">{{ item.answer }}</div>
             <div class="knowledge-meta">
               <span><i class="fas fa-eye"></i> {{ item.views }}次查看</span>
               <span><i class="fas fa-thumbs-up"></i> {{ item.likes }}次好评</span>
               <span><i class="fas fa-clock"></i> {{ item.time }}</span>
-            </div>
+            </div> -->
           </div>
           <div class="knowledge-actions" @click.stop>
             <button class="btn btn-secondary btn-sm" @click="editKnowledge(item)">
@@ -192,7 +192,7 @@
       </div>
     </div>
 
-    <!-- Q&A Records Tab -->
+    <!-- 问答记录 Tab -->
     <div v-if="activeTab === 'qa'" class="section">
       <div class="qa-section">
         <div class="qa-header">
@@ -241,7 +241,7 @@
       </div>
     </div>
 
-    <!-- Training Tab -->
+    <!-- AI训练 Tab -->
     <div v-if="activeTab === 'training'" class="section">
       <div class="training-section">
         <div class="training-header">
@@ -283,7 +283,7 @@
       </div>
     </div>
 
-    <!-- Settings Tab -->
+    <!-- 设置 Tab -->
     <div v-if="activeTab === 'settings'" class="section">
       <div class="settings-section">
         <div class="settings-card">
@@ -441,7 +441,7 @@
             </div>
             <div class="detail-stat">
               <i class="fas fa-clock"></i>
-              <span>更新于 {{ selectedKnowledge.time }}</span>
+              <span>更新于 {{ selectedKnowledge.timeip }}</span>
             </div>
           </div>
         </div>
@@ -499,6 +499,8 @@
 
 <script setup>
 import { ref, computed } from 'vue'
+import { knowledgeDatasets,knowledgeDocuments } from '../api/knowledge'
+import { formatTimeAgo } from '@/utils/time'
 
 const showAddModal = ref(false)
 const showDetailModal = ref(false)
@@ -529,13 +531,13 @@ const syncStatus = {
   lastSync: '2天前'
 }
 
-const tabs = [
-  { key: 'categories', label: '知识分类', count: 12, icon: 'fas fa-folder' },
-  { key: 'all', label: '全部知识', count: 1247, icon: 'fas fa-book' },
-  { key: 'qa', label: '问答记录', count: 356, icon: 'fas fa-comments' },
-  { key: 'training', label: 'AI 训练', count: 3, icon: 'fas fa-dumbbell' },
+const tabs = ref([
+  { key: 'categories', label: '知识分类', count: 0, icon: 'fas fa-folder' },
+  { key: 'all', label: '全部知识', count: 0, icon: 'fas fa-book' },
+  { key: 'qa', label: '问答记录', count: 0, icon: 'fas fa-comments' },
+  { key: 'training', label: 'AI 训练', count: 0, icon: 'fas fa-dumbbell' },
   { key: 'settings', label: '设置', count: 0, icon: 'fas fa-cog' }
-]
+])
 
 const newKnowledge = ref({
   question: '',
@@ -545,25 +547,25 @@ const newKnowledge = ref({
   relatedQuestions: ''
 })
 
-const categories = ref([
-  { id: 1, icon: '📦', iconBg: 'rgba(99, 102, 241, 0.2)', name: '产品知识', description: '产品规格、型号、功能特性', count: 234, queries: 1256, accuracy: 96, trending: true, updatedAt: '2小时前' },
-  { id: 2, icon: '💰', iconBg: 'rgba(16, 185, 129, 0.2)', name: '价格政策', description: '价格体系、折扣规则、付款条件', count: 156, queries: 892, accuracy: 98, trending: false, updatedAt: '昨天' },
-  { id: 3, icon: '🚚', iconBg: 'rgba(245, 158, 11, 0.2)', name: '物流信息', description: '交期、运费、物流方式', count: 89, queries: 567, accuracy: 92, trending: false, updatedAt: '3天前' },
-  { id: 4, icon: '📋', iconBg: 'rgba(139, 92, 246, 0.2)', name: '商务条款', description: '合同条款、质保政策、售后服务', count: 178, queries: 445, accuracy: 94, trending: false, updatedAt: '昨天' },
-  { id: 5, icon: '🏭', iconBg: 'rgba(6, 182, 212, 0.2)', name: '行业知识', description: '行业趋势、技术标准、认证信息', count: 245, queries: 334, accuracy: 88, trending: true, updatedAt: '1周前' },
-  { id: 6, icon: '🌍', iconBg: 'rgba(236, 72, 153, 0.2)', name: '市场信息', description: '市场行情、竞争对手、客户案例', count: 167, queries: 289, accuracy: 85, trending: false, updatedAt: '5天前' },
-  { id: 7, icon: '🔧', iconBg: 'rgba(59, 130, 246, 0.2)', name: '技术参数', description: '技术规格、性能指标、兼容性', count: 98, queries: 456, accuracy: 97, trending: false, updatedAt: '2天前' },
-  { id: 8, icon: '❓', iconBg: 'rgba(148, 163, 184, 0.2)', name: '常见问题', description: '客户高频问题及标准回答', count: 80, queries: 1234, accuracy: 95, trending: true, updatedAt: '今天' }
-])
-
-const knowledgeList = ref([
-  { id: 1, icon: '📦', iconBg: 'rgba(99, 102, 241, 0.2)', categoryName: '产品知识', question: 'MOQ最小起订量是多少?', answer: '标准产品MOQ为50件,定制产品MOQ为100件,具体视产品类型而定。', extendedAnswer: '对于常规库存产品,我们接受最低50件起订。定制产品由于需要单独开模和生产准备,最低起订量为100件。大客户可享受更灵活的起订量政策,请联系销售经理协商。', tags: ['MOQ', '起订量', '订单'], relatedQuestions: ['可以降低起订量吗?', '首单有优惠吗?'], views: 1234, likes: 89, time: '2小时前' },
-  { id: 2, icon: '🚚', iconBg: 'rgba(245, 158, 11, 0.2)', categoryName: '物流信息', question: '交期一般是多久?', answer: '常规产品15-20天,定制产品30-45天,加急订单可缩短至7天。', extendedAnswer: '标准产品通常有库存,交期15-20个工作日。定制产品需要根据复杂程度评估,一般为30-45天。如有紧急需求,我们提供加急服务,最快可7天交货,但需收取10-20%的加急费用。', tags: ['交期', '发货', '物流'], relatedQuestions: ['可以加急吗?', '运费谁承担?'], views: 987, likes: 67, time: '昨天' },
-  { id: 3, icon: '💰', iconBg: 'rgba(16, 185, 129, 0.2)', categoryName: '价格政策', question: '付款方式有哪些?', answer: '支持T/T、L/C、PayPal等多种付款方式,新客户首单建议30%预付款。', extendedAnswer: '我们接受电汇(T/T)、信用证(L/C)、PayPal、西联汇款等付款方式。对于老客户可提供月结30天账期。新客户首单建议预付30%,发货前付清尾款。大额订单可协商分期付款。', tags: ['付款', 'T/T', '信用证'], relatedQuestions: ['可以月结吗?', '支持什么货币?'], views: 876, likes: 54, time: '2天前' },
-  { id: 4, icon: '🔧', iconBg: 'rgba(59, 130, 246, 0.2)', categoryName: '技术参数', question: 'SSD的使用寿命是多久?', answer: '企业级SSD设计寿命为5-10年,写入寿命(TBW)根据容量不同。', extendedAnswer: '我们的企业级SSD采用高耐久性NAND闪存,设计寿命5-10年。1TB型号TBW为600TB,2TB型号为1200TB。在正常工作负载下,可稳定运行超过200万小时(MTBF)。', tags: ['SSD', '寿命', 'TBW'], relatedQuestions: ['如何查看剩余寿命?', '保修期多久?'], views: 654, likes: 45, time: '3天前' },
-  { id: 5, icon: '📋', iconBg: 'rgba(139, 92, 246, 0.2)', categoryName: '商务条款', question: '质保期是多久?', answer: '标准产品提供3年质保,企业级产品提供5年质保。', extendedAnswer: '消费级产品提供3年有限质保,企业级产品提供5年质保服务。质保期内非人为损坏可免费更换。我们提供全球联保服务,无论在哪购买均可享受本地化售后服务。', tags: ['质保', '售后', '保修'], relatedQuestions: ['质保包括哪些?', '如何申请保修?'], views: 543, likes: 38, time: '4天前' },
-  { id: 6, icon: '🏭', iconBg: 'rgba(6, 182, 212, 0.2)', categoryName: '行业知识', question: 'NVMe和SATA有什么区别?', answer: 'NVMe采用PCIe通道,速度更快延迟更低,适合高性能应用。', extendedAnswer: 'NVMe SSD通过PCIe总线直接与CPU通信,理论带宽可达32GB/s,延迟极低。SATA SSD受限于SATA接口,最大速度约600MB/s。对于需要高IOPS的应用场景,NVMe是更好的选择。', tags: ['NVMe', 'SATA', '技术'], relatedQuestions: ['如何选择接口?', '兼容性如何?'], views: 432, likes: 34, time: '5天前' }
-])
+// const categories = ref([
+//   { id: 1, icon: '📦', iconBg: 'rgba(99, 102, 241, 0.2)', name: '产品知识', description: '产品规格、型号、功能特性', count: 234, queries: 1256, accuracy: 96, trending: true, updatedAt: '2小时前' },
+//   { id: 2, icon: '💰', iconBg: 'rgba(16, 185, 129, 0.2)', name: '价格政策', description: '价格体系、折扣规则、付款条件', count: 156, queries: 892, accuracy: 98, trending: false, updatedAt: '昨天' },
+//   { id: 3, icon: '🚚', iconBg: 'rgba(245, 158, 11, 0.2)', name: '物流信息', description: '交期、运费、物流方式', count: 89, queries: 567, accuracy: 92, trending: false, updatedAt: '3天前' },
+//   { id: 4, icon: '📋', iconBg: 'rgba(139, 92, 246, 0.2)', name: '商务条款', description: '合同条款、质保政策、售后服务', count: 178, queries: 445, accuracy: 94, trending: false, updatedAt: '昨天' },
+//   { id: 5, icon: '🏭', iconBg: 'rgba(6, 182, 212, 0.2)', name: '行业知识', description: '行业趋势、技术标准、认证信息', count: 245, queries: 334, accuracy: 88, trending: true, updatedAt: '1周前' },
+//   { id: 6, icon: '🌍', iconBg: 'rgba(236, 72, 153, 0.2)', name: '市场信息', description: '市场行情、竞争对手、客户案例', count: 167, queries: 289, accuracy: 85, trending: false, updatedAt: '5天前' },
+//   { id: 7, icon: '🔧', iconBg: 'rgba(59, 130, 246, 0.2)', name: '技术参数', description: '技术规格、性能指标、兼容性', count: 98, queries: 456, accuracy: 97, trending: false, updatedAt: '2天前' },
+//   { id: 8, icon: '❓', iconBg: 'rgba(148, 163, 184, 0.2)', name: '常见问题', description: '客户高频问题及标准回答', count: 80, queries: 1234, accuracy: 95, trending: true, updatedAt: '今天' }
+// ])
+
+// const knowledgeList = ref([
+//   { id: 1, icon: '📦', iconBg: 'rgba(99, 102, 241, 0.2)', categoryName: '产品知识', question: 'MOQ最小起订量是多少?', answer: '标准产品MOQ为50件,定制产品MOQ为100件,具体视产品类型而定。', extendedAnswer: '对于常规库存产品,我们接受最低50件起订。定制产品由于需要单独开模和生产准备,最低起订量为100件。大客户可享受更灵活的起订量政策,请联系销售经理协商。', tags: ['MOQ', '起订量', '订单'], relatedQuestions: ['可以降低起订量吗?', '首单有优惠吗?'], views: 1234, likes: 89, time: '2小时前' },
+//   { id: 2, icon: '🚚', iconBg: 'rgba(245, 158, 11, 0.2)', categoryName: '物流信息', question: '交期一般是多久?', answer: '常规产品15-20天,定制产品30-45天,加急订单可缩短至7天。', extendedAnswer: '标准产品通常有库存,交期15-20个工作日。定制产品需要根据复杂程度评估,一般为30-45天。如有紧急需求,我们提供加急服务,最快可7天交货,但需收取10-20%的加急费用。', tags: ['交期', '发货', '物流'], relatedQuestions: ['可以加急吗?', '运费谁承担?'], views: 987, likes: 67, time: '昨天' },
+//   { id: 3, icon: '💰', iconBg: 'rgba(16, 185, 129, 0.2)', categoryName: '价格政策', question: '付款方式有哪些?', answer: '支持T/T、L/C、PayPal等多种付款方式,新客户首单建议30%预付款。', extendedAnswer: '我们接受电汇(T/T)、信用证(L/C)、PayPal、西联汇款等付款方式。对于老客户可提供月结30天账期。新客户首单建议预付30%,发货前付清尾款。大额订单可协商分期付款。', tags: ['付款', 'T/T', '信用证'], relatedQuestions: ['可以月结吗?', '支持什么货币?'], views: 876, likes: 54, time: '2天前' },
+//   { id: 4, icon: '🔧', iconBg: 'rgba(59, 130, 246, 0.2)', categoryName: '技术参数', question: 'SSD的使用寿命是多久?', answer: '企业级SSD设计寿命为5-10年,写入寿命(TBW)根据容量不同。', extendedAnswer: '我们的企业级SSD采用高耐久性NAND闪存,设计寿命5-10年。1TB型号TBW为600TB,2TB型号为1200TB。在正常工作负载下,可稳定运行超过200万小时(MTBF)。', tags: ['SSD', '寿命', 'TBW'], relatedQuestions: ['如何查看剩余寿命?', '保修期多久?'], views: 654, likes: 45, time: '3天前' },
+//   { id: 5, icon: '📋', iconBg: 'rgba(139, 92, 246, 0.2)', categoryName: '商务条款', question: '质保期是多久?', answer: '标准产品提供3年质保,企业级产品提供5年质保。', extendedAnswer: '消费级产品提供3年有限质保,企业级产品提供5年质保服务。质保期内非人为损坏可免费更换。我们提供全球联保服务,无论在哪购买均可享受本地化售后服务。', tags: ['质保', '售后', '保修'], relatedQuestions: ['质保包括哪些?', '如何申请保修?'], views: 543, likes: 38, time: '4天前' },
+//   { id: 6, icon: '🏭', iconBg: 'rgba(6, 182, 212, 0.2)', categoryName: '行业知识', question: 'NVMe和SATA有什么区别?', answer: 'NVMe采用PCIe通道,速度更快延迟更低,适合高性能应用。', extendedAnswer: 'NVMe SSD通过PCIe总线直接与CPU通信,理论带宽可达32GB/s,延迟极低。SATA SSD受限于SATA接口,最大速度约600MB/s。对于需要高IOPS的应用场景,NVMe是更好的选择。', tags: ['NVMe', 'SATA', '技术'], relatedQuestions: ['如何选择接口?', '兼容性如何?'], views: 432, likes: 34, time: '5天前' }
+// ])
 
 const qaRecords = ref([
   { id: 1, sourceType: 'customer', sourceLabel: '客户问答', question: '你们的NVMe SSD支持哪些接口?', answer: '我们提供M.2 2280、M.2 2242和U.2三种接口规格,支持PCIe 4.0 x4总线,向下兼容PCIe 3.0。', time: '10分钟前', rating: 'positive' },
@@ -579,19 +581,53 @@ const trainingTasks = ref([
   { id: 3, name: '客户问答优化', description: '基于历史问答记录优化回复准确性', status: 'paused', progress: 35, documents: 234, learned: 82 }
 ])
 
-const filteredCategories = computed(() => {
-  if (!searchQuery.value) return categories.value
-  return categories.value.filter(c => c.name.includes(searchQuery.value) || c.description.includes(searchQuery.value))
-})
+const categories = ref([])
+// 获取知识分类
+const getKnowledgeDatasets = () => {
+    knowledgeDatasets({ pageIndex: 1, pageSize: 10 }).then(res => {
+    categories.value = res.result.data
+    res.result.data.forEach(el => {
+      el.timeip = formatTimeAgo(el.updated_at)
+    });
+    tabs.value[0].count = res.result.total
+    console.log('res :>> ', res);
+  })
+}
+getKnowledgeDatasets()
 
-const filteredKnowledge = computed(() => {
-  if (!searchQuery.value) return knowledgeList.value
-  return knowledgeList.value.filter(k => 
-    k.question.includes(searchQuery.value) || 
-    k.answer.includes(searchQuery.value) ||
-    k.tags.some(t => t.includes(searchQuery.value))
-  )
-})
+const knowledgeList = ref([])
+// 获取知识分类下的知识
+const documentsParams = ref({})
+const openCategory = (category) => {
+  activeTab.value = 'all'
+  searchQuery.value = category.name
+  showToastMessage(`正在加载 ${category.name} 分类...`, 'success')
+  documentsParams.value = {
+    pageIndex:1,
+    pageSize:10,
+    dataset_id:category.id
+  }
+  knowledgeDocuments(documentsParams.value).then(res => {
+    console.log(res.result.data)
+    knowledgeList.value = res.result.data
+  })
+}
+
+
+
+// const filteredCategories = computed(() => {
+//   if (!searchQuery.value) return categories.value
+//   return categories.value.filter(c => c.name.includes(searchQuery.value) || c.description.includes(searchQuery.value))
+// })
+
+// const filteredKnowledge = computed(() => {
+//   if (!searchQuery.value) return knowledgeList.value
+//   return knowledgeList.value.filter(k => 
+//     k.question.includes(searchQuery.value) || 
+//     k.answer.includes(searchQuery.value) ||
+//     k.tags.some(t => t.includes(searchQuery.value))
+//   )
+// })
 
 const filteredQA = computed(() => {
   let result = qaRecords.value
@@ -608,11 +644,7 @@ const formatNumber = (num) => num.toLocaleString()
 
 const getTaskStatusLabel = (status) => ({ running: '训练中', completed: '已完成', paused: '已暂停' }[status] || status)
 
-const openCategory = (category) => {
-  activeTab.value = 'all'
-  searchQuery.value = category.name
-  showToastMessage(`正在加载 ${category.name} 分类...`, 'success')
-}
+
 
 const showKnowledgeDetail = (item) => {
   selectedKnowledge.value = item