diff --git a/nginx-docker.conf b/nginx-docker.conf index c0db43c..0c2eb49 100644 --- a/nginx-docker.conf +++ b/nginx-docker.conf @@ -35,6 +35,7 @@ server { location ^~ /wechat/register { try_files /index.html =200; } location ^~ /wechat/bind { try_files /index.html =200; } + location ^~ /track/query { try_files /index.html =200; } location ^~ /operation/inboundScan { try_files /index.html =200; } location ^~ /operation/outboundScan { try_files /index.html =200; } location ^~ /operation/printLabel { try_files /index.html =200; } diff --git a/src/api/h5/publicTrack.ts b/src/api/h5/publicTrack.ts new file mode 100644 index 0000000..3d23426 --- /dev/null +++ b/src/api/h5/publicTrack.ts @@ -0,0 +1,9 @@ +import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder'; + +/** H5 公开轨迹查询(按单号) */ +export async function queryPublicTrackTimeline(orderNumber: string) { + const no = orderNumber.trim(); + if (!no) return []; + const list = await postGetDetailTimeline({ orderNumber: no }); + return (list || []) as API.GetDetailTimelineOutput[]; +} diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 579e9f7..ed00290 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -1,6 +1,7 @@ import localeMessageBox from '@/components/message-box/locale/en-US'; import localeLogin from '@/views/login/locale/en-US'; import localeWechat from '@/views/wechat/locale/en-US'; +import localeH5TrackQuery from '@/views/h5/track-query/locale/en-US'; import localeWorkplace from '@/views/home/workplace/locale/en-US'; @@ -1060,6 +1061,7 @@ export default { 'system.role.tag.creating': 'Creating', 'system.role.tag.boundWarehouse': 'Warehouse bound', 'system.role.message.deleteSuccess': 'Delete successful', + 'system.role.message.cannotDeleteSystemRole': 'Built-in system roles (e.g. Customer) cannot be deleted', 'system.role.message.updateSuccess': 'Update successful', 'system.role.message.createSuccess': 'Create successful', @@ -2958,6 +2960,7 @@ export default { ...localeMessageBox, ...localeLogin, ...localeWechat, + ...localeH5TrackQuery, ...localeWorkplace, ...localeMonitor, diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 4e9346c..b3783b7 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -1,6 +1,7 @@ import localeMessageBox from '@/components/message-box/locale/zh-CN'; import localeLogin from '@/views/login/locale/zh-CN'; import localeWechat from '@/views/wechat/locale/zh-CN'; +import localeH5TrackQuery from '@/views/h5/track-query/locale/zh-CN'; import localeWorkplace from '@/views/home/workplace/locale/zh-CN'; @@ -946,6 +947,7 @@ export default { 'system.role.tag.creating': '新建中', 'system.role.tag.boundWarehouse': '已绑仓库', 'system.role.message.deleteSuccess': '删除成功', + 'system.role.message.cannotDeleteSystemRole': '系统内置角色(如客户)不可删除', 'system.role.message.updateSuccess': '更新成功', 'system.role.message.createSuccess': '创建成功', @@ -2995,6 +2997,7 @@ export default { ...localeMessageBox, ...localeLogin, ...localeWechat, + ...localeH5TrackQuery, ...localeWorkplace, ...localeMonitor, diff --git a/src/router/index.ts b/src/router/index.ts index aa54540..6049181 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -51,6 +51,15 @@ const router = createRouter({ hideInMenu: true, }, }, + { + path: '/track/query', + name: 'H5TrackQuery', + component: () => import('@/views/h5/track-query/index.vue'), + meta: { + requiresAuth: false, + hideInMenu: true, + }, + }, { path: '/wechat/register', name: 'WechatRegister', diff --git a/src/utils/rbacPermissionAssignRows.ts b/src/utils/rbacPermissionAssignRows.ts index 7d420af..ae714ad 100644 --- a/src/utils/rbacPermissionAssignRows.ts +++ b/src/utils/rbacPermissionAssignRows.ts @@ -111,3 +111,149 @@ export function collectPermissionCodesFromRows(rows: PermissionAssignRow[]): str }) return Array.from(codes) } + +export function getAssignablePermissionCodeSet(rows: PermissionAssignRow[]): Set { + return new Set(collectPermissionCodesFromRows(rows)) +} + +/** 仅统计分配树内的已选数量,并补全上级勾选 */ +export function normalizeSelectedPermissions( + selected: readonly string[], + rows: PermissionAssignRow[] +): string[] { + const assignable = getAssignablePermissionCodeSet(rows) + if (assignable.size === 0) return [] + + const context = buildPermissionCascadeContext(rows) + const next = new Set() + + selected.forEach((code) => { + if (!assignable.has(code)) return + next.add(code) + for (const ancestor of context.ancestors.get(code) ?? []) { + if (assignable.has(ancestor)) { + next.add(ancestor) + } + } + }) + + return Array.from(next) +} + +export function countSelectedAssignablePermissions( + selected: readonly string[], + rows: PermissionAssignRow[] +): number { + return normalizeSelectedPermissions(selected, rows).length +} + +export interface PermissionCascadeContext { + /** 权限 code -> 上级 code 列表(由近到远:二级、一级) */ + ancestors: Map + /** 一级菜单 code -> 其下全部权限 code(含自身、二级、按钮) */ + descendantsByLevel1: Map + /** 二级菜单 code -> 其下全部权限 code(含自身、按钮) */ + descendantsByLevel2: Map +} + +export function buildPermissionCascadeContext( + rows: PermissionAssignRow[] +): PermissionCascadeContext { + const ancestors = new Map() + const descendantsByLevel1 = new Map>() + const descendantsByLevel2 = new Map>() + + const ensureLevel1 = (code: string) => { + if (!descendantsByLevel1.has(code)) { + descendantsByLevel1.set(code, new Set()) + } + return descendantsByLevel1.get(code)! + } + + const ensureLevel2 = (code: string) => { + if (!descendantsByLevel2.has(code)) { + descendantsByLevel2.set(code, new Set()) + } + return descendantsByLevel2.get(code)! + } + + const addAncestors = (code: string, ancestorCodes: string[]) => { + if (!code || ancestorCodes.length === 0) return + const prev = ancestors.get(code) ?? [] + ancestors.set(code, Array.from(new Set([...prev, ...ancestorCodes]))) + } + + rows.forEach((row) => { + const { level1Code, level2Code } = row + + if (level1Code) { + ensureLevel1(level1Code).add(level1Code) + ancestors.set(level1Code, []) + } + + if (level2Code) { + ensureLevel2(level2Code).add(level2Code) + addAncestors(level2Code, level1Code ? [level1Code] : []) + if (level1Code) { + ensureLevel1(level1Code).add(level2Code) + } + } + + row.buttons.forEach((btn) => { + if (!btn.code) return + addAncestors(btn.code, [level2Code, level1Code].filter(Boolean) as string[]) + if (level2Code) { + ensureLevel2(level2Code).add(btn.code) + } + if (level1Code) { + ensureLevel1(level1Code).add(btn.code) + } + }) + }) + + const toArrayMap = (source: Map>) => + new Map(Array.from(source.entries(), ([key, value]) => [key, Array.from(value)])) + + return { + ancestors, + descendantsByLevel1: toArrayMap(descendantsByLevel1), + descendantsByLevel2: toArrayMap(descendantsByLevel2), + } +} + +/** 勾选子级时联动勾选上级;取消一级时联动取消其下全部权限 */ +export function togglePermissionWithCascade( + selected: readonly string[], + context: PermissionCascadeContext, + code: string, + enabled: boolean +): string[] { + if (!code) return [...selected] + + const next = new Set(selected) + + if (enabled) { + next.add(code) + for (const ancestor of context.ancestors.get(code) ?? []) { + next.add(ancestor) + } + return Array.from(next) + } + + if (context.descendantsByLevel1.has(code)) { + for (const descendant of context.descendantsByLevel1.get(code)!) { + next.delete(descendant) + } + return Array.from(next) + } + + if (context.descendantsByLevel2.has(code)) { + for (const descendant of context.descendantsByLevel2.get(code)!) { + next.delete(descendant) + } + return Array.from(next) + } + + next.delete(code) + return Array.from(next) +} diff --git a/src/views/h5/track-query/index.vue b/src/views/h5/track-query/index.vue new file mode 100644 index 0000000..58536aa --- /dev/null +++ b/src/views/h5/track-query/index.vue @@ -0,0 +1,315 @@ + + + + + diff --git a/src/views/h5/track-query/locale/en-US.ts b/src/views/h5/track-query/locale/en-US.ts new file mode 100644 index 0000000..f9dcecd --- /dev/null +++ b/src/views/h5/track-query/locale/en-US.ts @@ -0,0 +1,8 @@ +export default { + 'h5.trackQuery.title': 'Track Shipment', + 'h5.trackQuery.desc': 'Enter your tracking number to view the latest updates', + 'h5.trackQuery.placeholder': 'Enter tracking number', + 'h5.trackQuery.search': 'Search', + 'h5.trackQuery.empty': 'No tracking info found. Please check the number.', + 'h5.trackQuery.orderNo': 'Tracking No.', +}; diff --git a/src/views/h5/track-query/locale/zh-CN.ts b/src/views/h5/track-query/locale/zh-CN.ts new file mode 100644 index 0000000..609efb6 --- /dev/null +++ b/src/views/h5/track-query/locale/zh-CN.ts @@ -0,0 +1,8 @@ +export default { + 'h5.trackQuery.title': '物流轨迹查询', + 'h5.trackQuery.desc': '输入运单号,查看包裹最新物流动态', + 'h5.trackQuery.placeholder': '请输入运单号', + 'h5.trackQuery.search': '查询', + 'h5.trackQuery.empty': '暂无轨迹信息,请确认单号是否正确', + 'h5.trackQuery.orderNo': '运单号', +}; diff --git a/src/views/login/components/LoginHero.vue b/src/views/login/components/LoginHero.vue index edb0900..d5fba37 100644 --- a/src/views/login/components/LoginHero.vue +++ b/src/views/login/components/LoginHero.vue @@ -31,6 +31,7 @@ @@ -160,6 +163,8 @@ function onTrackSubmit() { width: 100%; max-width: 420px; height: auto; + margin-top: 200px; + margin-left: -200px; transform-origin: 70% 50%; animation: loginHeroPlane3D 3.6s linear 1 forwards; will-change: transform, opacity; diff --git a/src/views/system/permission/role/index.vue b/src/views/system/permission/role/index.vue index d75cc2e..85993e6 100644 --- a/src/views/system/permission/role/index.vue +++ b/src/views/system/permission/role/index.vue @@ -48,7 +48,7 @@