From 3a9dbbb60b710955b4bccbf53dff27820b63022a Mon Sep 17 00:00:00 2001 From: Timi Date: Wed, 23 Sep 2026 21:54:19 +0800 Subject: [PATCH] add point chart --- src/api/GaoPointChartAPI.ts | 26 ++ src/api/GaoRoleAPI.ts | 11 + .../fulfillment/OrderFulfillmentList.vue | 1 - src/pages/permission/PermissionSettings.vue | 31 +-- .../RoleDelegationPermissionAssign.vue | 2 +- src/pages/permission/RoleEdit.vue | 42 ++-- src/pages/permission/RoleList.vue | 37 +-- src/pages/permission/RolePermissionAssign.vue | 2 +- src/pages/product/ProductStockRecordList.vue | 1 + src/pages/store/StoreDataCharts.vue | 21 +- .../component/StorePointDailyTrendChart.vue | 225 ++++++++++++++++++ .../StorePointRedemptionRankChart.vue | 95 ++++++++ src/store/permissionSettingsStore.ts | 41 +++- src/types/GaoChart.ts | 20 ++ 14 files changed, 462 insertions(+), 93 deletions(-) create mode 100644 src/api/GaoPointChartAPI.ts create mode 100644 src/api/GaoRoleAPI.ts create mode 100644 src/pages/store/component/StorePointDailyTrendChart.vue create mode 100644 src/pages/store/component/StorePointRedemptionRankChart.vue diff --git a/src/api/GaoPointChartAPI.ts b/src/api/GaoPointChartAPI.ts new file mode 100644 index 0000000..1f3437d --- /dev/null +++ b/src/api/GaoPointChartAPI.ts @@ -0,0 +1,26 @@ +import { axios } from "timi-web-api"; +import type { GaoPointChartReq, GaoPointDailyStatView, GaoPointRedemptionProductRankView } from "@/types/GaoChart"; +import { getStoreChartCache } from "@/utils/storeChartCache"; +import { withCurrentStoreId } from "@/utils/store"; + +const BASE_URI = "/gao/store/chart"; + +/** 查询按日汇总的获得积分、消耗积分及两者差值。 */ +async function dailyTrend(req: GaoPointChartReq): Promise { + return await requestList("point-daily-trend", "/point/ledger/daily", req); +} + +/** 查询兑换积分最高的前 10 个产品,不扣除退回积分。 */ +async function redemptionProductRank(req: GaoPointChartReq): Promise { + return await requestList("point-redemption-product-rank", "/point/redemption/product/rank", req); +} + +async function requestList(name: string, path: string, req: GaoPointChartReq): Promise { + const request = withCurrentStoreId(req); + return await getStoreChartCache(`gao-point:${name}:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}${path}`, request)); +} + +export default { + dailyTrend, + redemptionProductRank +}; diff --git a/src/api/GaoRoleAPI.ts b/src/api/GaoRoleAPI.ts new file mode 100644 index 0000000..c3eba6a --- /dev/null +++ b/src/api/GaoRoleAPI.ts @@ -0,0 +1,11 @@ +import { axios } from "timi-web-api"; +import type { Role } from "timi-web-api"; + +const BASE_URI = "/gao/role"; + +export const GaoRoleAPI = { + /** 查询登录用户所属门店内可管理的角色。 */ + async managementList(): Promise { + return await axios.post(`${BASE_URI}/management/list`); + } +}; diff --git a/src/pages/fulfillment/OrderFulfillmentList.vue b/src/pages/fulfillment/OrderFulfillmentList.vue index 4f51e28..fb1b85c 100644 --- a/src/pages/fulfillment/OrderFulfillmentList.vue +++ b/src/pages/fulfillment/OrderFulfillmentList.vue @@ -554,7 +554,6 @@ onUnmounted(() => navBarStore.clearRight()); .list-wrap { gap: 1rem; display: flex; - padding-top: 1rem; flex-direction: column; .date-summary { diff --git a/src/pages/permission/PermissionSettings.vue b/src/pages/permission/PermissionSettings.vue index 63ad697..3a631e1 100644 --- a/src/pages/permission/PermissionSettings.vue +++ b/src/pages/permission/PermissionSettings.vue @@ -92,12 +92,10 @@ const router = useRouter(); const userStore = useUserStore(); const permissionStore = usePermissionSettingsStore(); const authorizationRoleList = ref([]); -const directRoleIdSet = ref(new Set()); -const manageableRoleIdSet = ref(new Set()); const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin)); const roleCount = computed(() => authorizationRoleList.value.length); const permissionCount = computed(() => permissionStore.permissionList.length); -type RoleTreeItem = Pick & { level: number }; +type RoleTreeItem = Pick & { level: number }; const visibleRoleTree = computed(() => { if (isSystemAdmin.value) { @@ -147,36 +145,15 @@ function canAssignRole(role: Pick): boolean { return isSystemAdmin.value || role.protectedRole !== true; } -function getRoleScopeLabel(role: Pick): string { - const direct = directRoleIdSet.value.has(role.id); - const manageable = manageableRoleIdSet.value.has(role.id); - if (direct && manageable) { - return "直属且可管理"; - } - return direct ? "直属角色" : "可管理角色"; +function getRoleScopeLabel(role: Pick): string { + return isSystemAdmin.value ? "系统角色" : role.ownerType === "STORE" ? "本店角色" : "可管理角色"; } // ---------- 页面数据 ---------- onMounted(async () => { - if (isSystemAdmin.value) { - await permissionStore.loadAll(); - authorizationRoleList.value = permissionStore.roleList; - return; - } - await permissionStore.loadAll(); - const [manageableRoleList, currentRoleList] = await Promise.all([ - permissionStore.loadManageableRoles(), - permissionStore.loadCurrentRoleDetails() - ]); - const roleMap = new Map(); - for (const role of [...currentRoleList, ...manageableRoleList]) { - roleMap.set(role.id, role); - } - authorizationRoleList.value = Array.from(roleMap.values()); - directRoleIdSet.value = new Set(currentRoleList.map(item => item.id).filter((id): id is string => Boolean(id))); - manageableRoleIdSet.value = new Set(manageableRoleList.map(item => item.id).filter((id): id is string => Boolean(id))); + authorizationRoleList.value = permissionStore.roleList; }); diff --git a/src/pages/permission/RoleDelegationPermissionAssign.vue b/src/pages/permission/RoleDelegationPermissionAssign.vue index 7381d3b..058e2c9 100644 --- a/src/pages/permission/RoleDelegationPermissionAssign.vue +++ b/src/pages/permission/RoleDelegationPermissionAssign.vue @@ -186,7 +186,7 @@ function clearPermissionState(): void { // ---------- 授权范围 ---------- async function loadAuthorizationScope(): Promise { - candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles(); + candidateRoleList.value = permissionStore.roleList; } onMounted(async () => { diff --git a/src/pages/permission/RoleEdit.vue b/src/pages/permission/RoleEdit.vue index 89ec237..5a88195 100644 --- a/src/pages/permission/RoleEdit.vue +++ b/src/pages/permission/RoleEdit.vue @@ -330,7 +330,7 @@ async function fillForm(): Promise { await permissionStore.loadAll(); await loadAuthorizationScope(); if (isCreateMode.value) { - parentRoleOptions.value = await permissionStore.loadCreateParentGrantableRoles(); + parentRoleOptions.value = filterStoreParentRoles(await permissionStore.loadCreateParentGrantableRoles()); formModel.parentRoleId = parentRoleOptions.value[0]?.id || ""; return; } @@ -355,30 +355,38 @@ async function reloadCurrentRole(): Promise { childRoleIdList }); parentRoleOptions.value = canEditParentRole.value - ? await permissionStore.loadParentGrantableRoles(current.id) + ? filterStoreParentRoles(await permissionStore.loadParentGrantableRoles(current.id)) : []; - if (current.parentRole?.id && !parentRoleOptions.value.some(item => item.id === current.parentRoleId)) { + const currentParent = current.parentRole; + const canKeepCurrentParent = isSystemAdmin.value || Boolean(currentParent && ( + currentParent.moduleCode === moduleCode && currentParent.code === "STORE_MANAGER" + || permissionStore.roleList.some(item => item.id === currentParent.id) + )); + if (currentParent?.id && canKeepCurrentParent && !parentRoleOptions.value.some(item => item.id === current.parentRoleId)) { parentRoleOptions.value.unshift({ - ...current.parentRole, - id: current.parentRole.id, - permissionIdList: current.parentRole.permissionIdList || current.parentRole.permissionList?.map(item => item.id) || [], - childRoleIdList: current.parentRole.childRoleIdList || current.parentRole.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [], - delegationPermissionIdList: current.parentRole.delegationPermissionIdList || current.parentRole.delegationPermissionList?.map(item => item.id) || [] + ...currentParent, + id: currentParent.id, + permissionIdList: currentParent.permissionIdList || currentParent.permissionList?.map(item => item.id) || [], + childRoleIdList: currentParent.childRoleIdList || currentParent.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [], + delegationPermissionIdList: currentParent.delegationPermissionIdList || currentParent.delegationPermissionList?.map(item => item.id) || [] }); } } -async function loadAuthorizationScope(): Promise { - candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles(); - inheritanceRoleList.value = mergeRoleList(permissionStore.roleList, candidateRoleList.value); +/** 限制门店角色可选择的管理父级。 */ +function filterStoreParentRoles(roleList: Role[]): Role[] { + if (isSystemAdmin.value) { + return roleList; + } + + const storeRoleIdSet = new Set(permissionStore.roleList.map(item => item.id)); + return roleList.filter(role => storeRoleIdSet.has(role.id) + || (role.moduleCode === moduleCode && role.code === "STORE_MANAGER")); } -function mergeRoleList(...roleLists: Role[][]): Role[] { - const roleMap = new Map(); - for (const role of roleLists.flat()) { - roleMap.set(role.id, role); - } - return Array.from(roleMap.values()); +async function loadAuthorizationScope(): Promise { + candidateRoleList.value = permissionStore.roleList; + inheritanceRoleList.value = [...permissionStore.roleList]; } // ---------- 保存 ---------- diff --git a/src/pages/permission/RoleList.vue b/src/pages/permission/RoleList.vue index 463f351..e7f70a9 100644 --- a/src/pages/permission/RoleList.vue +++ b/src/pages/permission/RoleList.vue @@ -53,7 +53,6 @@ const permissionStore = usePermissionSettingsStore(); const userStore = useUserStore(); const isRefreshing = ref(false); const deletableRoleIdSet = ref(new Set()); -const directRoleIdSet = ref(new Set()); // ---------- 顶部导航 ---------- @@ -80,9 +79,11 @@ const sortedRoles = computed>(() => { async function removeRole(id: string): Promise { try { await permissionStore.removeRole(id); - } catch (error) { - await refreshAuthorizationScope(); await permissionStore.loadAll(true); + await refreshAuthorizationScope(); + } catch (error) { + await permissionStore.loadAll(true); + await refreshAuthorizationScope(); Message.error(getErrorMessage(error, "角色删除失败")); } } @@ -95,12 +96,7 @@ function canDeleteRole(role: Role): boolean { } function getRoleScopeLabel(role: Role): string { - const direct = directRoleIdSet.value.has(role.id); - const manageable = deletableRoleIdSet.value.has(role.id); - if (direct && manageable) { - return "直属且可管理"; - } - return direct ? "直属角色" : "可管理角色"; + return userStore.hasRoles(coreRoleCodes.admin) ? "系统角色" : role.ownerType === "STORE" ? "本店角色" : "可管理角色"; } // ---------- 页面数据 ---------- @@ -121,9 +117,7 @@ onMounted(async () => { if (canCreateRole.value) { navBarStore.setRightText("添加", "var(--td-brand-color)", toCreate); } - if (userStore.hasRoles(coreRoleCodes.admin)) { - await permissionStore.loadAll(); - } + await permissionStore.loadAll(); await refreshAuthorizationScope(); if (!displayRoleList.value.length) { displayRoleList.value = getDisplayRoleList(); @@ -163,23 +157,8 @@ async function refreshAuthorizationScope(): Promise { deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id)); return; } - - try { - const [manageableRoleList, currentRoleList] = await Promise.all([ - permissionStore.loadManageableRoles(), - permissionStore.loadCurrentRoleDetails() - ]); - const roleMap = new Map(); - for (const role of [...currentRoleList, ...manageableRoleList]) { - roleMap.set(role.id, role); - } - displayRoleList.value = Array.from(roleMap.values()); - directRoleIdSet.value = new Set(currentRoleList.map(item => item.id).filter((id): id is string => Boolean(id))); - deletableRoleIdSet.value = new Set(manageableRoleList.map(item => item.id).filter((id): id is string => Boolean(id))); - } catch { - directRoleIdSet.value = new Set(); - deletableRoleIdSet.value = new Set(); - } + displayRoleList.value = permissionStore.roleList; + deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id)); } diff --git a/src/pages/permission/RolePermissionAssign.vue b/src/pages/permission/RolePermissionAssign.vue index 7255034..60b533e 100644 --- a/src/pages/permission/RolePermissionAssign.vue +++ b/src/pages/permission/RolePermissionAssign.vue @@ -217,7 +217,7 @@ function clearPermissionState(): void { // ---------- 授权范围 ---------- async function loadAuthorizationScope(): Promise { - candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles(); + candidateRoleList.value = permissionStore.roleList; } onMounted(async () => { diff --git a/src/pages/product/ProductStockRecordList.vue b/src/pages/product/ProductStockRecordList.vue index d516edc..f314b16 100644 --- a/src/pages/product/ProductStockRecordList.vue +++ b/src/pages/product/ProductStockRecordList.vue @@ -584,6 +584,7 @@ onUnmounted(() => { } .list-wrap { + gap: 1rem; display: flex; flex-direction: column; diff --git a/src/pages/store/StoreDataCharts.vue b/src/pages/store/StoreDataCharts.vue index 1c12a33..0f2153a 100644 --- a/src/pages/store/StoreDataCharts.vue +++ b/src/pages/store/StoreDataCharts.vue @@ -15,10 +15,16 @@ @@ -34,6 +40,8 @@ import StoreEventRecordCalendarSection from "@/pages/store/component/StoreEventR import StoreEventRecordTrendChart from "@/pages/store/component/StoreEventRecordTrendChart.vue"; import StoreEventRecordNumberValueChart from "@/pages/store/component/StoreEventRecordNumberValueChart.vue"; import StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.vue"; +import StorePointDailyTrendChart from "@/pages/store/component/StorePointDailyTrendChart.vue"; +import StorePointRedemptionRankChart from "@/pages/store/component/StorePointRedemptionRankChart.vue"; import { permissionCodes } from "@/utils/permission"; import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue"; @@ -41,14 +49,15 @@ defineOptions({ name: "StoreDataCharts" }); -type ChartMode = "event" | "customer"; +type ChartMode = "event" | "customer" | "point"; // ---------- 图表切换 ---------- const chartMode = ref("event"); const chartModeOptions = [ { value: "event" as ChartMode, label: "事件登记图表" }, - { value: "customer" as ChartMode, label: "客户分析图表" } + { value: "customer" as ChartMode, label: "客户分析图表" }, + { value: "point" as ChartMode, label: "积分分析图表" } ]; // ---------- 数据源 ---------- diff --git a/src/pages/store/component/StorePointDailyTrendChart.vue b/src/pages/store/component/StorePointDailyTrendChart.vue new file mode 100644 index 0000000..ca51dca --- /dev/null +++ b/src/pages/store/component/StorePointDailyTrendChart.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/src/pages/store/component/StorePointRedemptionRankChart.vue b/src/pages/store/component/StorePointRedemptionRankChart.vue new file mode 100644 index 0000000..d69b670 --- /dev/null +++ b/src/pages/store/component/StorePointRedemptionRankChart.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/store/permissionSettingsStore.ts b/src/store/permissionSettingsStore.ts index 997576a..638edde 100644 --- a/src/store/permissionSettingsStore.ts +++ b/src/store/permissionSettingsStore.ts @@ -1,6 +1,9 @@ import { defineStore } from "pinia"; import { PermissionAPI } from "timi-web-api/api/PermissionAPI"; import { RoleAPI } from "timi-web-api/api/RoleAPI"; +import { useUserStore } from "timi-web-api"; +import { GaoRoleAPI } from "@/api/GaoRoleAPI"; +import { coreRoleCodes } from "@/utils/permission"; import type { ModuleCode, PermissionPayload, Role as UserRole } from "timi-web-api/types"; import type { Permission, @@ -58,6 +61,7 @@ function createEmptyPermission(): Permission { } export const usePermissionSettingsStore = defineStore("permission-settings", () => { + const userStore = useUserStore(); const roleList = ref([]); const permissionList = ref([]); const manageableRoleList = ref([]); @@ -72,6 +76,24 @@ export const usePermissionSettingsStore = defineStore("permission-settings", () return roots.flatMap(item => buildRoleTree(item.id, 0, new Set())); }); + async function loadRoleList(): Promise { + if (!userStore.hasRoles(coreRoleCodes.admin)) { + return (await GaoRoleAPI.managementList()).map(normalizeRole); + } + + const result = await RoleAPI.list({ + index: 0, + size: pageSize, + equalsExample: { + moduleCode: moduleCode + }, + orderMap: { + code: "ASC" + } + }); + return result.list.map(normalizeRole); + } + // ---------- 基础数据 ---------- async function loadAll(force = false): Promise { @@ -87,16 +109,7 @@ export const usePermissionSettingsStore = defineStore("permission-settings", () loading.value = true; loadTask = (async () => { const [roleResult, permissionResult] = await Promise.all([ - RoleAPI.list({ - index: 0, - size: pageSize, - equalsExample: { - moduleCode: moduleCode - }, - orderMap: { - code: "ASC" - } - }), + loadRoleList(), PermissionAPI.list({ index: 0, size: pageSize, @@ -108,7 +121,10 @@ export const usePermissionSettingsStore = defineStore("permission-settings", () } }) ]); - roleList.value = roleResult.list.map(normalizeRole); + roleList.value = roleResult; + if (!userStore.hasRoles(coreRoleCodes.admin)) { + manageableRoleList.value = roleResult; + } permissionList.value = permissionResult.list; loaded.value = true; })(); @@ -125,6 +141,9 @@ export const usePermissionSettingsStore = defineStore("permission-settings", () if (!id) { return createEmptyRole(); } + if (!userStore.hasRoles(coreRoleCodes.admin) && !roleList.value.some(item => item.id === id)) { + throw new Error("角色不在本店可管理范围内"); + } const role = normalizeRole(await RoleAPI.detail(id)); upsertLocalRole(role); diff --git a/src/types/GaoChart.ts b/src/types/GaoChart.ts index a03e994..901ec8e 100644 --- a/src/types/GaoChart.ts +++ b/src/types/GaoChart.ts @@ -11,6 +11,26 @@ export type GaoProductChartReq = { topN?: number; }; +export type GaoPointChartReq = { + storeId?: string; + beginAt: number; + endAt: number; +}; + +export type GaoPointDailyStatView = { + dateValue?: number; + earnedPoints?: number; + consumedPoints?: number; + netChangePoints?: number; +}; + +export type GaoPointRedemptionProductRankView = { + productId?: string; + productName?: string; + redemptionCount?: number; + pointCost?: number; +}; + export type GaoSalesOrderChartReq = { storeId?: string; customerId?: string;