lusa 2 周之前
父節點
當前提交
a644e4b5bf
共有 37 個文件被更改,包括 641 次插入69 次删除
  1. 1 2
      .idea/compiler.xml
  2. 0 6
      .idea/encodings.xml
  3. 2 2
      ui/sp-user-center/.env.development
  4. 1 6
      ui/sp-user-center/index.html
  5. 14 0
      ui/sp-user-center/src/api/app.ts
  6. 二進制
      ui/sp-user-center/src/assets/homeIcon/all_customers@2x.png
  7. 二進制
      ui/sp-user-center/src/assets/homeIcon/all_tasks@2x.png
  8. 二進制
      ui/sp-user-center/src/assets/homeIcon/at_my_tasks@2x.png
  9. 二進制
      ui/sp-user-center/src/assets/homeIcon/briefing_draft@2x.png
  10. 二進制
      ui/sp-user-center/src/assets/homeIcon/command_center@2x.png
  11. 二進制
      ui/sp-user-center/src/assets/homeIcon/contacts@2x.png
  12. 二進制
      ui/sp-user-center/src/assets/homeIcon/data_center@2x.png
  13. 二進制
      ui/sp-user-center/src/assets/homeIcon/follow_up_records@2x.png
  14. 77 0
      ui/sp-user-center/src/assets/homeIcon/iconUtils.ts
  15. 二進制
      ui/sp-user-center/src/assets/homeIcon/key_customers@2x.png
  16. 二進制
      ui/sp-user-center/src/assets/homeIcon/leave_balance@2x.png
  17. 二進制
      ui/sp-user-center/src/assets/homeIcon/monthly_attendance_report@2x.png
  18. 二進制
      ui/sp-user-center/src/assets/homeIcon/my_briefing@2x.png
  19. 二進制
      ui/sp-user-center/src/assets/homeIcon/my_goals@2x.png
  20. 二進制
      ui/sp-user-center/src/assets/homeIcon/my_leads@2x.png
  21. 二進制
      ui/sp-user-center/src/assets/homeIcon/my_opportunities@2x.png
  22. 二進制
      ui/sp-user-center/src/assets/homeIcon/my_orders@2x.png
  23. 二進制
      ui/sp-user-center/src/assets/homeIcon/my_projects@2x.png
  24. 二進制
      ui/sp-user-center/src/assets/homeIcon/my_tasks@2x.png
  25. 二進制
      ui/sp-user-center/src/assets/homeIcon/payslip@2x.png
  26. 二進制
      ui/sp-user-center/src/assets/homeIcon/personal_performance@2x.png
  27. 二進制
      ui/sp-user-center/src/assets/homeIcon/procurement_management@2x.png
  28. 二進制
      ui/sp-user-center/src/assets/homeIcon/provident_fund_payment_records@2x.png
  29. 二進制
      ui/sp-user-center/src/assets/homeIcon/public_leads@2x.png
  30. 二進制
      ui/sp-user-center/src/assets/homeIcon/public_sea_customers@2x.png
  31. 二進制
      ui/sp-user-center/src/assets/homeIcon/social_security_payment_records@2x.png
  32. 二進制
      ui/sp-user-center/src/assets/homeIcon/tasks@2x.png
  33. 二進制
      ui/sp-user-center/src/assets/homeIcon/test@2x.png
  34. 207 0
      ui/sp-user-center/src/components/HomeIconSelect/index.vue
  35. 3 51
      ui/sp-user-center/src/permission.ts
  36. 13 2
      ui/sp-user-center/src/router/index.ts
  37. 323 0
      ui/sp-user-center/src/views/iconEdit.vue

+ 1 - 2
.idea/compiler.xml

@@ -16,7 +16,6 @@
         <module name="storlead-web" />
         <module name="storlead-user" />
         <module name="storlead-wx" />
-        <module name="storlead-dependencies" />
         <module name="storlead-mybatis" />
         <module name="storlead-sms" />
       </profile>
@@ -24,7 +23,7 @@
     <bytecodeTargetLevel>
       <module name="storlead-ai-api" target="17" />
       <module name="storlead-centre-wx" target="17" />
-      <module name="storlead-dependencies" target="1.5" />
+      <module name="storlead-dependencies" target="1.8" />
       <module name="storlead-platform" target="17" />
       <module name="storlead-util" target="17" />
     </bytecodeTargetLevel>

+ 0 - 6
.idea/encodings.xml

@@ -1,15 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="Encoding">
-    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
-    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/storlead-centre-api/src/main/java" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/storlead-centre-api/src/main/resources" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/storlead-centre-service/src/main/java" charset="UTF-8" />
-    <file url="file://$PROJECT_DIR$/storlead-dependencies/src/main/java" charset="UTF-8" />
-    <file url="file://$PROJECT_DIR$/storlead-dependencies/src/main/resources" charset="UTF-8" />
-    <file url="file://$PROJECT_DIR$/storlead-framework/src/main/java" charset="UTF-8" />
-    <file url="file://$PROJECT_DIR$/storlead-framework/src/main/resources" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/storlead-framework/storlead-auth/src/main/java" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/storlead-framework/storlead-common/src/main/java" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/storlead-framework/storlead-core/src/main/java" charset="UTF-8" />

+ 2 - 2
ui/sp-user-center/.env.development

@@ -10,7 +10,7 @@ VITE_APP_BASE_API = '/api'
 VITE_APP_CONTEXT_PATH = '/'
 
 
-VITE_APP_PORT = 8000
+VITE_APP_PORT = 8688
 
 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
 VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCOlrKBglhJkU/bfgEEv7zuo4y5RiScg3XeDTBrHHePoixDOvyvBsiAc8b33bjin9j77EzRusJT2jjJV1hnHIbu5HkMd3T5JK00gMviCiSbfCkS4/tf3CnFMWLn31YcBgdCOFoiKh/JpGEvTx192LWSKNupDkR6bgpcRvWr5Yw2swIDAQAB'
@@ -27,4 +27,4 @@ VITE_APP_BASE_URL = 'http://localhost:8000/'
 
 VITE_APP_PAGESIZE = 10
 
-VITE_APP_WS_HOST = 'ws://127.0.0.1:18090/sales/chatWebSocket'
+VITE_APP_WS_HOST = 'ws://127.0.0.1:18090/sales/chatWebSocket'

+ 1 - 6
ui/sp-user-center/index.html

@@ -221,12 +221,7 @@
 <!--        <div class="load_title">正在加载系统资源,请耐心等待</div>-->
 <!--      </div>-->
     </div>
-    <script type="module">
-      var script = document.createElement('script');
-      script.type = 'text/javascript';
-      script.src = import.meta.env.VITE_APP_BASE_URL + 'ckeditor/ckeditor.js'; // 这里替换为你的动态路径
-      document.head.appendChild(script);
-    </script>
+
     <script type="module" src="/src/main.ts"></script>
     <script type="module" src="//at.alicdn.com/t/c/font_4709813_4b5jng2vnfy.js"></script>
   </body>

+ 14 - 0
ui/sp-user-center/src/api/app.ts

@@ -17,3 +17,17 @@ export function jumpToPage(pageId: number): AxiosPromise<any> {
     params: { pageId }
   });
 }
+
+// Update Icon
+export interface UpdateIconParams {
+  id: number;
+  icon: string;
+}
+
+export function updateIcon(params: UpdateIconParams): AxiosPromise<any> {
+  return request({
+    url: '/app-centre/manage/update/icon',
+    method: 'post',
+    data: params
+  });
+}

二進制
ui/sp-user-center/src/assets/homeIcon/all_customers@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/all_tasks@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/at_my_tasks@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/briefing_draft@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/command_center@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/contacts@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/data_center@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/follow_up_records@2x.png


+ 77 - 0
ui/sp-user-center/src/assets/homeIcon/iconUtils.ts

@@ -0,0 +1,77 @@
+/**
+ * 获取所有 homeIcon 图标
+ */
+export async function getHomeIcons(): Promise<Array<{ name: string; url: string }>> {
+  const icons: Array<{ name: string; url: string }> = [];
+  
+  // 使用 import.meta.glob 动态导入所有图标
+  const modules = import.meta.glob('./*.png', { eager: true });
+  
+  for (const path in modules) {
+    const module = modules[path] as { default: string };
+    const fileName = path.split('/').pop()?.replace('.png', '') || '';
+    // 排除 IconSelect 组件文件,只处理图标文件
+    if (fileName && fileName !== 'IconSelect' && fileName.includes('@2x')) {
+      // 移除 @2x 后缀作为图标名称
+      const iconName = fileName.replace('@2x', '');
+      icons.push({
+        name: iconName,
+        url: module.default
+      });
+    }
+  }
+  
+  return icons.sort((a, b) => a.name.localeCompare(b.name));
+}
+
+/**
+ * 获取图标的显示名称(将英文名称转换为中文)
+ */
+export function getIconDisplayName(iconName: string): string {
+  const nameMap: Record<string, string> = {
+    'my_goals': '我的目标',
+    'my_tasks': '我的任务',
+    'at_my_tasks': '@我的任务',
+    'my_briefing': '我的简报',
+    'briefing_draft': '简报草稿',
+    'personal_performance': '个人绩效',
+    'my_leads': '我的线索',
+    'public_leads': '公共线索',
+    'all_customers': '全部客户',
+    'public_sea_customers': '公海客户',
+    'key_customers': '重点客户',
+    'follow_up_records': '跟进记录',
+    'contacts': '联系人',
+    'my_opportunities': '我的商机',
+    'my_orders': '我的订单',
+    'all_tasks': '全部任务',
+    'payslip': '工资条',
+    'monthly_attendance_report': '月考勤报表',
+    'leave_balance': '假期余额',
+    'social_security_payment_records': '社保缴纳记录',
+    'provident_fund_payment_records': '公积金缴纳记录',
+    'command_center': '指挥中心',
+    'data_center': '数据中心',
+    'tasks': '任务',
+    'test': '测试',
+    'my_projects': '我的项目',
+    'procurement_management': '采购管理'
+  };
+  
+  return nameMap[iconName] || iconName;
+}
+
+/**
+ * 根据图标名称获取图标路径
+ */
+export function getIconPath(iconName: string): string {
+  // 如果已经包含 @2x,直接使用,否则添加
+  const fileName = iconName.includes('@2x') ? iconName : `${iconName}@2x.png`;
+  try {
+    return new URL(`./${fileName}`, import.meta.url).href;
+  } catch (error) {
+    // 如果动态导入失败,使用静态路径
+    return `/src/assets/homeIcon/${fileName}`;
+  }
+}
+

二進制
ui/sp-user-center/src/assets/homeIcon/key_customers@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/leave_balance@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/monthly_attendance_report@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/my_briefing@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/my_goals@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/my_leads@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/my_opportunities@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/my_orders@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/my_projects@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/my_tasks@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/payslip@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/personal_performance@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/procurement_management@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/provident_fund_payment_records@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/public_leads@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/public_sea_customers@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/social_security_payment_records@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/tasks@2x.png


二進制
ui/sp-user-center/src/assets/homeIcon/test@2x.png


+ 207 - 0
ui/sp-user-center/src/components/HomeIconSelect/index.vue

@@ -0,0 +1,207 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="选择图标"
+    width="800px"
+    :before-close="handleClose"
+  >
+    <div class="icon-select-container">
+      <el-input
+        v-model="filterValue"
+        placeholder="搜索图标"
+        clearable
+        class="search-input"
+        @input="filterIcons"
+      >
+        <template #prefix>
+          <el-icon><Search /></el-icon>
+        </template>
+      </el-input>
+
+      <el-scrollbar height="400px">
+        <div class="icon-grid">
+          <div
+            v-for="icon in filteredIcons"
+            :key="icon.name"
+            :class="['icon-item', { active: selectedIcon === icon.name }]"
+            @click="selectIcon(icon)"
+          >
+            <div class="icon-wrapper">
+              <img :src="icon.url" :alt="icon.name" />
+            </div>
+            <div class="icon-name">{{ getIconDisplayName(icon.name) }}</div>
+          </div>
+        </div>
+      </el-scrollbar>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleConfirm">确定</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Search } from '@element-plus/icons-vue';
+import { getHomeIcons, getIconDisplayName } from '@/assets/homeIcon/iconUtils';
+
+interface IconItem {
+  name: string;
+  url: string;
+}
+
+const props = defineProps<{
+  modelValue: boolean;
+  currentIcon?: string;
+}>();
+
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean];
+  'confirm': [iconName: string];
+}>();
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const filterValue = ref('');
+const selectedIcon = ref<string>(props.currentIcon || '');
+const allIcons = ref<IconItem[]>([]);
+
+// 获取所有图标
+const loadIcons = async () => {
+  allIcons.value = await getHomeIcons();
+  if (props.currentIcon) {
+    selectedIcon.value = props.currentIcon;
+  }
+};
+
+// 过滤图标
+const filteredIcons = computed(() => {
+  if (!filterValue.value) {
+    return allIcons.value;
+  }
+  const keyword = filterValue.value.toLowerCase();
+  return allIcons.value.filter(icon => {
+    const displayName = getIconDisplayName(icon.name).toLowerCase();
+    return displayName.includes(keyword) || icon.name.toLowerCase().includes(keyword);
+  });
+});
+
+// 选择图标
+const selectIcon = (icon: IconItem) => {
+  selectedIcon.value = icon.name;
+};
+
+// 过滤图标
+const filterIcons = () => {
+  // 通过 computed 自动处理
+};
+
+// 确认选择
+const handleConfirm = () => {
+  if (selectedIcon.value) {
+    emit('confirm', selectedIcon.value);
+    visible.value = false;
+  } else {
+    ElMessage.warning('请选择一个图标');
+  }
+};
+
+// 关闭对话框
+const handleClose = () => {
+  visible.value = false;
+};
+
+// 监听 visible 变化,打开时加载图标
+watch(visible, (newVal) => {
+  if (newVal) {
+    loadIcons();
+    selectedIcon.value = props.currentIcon || '';
+    filterValue.value = '';
+  }
+});
+
+// 监听 currentIcon 变化
+watch(() => props.currentIcon, (newVal) => {
+  if (newVal) {
+    selectedIcon.value = newVal;
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.icon-select-container {
+  .search-input {
+    margin-bottom: 16px;
+  }
+
+  .icon-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+    gap: 16px;
+    padding: 8px;
+
+    .icon-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      padding: 12px;
+      border: 2px solid #e4e7ed;
+      border-radius: 8px;
+      cursor: pointer;
+      transition: all 0.3s ease;
+      background: #fff;
+
+      &:hover {
+        border-color: #409eff;
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
+      }
+
+      &.active {
+        border-color: #409eff;
+        background: #ecf5ff;
+      }
+
+      .icon-wrapper {
+        width: 64px;
+        height: 64px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-bottom: 8px;
+        border-radius: 8px;
+        background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+
+        img {
+          width: 48px;
+          height: 48px;
+          object-fit: contain;
+        }
+      }
+
+      .icon-name {
+        font-size: 12px;
+        color: #606266;
+        text-align: center;
+        word-break: break-all;
+        line-height: 1.4;
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>
+

+ 3 - 51
ui/sp-user-center/src/permission.ts

@@ -13,11 +13,10 @@ import {LoginData} from "@/api/types";
 NProgress.configure({ showSpinner: false });
 import { setToken } from '@/utils/auth';
 import { ElMessage } from 'element-plus';
-const whiteList = ['/login', '/register', '/social-callback', '/index'];
+const whiteList = ['/login', '/register', '/social-callback'];
 
 router.beforeEach(async (to, from, next) => {
     NProgress.start();
-
     // https://open.weixin.qq.com/connect/oauth2/authorize?appid=ww5323bd8ab4394132&redirect_uri=https%3A%2F%2Fsales.test.storlead.com%2Findex&response_type=code&scope=snsapi_base&agentid=1000033&state=fromQYwx#wechat_redirect
     if(to.query.code && to.query.state === 'fromQYwx') {
         const userStore = useUserStore();
@@ -43,22 +42,6 @@ router.beforeEach(async (to, from, next) => {
         return
     }
 
-    if (to.query.token && to.query.state === 'fromQYwx'){
-        debugger
-        let redirect = to.query.redirect as string
-        let token  = to.query.token as string;
-        setToken(token);
-
-        //获取用户信息
-        const [err] = await tos(useUserStore().getInfo());
-        // if (err) {
-        //     await useUserStore().logout();
-        //     ElMessage.error(err);
-        //     next({path: '/login'});
-        // } else {
-        //     next(redirect);
-        // }
-    }
 
 
 
@@ -67,43 +50,12 @@ router.beforeEach(async (to, from, next) => {
         to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
         /* has token*/
         if (to.path === '/login') {
-            next({path: '/'});
+            next({path: '/index'});
             NProgress.done();
         } else if (whiteList.indexOf(to.path) !== -1) {
             next()
         } else {
-            if (useUserStore().roles.length === 0) {
-                isRelogin.show = true;
-                // 判断当前用户是否已拉取完user_info信息
-                const [err] = await tos(useUserStore().getInfo());
-                if (err) {
-                    await useUserStore().logout();
-                    ElMessage.error(err);
-                    next({path: '/login'});
-                } else {
-                    isRelogin.show = false;
-                    const accessRoutes = await usePermissionStore().generateRoutes();
-                    // 根据roles权限生成可访问的路由表
-                    accessRoutes.forEach((route) => {
-
-                        if (!isHttp(route.path)) {
-                            router.addRoute(route); // 动态添加可访问路由表
-                        }
-                    });
-                    // 邮箱页面不需要循环调未读数接口
-                    if(to.path.indexOf('/email') == -1) {
-                        useNoticeStore().getCount() // 邮箱未读
-                        useNoticeStore().getMessageCount() // 消息未读
-                        setInterval(() => {
-                            useNoticeStore().getCount()
-                            useNoticeStore().getMessageCount()
-                        }, 1000 * 60)
-                    }
-                    next({...to, replace: true}); // hack方法 确保addRoutes已完成
-                }
-            } else {
-                next();
-            }
+           next();
         }
     } else {
         // 没有token

+ 13 - 2
ui/sp-user-center/src/router/index.ts

@@ -27,6 +27,11 @@ import Layout from '@/layout/index.vue';
 
 // 公共路由
 export const constantRoutes: RouteRecordRaw[] = [
+    {
+        path: '/',
+        redirect: '/index'
+    },
+
     {
         path: '/redirect',
         component: Layout,
@@ -39,14 +44,20 @@ export const constantRoutes: RouteRecordRaw[] = [
         ]
     },
 
+    {
+        path: '/login',
+        component: () => import('@/views/login.vue'),
+        hidden: true
+    },
+
     {
         path: '/index',
         component: () => import('@/views/index.vue'),
     },
 
     {
-        path: '/login',
-        component: () => import('@/views/login.vue'),
+        path: '/iconEdit',
+        component: () => import('@/views/iconEdit.vue'),
         hidden: true
     },
 

+ 323 - 0
ui/sp-user-center/src/views/iconEdit.vue

@@ -0,0 +1,323 @@
+<template>
+  <div class="icon-edit-page">
+    <!-- Header -->
+    <div class="header">
+      <div class="logo">
+        <img src="@/assets/images/home/logo@2x.png" alt="STORLEAD" />
+      </div>
+      <div class="user-info">
+        <img src="@/assets/images/home/Group@2x.png" alt="User" class="user-avatar" />
+        <span class="username">祝小清</span>
+      </div>
+    </div>
+
+    <!-- Main Content -->
+    <div class="main-content">
+      <div class="section" v-for="app in appList" :key="app.appId">
+        <h2 class="section-title">{{ app.appName }}</h2>
+        <div class="icon-grid">
+          <div
+            class="icon-item"
+            v-for="page in app.appPages"
+            :key="page.pageId"
+            @click="handleIconClick(page)"
+          >
+            <div class="icon-wrapper">
+              <img
+                :src="getIconPath(page.iconName || getDefaultIcon(page.appName))"
+                :alt="page.appName"
+              />
+              <div class="edit-overlay">
+                <el-icon><Edit /></el-icon>
+                <span>编辑</span>
+              </div>
+            </div>
+            <span class="icon-label">{{ page.appName }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Icon Select Dialog -->
+    <HomeIconSelect
+      v-model="iconSelectVisible"
+      :current-icon="currentIconName"
+      @confirm="handleIconConfirm"
+    />
+  </div>
+</template>
+
+<script setup name="IconEdit" lang="ts">
+import { ref, onMounted } from 'vue';
+import { Edit } from '@element-plus/icons-vue';
+import { ElMessage } from 'element-plus';
+import { getHomeApp, updateIcon } from '@/api/app';
+import HomeIconSelect from '@/components/HomeIconSelect/index.vue';
+import { getIconPath as getIconPathUtil } from '@/assets/homeIcon/iconUtils';
+
+interface AppPage {
+  pageId: number;
+  appName: string;
+  iconName?: string;
+}
+
+interface AppInfo {
+  appId: number;
+  appName: string;
+  appPages: AppPage[];
+}
+
+const appList = ref<AppInfo[]>([]);
+const iconSelectVisible = ref(false);
+const currentEditingPage = ref<AppPage | null>(null);
+const currentIconName = ref<string>('');
+
+// 默认图标映射(中文名称到英文图标名称)
+const defaultIconMap: Record<string, string> = {
+  '我的目标': 'my_goals',
+  '我的任务': 'my_tasks',
+  '@我的任务': 'at_my_tasks',
+  '我的简报': 'my_briefing',
+  '简报草稿': 'briefing_draft',
+  '个人绩效': 'personal_performance',
+  '我的线索': 'my_leads',
+  '公共线索': 'public_leads',
+  '全部客户': 'all_customers',
+  '公海客户': 'public_sea_customers',
+  '重点客户': 'key_customers',
+  '跟进记录': 'follow_up_records',
+  '联系人': 'contacts',
+  '我的商机': 'my_opportunities',
+  '我的订单': 'my_orders',
+  '全部任务': 'all_tasks',
+  '工资条': 'payslip',
+  '月考勤报表': 'monthly_attendance_report',
+  '假期余额': 'leave_balance',
+  '社保缴纳记录': 'social_security_payment_records',
+  '公积金缴纳记录': 'provident_fund_payment_records',
+  '指挥中心': 'command_center',
+  '数据中心': 'data_center',
+  '任务': 'tasks',
+  '测试': 'test',
+  '我的项目': 'my_projects',
+  '采购管理': 'procurement_management'
+};
+
+// 获取默认图标名称
+const getDefaultIcon = (appName: string): string => {
+  return defaultIconMap[appName] || 'my_goals';
+};
+
+// 获取图标路径
+const getIconPath = (iconName: string): string => {
+  return getIconPathUtil(iconName);
+};
+
+// 点击图标
+const handleIconClick = (page: AppPage) => {
+  currentEditingPage.value = page;
+  currentIconName.value = page.iconName || getDefaultIcon(page.appName);
+  iconSelectVisible.value = true;
+};
+
+// 确认选择图标
+const handleIconConfirm = async (iconName: string) => {
+  if (!currentEditingPage.value) return;
+
+  try {
+    await updateIcon({
+      id: currentEditingPage.value.pageId,
+      icon: iconName
+    });
+
+    // 更新本地数据
+    if (currentEditingPage.value) {
+      currentEditingPage.value.iconName = iconName;
+    }
+
+    ElMessage.success('图标更新成功');
+    iconSelectVisible.value = false;
+    currentEditingPage.value = null;
+  } catch (error) {
+    console.error('更新图标失败:', error);
+    ElMessage.error('图标更新失败');
+  }
+};
+
+// 初始化数据
+const initData = async () => {
+  try {
+    const res = await getHomeApp();
+    if (res.result) {
+      appList.value = res.result;
+    }
+  } catch (error) {
+    console.error('Failed to get home app list:', error);
+  }
+};
+
+onMounted(() => {
+  initData();
+});
+</script>
+
+<style scoped lang="scss">
+.icon-edit-page {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #F5F7FA 0%, #FFFFFF 100%);
+  overflow-y: scroll;
+}
+
+.header {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 100;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 40px;
+  background: linear-gradient(90deg, #1E88E5 0%, #42A5F5 100%);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+  .logo {
+    img {
+      height: 32px;
+    }
+  }
+
+  .user-info {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    color: white;
+    cursor: pointer;
+
+    .user-avatar {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      border: 2px solid rgba(255, 255, 255, 0.3);
+    }
+
+    .username {
+      font-size: 14px;
+      font-weight: 500;
+    }
+  }
+}
+
+.main-content {
+  max-width: 1400px;
+  margin: 0 auto;
+  padding: 92px 20px 40px 20px;
+}
+
+.section {
+  margin-bottom: 48px;
+
+  .section-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #333;
+    margin-bottom: 24px;
+    padding-left: 12px;
+    border-left: 4px solid #1E88E5;
+  }
+
+  .icon-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+    gap: 32px 24px;
+    background: white;
+    padding: 32px;
+    border-radius: 12px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    @media (max-width: 768px) {
+      grid-template-columns: repeat(4, 1fr);
+      gap: 24px 16px;
+      padding: 24px;
+    }
+
+    @media (max-width: 480px) {
+      grid-template-columns: repeat(3, 1fr);
+      gap: 20px 12px;
+      padding: 20px;
+    }
+  }
+
+  .icon-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+    cursor: pointer;
+    transition: transform 0.2s ease;
+
+    &:hover {
+      transform: translateY(-4px);
+
+      .icon-wrapper {
+        .edit-overlay {
+          opacity: 1;
+        }
+      }
+    }
+
+    .icon-wrapper {
+      width: 64px;
+      height: 64px;
+      border-radius: 16px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.3s ease;
+      position: relative;
+      overflow: hidden;
+
+      img {
+        width: 64px;
+        height: 64px;
+        object-fit: contain;
+      }
+
+      .edit-overlay {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: rgba(0, 0, 0, 0.6);
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        color: white;
+        opacity: 0;
+        transition: opacity 0.3s ease;
+        border-radius: 16px;
+
+        .el-icon {
+          font-size: 20px;
+          margin-bottom: 4px;
+        }
+
+        span {
+          font-size: 12px;
+        }
+      }
+    }
+
+    .icon-label {
+      font-size: 14px;
+      color: #666;
+      text-align: center;
+      font-weight: 500;
+      white-space: nowrap;
+    }
+  }
+}
+</style>
+