diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4c4ffc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.zip diff --git a/client/src/assets/fonts/iconfont/iconfont.css b/client/src/assets/fonts/iconfont/iconfont.css new file mode 100644 index 0000000..b6e0b35 --- /dev/null +++ b/client/src/assets/fonts/iconfont/iconfont.css @@ -0,0 +1,41 @@ +@font-face { + font-family: "iconfont"; /* Project id 5232815 */ + /* Color fonts */ + src: + url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), + url('iconfont.woff?t=1789098374123') format('woff'), + url('iconfont.ttf?t=1789098374123') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.xt-jihuabaifang:before { + content: "\e6c4"; +} + +.xt-xiaochengxu:before { + content: "\e61e"; +} + +.xt-baifangshenyue:before { + content: "\e6c6"; +} + +.xt-topology:before { + content: "\e67c"; +} + +.xt-xiangmukancha:before { + content: "\e633"; +} + +.xt-xiangmufangan:before { + content: "\e738"; +} + diff --git a/client/src/assets/fonts/iconfont/iconfont.js b/client/src/assets/fonts/iconfont/iconfont.js new file mode 100644 index 0000000..164d377 --- /dev/null +++ b/client/src/assets/fonts/iconfont/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_5232815='',(e=>{var t=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var l,h,a,n,i,o=function(t,c){c.parentNode.insertBefore(t,c)};if(t&&!e.__iconfont__svg__cssinject__){e.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(t){console&&console.log(t)}}l=function(){var t,c=document.createElement("div");c.innerHTML=e._iconfont_svg_string_5232815,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(t=document.body).firstChild?o(c,t.firstChild):t.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),l()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(a=l,n=e.document,i=!1,s(),n.onreadystatechange=function(){"complete"==n.readyState&&(n.onreadystatechange=null,v())})}function v(){i||(i=!0,a())}function s(){try{n.documentElement.doScroll("left")}catch(t){return void setTimeout(s,50)}v()}})(window); \ No newline at end of file diff --git a/client/src/assets/fonts/iconfont/iconfont.json b/client/src/assets/fonts/iconfont/iconfont.json new file mode 100644 index 0000000..82f3ab7 --- /dev/null +++ b/client/src/assets/fonts/iconfont/iconfont.json @@ -0,0 +1,51 @@ +{ + "id": "5232815", + "name": "鑫潼智电后台", + "font_family": "iconfont", + "css_prefix_text": "xt-", + "description": "", + "glyphs": [ + { + "icon_id": "905033", + "name": "客户拜访", + "font_class": "jihuabaifang", + "unicode": "e6c4", + "unicode_decimal": 59076 + }, + { + "icon_id": "4424124", + "name": "小程序", + "font_class": "xiaochengxu", + "unicode": "e61e", + "unicode_decimal": 58910 + }, + { + "icon_id": "4520265", + "name": "拜访审阅", + "font_class": "baifangshenyue", + "unicode": "e6c6", + "unicode_decimal": 59078 + }, + { + "icon_id": "4922685", + "name": "拓扑", + "font_class": "topology", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "11622405", + "name": "项目勘察", + "font_class": "xiangmukancha", + "unicode": "e633", + "unicode_decimal": 58931 + }, + { + "icon_id": "46034577", + "name": "项目方案", + "font_class": "xiangmufangan", + "unicode": "e738", + "unicode_decimal": 59192 + } + ] +} diff --git a/client/src/assets/fonts/iconfont/iconfont.ttf b/client/src/assets/fonts/iconfont/iconfont.ttf new file mode 100644 index 0000000..24d16ad Binary files /dev/null and b/client/src/assets/fonts/iconfont/iconfont.ttf differ diff --git a/client/src/assets/fonts/iconfont/iconfont.woff b/client/src/assets/fonts/iconfont/iconfont.woff new file mode 100644 index 0000000..7c855e7 Binary files /dev/null and b/client/src/assets/fonts/iconfont/iconfont.woff differ diff --git a/client/src/assets/fonts/iconfont/iconfont.woff2 b/client/src/assets/fonts/iconfont/iconfont.woff2 new file mode 100644 index 0000000..9562545 Binary files /dev/null and b/client/src/assets/fonts/iconfont/iconfont.woff2 differ diff --git a/client/src/layouts/index.vue b/client/src/layouts/index.vue index 69299cd..7dd6c7e 100644 --- a/client/src/layouts/index.vue +++ b/client/src/layouts/index.vue @@ -140,7 +140,14 @@ const iconMap: Record = { DashboardOutlined, } +// iconfont 图标类名前缀(对应 assets/fonts/iconfont/iconfont.css 的 css_prefix_text) +const ICONFONT_PREFIX = 'xt-' + function resolveIcon(icon?: string | null) { + // icon 值形如 xt-xxx 时渲染 iconfont 图标,否则回退到 ant-design 图标 + if (icon && icon.startsWith(ICONFONT_PREFIX)) { + return () => h('i', { class: `iconfont ${icon}` }) + } const iconComponent = icon ? iconMap[icon] : undefined return () => h(iconComponent || MenuOutlined) } @@ -194,6 +201,20 @@ const menuItems = computed(() => [ ...buildMenuItems(permissionStore.menus), ]) +// 子菜单路径 → 所属父级目录的菜单 key,用于按当前路由展开对应的父菜单 +// (不能按路径首段推导:如“客户拜访”归属项目管理,但路径仍为 /customer/visit) +const parentKeyMap = computed>(() => { + const map: Record = {} + for (const node of permissionStore.menus) { + if (node.menuType !== 1) continue + const dirKey = node.menuPath || `dir-${node.id}` + for (const child of node.children || []) { + if (child.menuPath) map[child.menuPath] = dirKey + } + } + return map +}) + // 计算属性 const userInfo = computed(() => permissionStore.userInfo) const displayName = computed(() => userInfo.value?.realName || userInfo.value?.username || '管理员') @@ -219,10 +240,10 @@ watch( (path) => { selectedKeys.value = [path] - // 自动展开父级菜单 - const pathParts = path.split('/').filter(Boolean) - if (pathParts.length > 1) { - openKeys.value = [`/${pathParts[0]}`] + // 自动展开当前菜单所属的父级目录 + const parentKey = parentKeyMap.value[path] + if (parentKey) { + openKeys.value = [parentKey] } }, { immediate: true } @@ -234,9 +255,9 @@ watch(collapsed, (val) => { openKeys.value = [] } else { // 展开时根据当前路由自动打开对应的父菜单 - const pathParts = route.path.split('/').filter(Boolean) - if (pathParts.length > 1) { - openKeys.value = [`/${pathParts[0]}`] + const parentKey = parentKeyMap.value[route.path] + if (parentKey) { + openKeys.value = [parentKey] } } }) @@ -306,6 +327,30 @@ function handleUserMenuClick({ key }: { key: string }) { border-right: none; } + // iconfont 图标统一:该图标集为彩色字体,颜色写死在字形里,无法用 color 覆盖, + // 这里用 filter 统一压成单色,并按菜单状态在灰 / 白之间切换,与 ant-design 图标观感保持一致 + // 尺寸统一:ant-design 菜单图标为 14px,而 iconfont.css 自带 font-size: 16px, + // 故此处显式对齐为 14px,并重置 line-height,避免字体图标在 antd 的 line-height: 0 下出现基线偏移 + :deep(.ant-menu) .iconfont { + display: inline-block; + font-size: 14px; + line-height: 1; + vertical-align: -0.125em; + filter: brightness(0) invert(0.65); + } + + // 折叠态与 ant-design 图标同步放大到 16px + :deep(.ant-menu-inline-collapsed) .iconfont { + font-size: 16px; + } + + :deep(.ant-menu-item:hover) .iconfont, + :deep(.ant-menu-item-selected) .iconfont, + :deep(.ant-menu-submenu-title:hover) .iconfont, + :deep(.ant-menu-submenu-selected) .iconfont { + filter: brightness(0) invert(1); + } + .collapse-btn { position: absolute; bottom: 0; diff --git a/client/src/main.ts b/client/src/main.ts index 71bbb09..ae19d96 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from "vue"; import Antd from "ant-design-vue"; import "ant-design-vue/dist/reset.css"; import "@/assets/styles/global.less"; +import "@/assets/fonts/iconfont/iconfont.css"; import App from "./App.vue"; import router from "./router"; import pinia from "./stores"; diff --git a/database/db_seed.sql b/database/db_seed.sql index 70aff4b..c1a19fa 100644 --- a/database/db_seed.sql +++ b/database/db_seed.sql @@ -85,7 +85,7 @@ ON CONFLICT (id) DO UPDATE SET INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES (12, 1, '客户档案管理', '/customer/info', 'customer/info/index', 'UserOutlined', 1, 2, 'customer:list', 1), (13, 1, '客户部门', '/customer/department', 'customer/department/index', 'TeamOutlined', 2, 2, 'customer:department', 1), -(14, 1, '客户小程序授权管理', '/customer/account', 'customer/account/index', 'SafetyOutlined', 3, 2, 'customer:account', 1), +(14, 1, '客户小程序授权管理', '/customer/account', 'customer/account/index', 'xt-xiaochengxu', 3, 2, 'customer:account', 1), (15, 1, '新增客户', NULL, NULL, NULL, 1, 3, 'customer:create', 1), (16, 1, '编辑客户', NULL, NULL, NULL, 2, 3, 'customer:update', 1), (17, 1, '删除客户', NULL, NULL, NULL, 3, 3, 'customer:delete', 1), @@ -106,18 +106,19 @@ ON CONFLICT (id) DO UPDATE SET permission = EXCLUDED.permission, visible = EXCLUDED.visible; --- 初始菜单(客户扩展子菜单 + 项目域子菜单,id:24-31 + 空间拓扑 32) --- 客户域:客户拜访(customer:visit) / 空间拓扑与用电架构(customer:topology) --- 项目域:项目勘察(project:survey) / 项目方案(project:plan) / 合同管理(project:contract) --- 项目验收(project:acceptance) / 回访记录(project:visit) +-- 初始菜单(项目域子菜单 + 客户扩展子菜单,id:24-31 + 空间拓扑 32) +-- 项目域:客户拜访(customer:visit) / 项目勘察(project:survey) / 项目方案(project:plan) +-- 合同管理(project:contract) / 项目验收(project:acceptance) / 回访记录(project:visit) +-- 客户域:空间拓扑与用电架构(customer:topology) +-- 注:客户拜访归属“项目管理”,但仍沿用 /customer/visit 路径与 customer:visit 权限码 INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES -(24, 1, '客户拜访', '/customer/visit', 'customer/visit/index', 'AccountBookOutlined', 4, 2, 'customer:visit', 1), -(27, 2, '项目勘察', '/project/survey', 'project/survey/index', 'CarryOutOutlined', 1, 2, 'project:survey', 1), -(28, 2, '项目方案', '/project/plan', 'project/plan/index', 'FileSearchOutlined', 2, 2, 'project:plan', 1), -(29, 2, '合同管理', '/project/contract', 'project/contract/index', 'FileDoneOutlined', 3, 2, 'project:contract', 1), -(30, 2, '项目验收', '/project/acceptance', 'project/acceptance/index', 'ThunderboltOutlined', 4, 2, 'project:acceptance',1), -(31, 2, '回访记录', '/project/visit', 'project/visit/index', 'AuditOutlined', 5, 2, 'project:visit', 1), -(32, 1, '空间拓扑与用电架构', '/customer/topology', 'customer/topology/index', 'ApartmentOutlined', 5, 2, 'customer:topology',1) +(24, 2, '客户拜访', '/customer/visit', 'customer/visit/index', 'xt-jihuabaifang', 1, 2, 'customer:visit', 1), +(27, 2, '项目勘察', '/project/survey', 'project/survey/index', 'xt-xiangmukancha', 2, 2, 'project:survey', 1), +(28, 2, '项目方案', '/project/plan', 'project/plan/index', 'xt-xiangmufangan', 3, 2, 'project:plan', 1), +(29, 2, '合同管理', '/project/contract', 'project/contract/index', 'FileDoneOutlined', 4, 2, 'project:contract', 1), +(30, 2, '项目验收', '/project/acceptance', 'project/acceptance/index', 'ThunderboltOutlined', 5, 2, 'project:acceptance',1), +(31, 2, '回访记录', '/project/visit', 'project/visit/index', 'AuditOutlined', 6, 2, 'project:visit', 1), +(32, 1, '空间拓扑与用电架构', '/customer/topology', 'customer/topology/index', 'xt-topology', 5, 2, 'customer:topology',1) ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, menu_name = EXCLUDED.menu_name, diff --git a/database/migrations/20260911_menu_iconfont.sql b/database/migrations/20260911_menu_iconfont.sql new file mode 100644 index 0000000..cc2dcdf --- /dev/null +++ b/database/migrations/20260911_menu_iconfont.sql @@ -0,0 +1,31 @@ +-- ============================================================ +-- 增量脚本:导航菜单图标切换为 iconfont 自定义图标 +-- 日期:2026-09-11 +-- 数据库:PostgreSQL 15+ (xintong) +-- 说明:将 5 个业务菜单的 sys_menu.icon 由 ant-design 图标名 +-- 改为前端 iconfont 类名(xt- 前缀),前端据此渲染字体图标。 +-- 图标源自 client/src/assets/fonts/iconfont(css_prefix_text = xt-)。 +-- 幂等脚本,可重复执行。 +-- +-- 用法: +-- psql -U postgres -d xintong -f 20260911_menu_iconfont.sql +-- ============================================================ + +BEGIN; + +UPDATE sys_menu SET icon = 'xt-xiaochengxu' WHERE menu_path = '/customer/account'; -- 客户小程序授权管理 +UPDATE sys_menu SET icon = 'xt-jihuabaifang' WHERE menu_path = '/customer/visit'; -- 客户拜访 +UPDATE sys_menu SET icon = 'xt-topology' WHERE menu_path = '/customer/topology'; -- 空间拓扑与用电架构 +UPDATE sys_menu SET icon = 'xt-xiangmukancha' WHERE menu_path = '/project/survey'; -- 项目勘察 +UPDATE sys_menu SET icon = 'xt-xiangmufangan' WHERE menu_path = '/project/plan'; -- 项目方案 + +COMMIT; + +-- ============================================================ +-- 验证查询(可选) +-- ============================================================ +SELECT id, menu_name, menu_path, icon +FROM sys_menu +WHERE menu_path IN ('/customer/account', '/customer/visit', '/customer/topology', + '/project/survey', '/project/plan') +ORDER BY id; diff --git a/database/migrations/20260911_move_customer_visit_to_project.sql b/database/migrations/20260911_move_customer_visit_to_project.sql new file mode 100644 index 0000000..78001bb --- /dev/null +++ b/database/migrations/20260911_move_customer_visit_to_project.sql @@ -0,0 +1,16 @@ +-- 把“客户拜访”从“客户管理”移动到“项目管理”,并调整为项目管理下的第 1 个菜单 +-- 说明:仅调整归属(parent_id)与排序(sort_order), +-- menu_path(/customer/visit)、component(customer/visit/index)、permission(customer:visit) 保持不变 +BEGIN; + +-- 客户拜访改挂到项目管理(parent_id=2),并排在项目勘察之前 +UPDATE sys_menu SET parent_id = 2, sort_order = 1 WHERE menu_path = '/customer/visit'; + +-- 项目管理下原有子菜单顺序后移,保持连续 +UPDATE sys_menu SET sort_order = 2 WHERE menu_path = '/project/survey'; -- 项目勘察 +UPDATE sys_menu SET sort_order = 3 WHERE menu_path = '/project/plan'; -- 项目方案 +UPDATE sys_menu SET sort_order = 4 WHERE menu_path = '/project/contract'; -- 合同管理 +UPDATE sys_menu SET sort_order = 5 WHERE menu_path = '/project/acceptance'; -- 项目验收 +UPDATE sys_menu SET sort_order = 6 WHERE menu_path = '/project/visit'; -- 回访记录 + +COMMIT;