add point chart
This commit is contained in:
@@ -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<GaoPointDailyStatView[]> {
|
||||||
|
return await requestList("point-daily-trend", "/point/ledger/daily", req);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询兑换积分最高的前 10 个产品,不扣除退回积分。 */
|
||||||
|
async function redemptionProductRank(req: GaoPointChartReq): Promise<GaoPointRedemptionProductRankView[]> {
|
||||||
|
return await requestList("point-redemption-product-rank", "/point/redemption/product/rank", req);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestList<T>(name: string, path: string, req: GaoPointChartReq): Promise<T[]> {
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -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<Role[]> {
|
||||||
|
return await axios.post(`${BASE_URI}/management/list`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -554,7 +554,6 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
.list-wrap {
|
.list-wrap {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-top: 1rem;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.date-summary {
|
.date-summary {
|
||||||
|
|||||||
@@ -92,12 +92,10 @@ const router = useRouter();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
const authorizationRoleList = ref<Role[]>([]);
|
const authorizationRoleList = ref<Role[]>([]);
|
||||||
const directRoleIdSet = ref(new Set<string>());
|
|
||||||
const manageableRoleIdSet = ref(new Set<string>());
|
|
||||||
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
||||||
const roleCount = computed(() => authorizationRoleList.value.length);
|
const roleCount = computed(() => authorizationRoleList.value.length);
|
||||||
const permissionCount = computed(() => permissionStore.permissionList.length);
|
const permissionCount = computed(() => permissionStore.permissionList.length);
|
||||||
type RoleTreeItem = Pick<Role, "id" | "name" | "parentRoleId" | "protectedRole"> & { level: number };
|
type RoleTreeItem = Pick<Role, "id" | "name" | "parentRoleId" | "protectedRole" | "ownerType"> & { level: number };
|
||||||
|
|
||||||
const visibleRoleTree = computed<RoleTreeItem[]>(() => {
|
const visibleRoleTree = computed<RoleTreeItem[]>(() => {
|
||||||
if (isSystemAdmin.value) {
|
if (isSystemAdmin.value) {
|
||||||
@@ -147,36 +145,15 @@ function canAssignRole(role: Pick<Role, "protectedRole">): boolean {
|
|||||||
return isSystemAdmin.value || role.protectedRole !== true;
|
return isSystemAdmin.value || role.protectedRole !== true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoleScopeLabel(role: Pick<Role, "id">): string {
|
function getRoleScopeLabel(role: Pick<Role, "ownerType">): string {
|
||||||
const direct = directRoleIdSet.value.has(role.id);
|
return isSystemAdmin.value ? "系统角色" : role.ownerType === "STORE" ? "本店角色" : "可管理角色";
|
||||||
const manageable = manageableRoleIdSet.value.has(role.id);
|
|
||||||
if (direct && manageable) {
|
|
||||||
return "直属且可管理";
|
|
||||||
}
|
|
||||||
return direct ? "直属角色" : "可管理角色";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 页面数据 ----------
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (isSystemAdmin.value) {
|
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
authorizationRoleList.value = permissionStore.roleList;
|
authorizationRoleList.value = permissionStore.roleList;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await permissionStore.loadAll();
|
|
||||||
const [manageableRoleList, currentRoleList] = await Promise.all([
|
|
||||||
permissionStore.loadManageableRoles(),
|
|
||||||
permissionStore.loadCurrentRoleDetails()
|
|
||||||
]);
|
|
||||||
const roleMap = new Map<string, Role>();
|
|
||||||
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)));
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ function clearPermissionState(): void {
|
|||||||
// ---------- 授权范围 ----------
|
// ---------- 授权范围 ----------
|
||||||
|
|
||||||
async function loadAuthorizationScope(): Promise<void> {
|
async function loadAuthorizationScope(): Promise<void> {
|
||||||
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
candidateRoleList.value = permissionStore.roleList;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ async function fillForm(): Promise<void> {
|
|||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
await loadAuthorizationScope();
|
await loadAuthorizationScope();
|
||||||
if (isCreateMode.value) {
|
if (isCreateMode.value) {
|
||||||
parentRoleOptions.value = await permissionStore.loadCreateParentGrantableRoles();
|
parentRoleOptions.value = filterStoreParentRoles(await permissionStore.loadCreateParentGrantableRoles());
|
||||||
formModel.parentRoleId = parentRoleOptions.value[0]?.id || "";
|
formModel.parentRoleId = parentRoleOptions.value[0]?.id || "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -355,30 +355,38 @@ async function reloadCurrentRole(): Promise<void> {
|
|||||||
childRoleIdList
|
childRoleIdList
|
||||||
});
|
});
|
||||||
parentRoleOptions.value = canEditParentRole.value
|
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({
|
parentRoleOptions.value.unshift({
|
||||||
...current.parentRole,
|
...currentParent,
|
||||||
id: current.parentRole.id,
|
id: currentParent.id,
|
||||||
permissionIdList: current.parentRole.permissionIdList || current.parentRole.permissionList?.map(item => item.id) || [],
|
permissionIdList: currentParent.permissionIdList || currentParent.permissionList?.map(item => item.id) || [],
|
||||||
childRoleIdList: current.parentRole.childRoleIdList || current.parentRole.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
|
childRoleIdList: currentParent.childRoleIdList || currentParent.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
|
||||||
delegationPermissionIdList: current.parentRole.delegationPermissionIdList || current.parentRole.delegationPermissionList?.map(item => item.id) || []
|
delegationPermissionIdList: currentParent.delegationPermissionIdList || currentParent.delegationPermissionList?.map(item => item.id) || []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAuthorizationScope(): Promise<void> {
|
/** 限制门店角色可选择的管理父级。 */
|
||||||
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
function filterStoreParentRoles(roleList: Role[]): Role[] {
|
||||||
inheritanceRoleList.value = mergeRoleList(permissionStore.roleList, candidateRoleList.value);
|
if (isSystemAdmin.value) {
|
||||||
|
return roleList;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeRoleList(...roleLists: Role[][]): Role[] {
|
const storeRoleIdSet = new Set(permissionStore.roleList.map(item => item.id));
|
||||||
const roleMap = new Map<string, Role>();
|
return roleList.filter(role => storeRoleIdSet.has(role.id)
|
||||||
for (const role of roleLists.flat()) {
|
|| (role.moduleCode === moduleCode && role.code === "STORE_MANAGER"));
|
||||||
roleMap.set(role.id, role);
|
|
||||||
}
|
}
|
||||||
return Array.from(roleMap.values());
|
|
||||||
|
async function loadAuthorizationScope(): Promise<void> {
|
||||||
|
candidateRoleList.value = permissionStore.roleList;
|
||||||
|
inheritanceRoleList.value = [...permissionStore.roleList];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 保存 ----------
|
// ---------- 保存 ----------
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ const permissionStore = usePermissionSettingsStore();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const isRefreshing = ref(false);
|
const isRefreshing = ref(false);
|
||||||
const deletableRoleIdSet = ref(new Set<string>());
|
const deletableRoleIdSet = ref(new Set<string>());
|
||||||
const directRoleIdSet = ref(new Set<string>());
|
|
||||||
|
|
||||||
// ---------- 顶部导航 ----------
|
// ---------- 顶部导航 ----------
|
||||||
|
|
||||||
@@ -80,9 +79,11 @@ const sortedRoles = computed<Array<Role & { level: number }>>(() => {
|
|||||||
async function removeRole(id: string): Promise<void> {
|
async function removeRole(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await permissionStore.removeRole(id);
|
await permissionStore.removeRole(id);
|
||||||
} catch (error) {
|
|
||||||
await refreshAuthorizationScope();
|
|
||||||
await permissionStore.loadAll(true);
|
await permissionStore.loadAll(true);
|
||||||
|
await refreshAuthorizationScope();
|
||||||
|
} catch (error) {
|
||||||
|
await permissionStore.loadAll(true);
|
||||||
|
await refreshAuthorizationScope();
|
||||||
Message.error(getErrorMessage(error, "角色删除失败"));
|
Message.error(getErrorMessage(error, "角色删除失败"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,12 +96,7 @@ function canDeleteRole(role: Role): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRoleScopeLabel(role: Role): string {
|
function getRoleScopeLabel(role: Role): string {
|
||||||
const direct = directRoleIdSet.value.has(role.id);
|
return userStore.hasRoles(coreRoleCodes.admin) ? "系统角色" : role.ownerType === "STORE" ? "本店角色" : "可管理角色";
|
||||||
const manageable = deletableRoleIdSet.value.has(role.id);
|
|
||||||
if (direct && manageable) {
|
|
||||||
return "直属且可管理";
|
|
||||||
}
|
|
||||||
return direct ? "直属角色" : "可管理角色";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 页面数据 ----------
|
// ---------- 页面数据 ----------
|
||||||
@@ -121,9 +117,7 @@ onMounted(async () => {
|
|||||||
if (canCreateRole.value) {
|
if (canCreateRole.value) {
|
||||||
navBarStore.setRightText("添加", "var(--td-brand-color)", toCreate);
|
navBarStore.setRightText("添加", "var(--td-brand-color)", toCreate);
|
||||||
}
|
}
|
||||||
if (userStore.hasRoles(coreRoleCodes.admin)) {
|
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
}
|
|
||||||
await refreshAuthorizationScope();
|
await refreshAuthorizationScope();
|
||||||
if (!displayRoleList.value.length) {
|
if (!displayRoleList.value.length) {
|
||||||
displayRoleList.value = getDisplayRoleList();
|
displayRoleList.value = getDisplayRoleList();
|
||||||
@@ -163,23 +157,8 @@ async function refreshAuthorizationScope(): Promise<void> {
|
|||||||
deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id));
|
deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
displayRoleList.value = permissionStore.roleList;
|
||||||
try {
|
deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id));
|
||||||
const [manageableRoleList, currentRoleList] = await Promise.all([
|
|
||||||
permissionStore.loadManageableRoles(),
|
|
||||||
permissionStore.loadCurrentRoleDetails()
|
|
||||||
]);
|
|
||||||
const roleMap = new Map<string, Role>();
|
|
||||||
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<string>();
|
|
||||||
deletableRoleIdSet.value = new Set<string>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ function clearPermissionState(): void {
|
|||||||
// ---------- 授权范围 ----------
|
// ---------- 授权范围 ----------
|
||||||
|
|
||||||
async function loadAuthorizationScope(): Promise<void> {
|
async function loadAuthorizationScope(): Promise<void> {
|
||||||
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
candidateRoleList.value = permissionStore.roleList;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -584,6 +584,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-wrap {
|
.list-wrap {
|
||||||
|
gap: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,17 @@
|
|||||||
<store-event-structure-chart v-entrance-transition="280" :date-text-list="dateTextList" />
|
<store-event-structure-chart v-entrance-transition="280" :date-text-list="dateTextList" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<template v-if="chartMode === 'customer'">
|
||||||
<store-customer-growth-chart v-entrance-transition="70" :date-text-list="dateTextList" />
|
<store-customer-growth-chart v-entrance-transition="70" :date-text-list="dateTextList" />
|
||||||
<store-customer-age-chart v-entrance-transition="140" />
|
<store-customer-age-chart v-entrance-transition="140" />
|
||||||
<store-customer-disease-word-cloud-chart v-entrance-transition="210" />
|
<store-customer-disease-word-cloud-chart v-entrance-transition="210" />
|
||||||
<store-customer-gender-chart v-entrance-transition="280" />
|
<store-customer-gender-chart v-entrance-transition="280" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<store-point-daily-trend-chart v-entrance-transition="70" :date-text-list="dateTextList" />
|
||||||
|
<store-point-redemption-rank-chart v-entrance-transition="140" :date-text-list="dateTextList" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -34,6 +40,8 @@ import StoreEventRecordCalendarSection from "@/pages/store/component/StoreEventR
|
|||||||
import StoreEventRecordTrendChart from "@/pages/store/component/StoreEventRecordTrendChart.vue";
|
import StoreEventRecordTrendChart from "@/pages/store/component/StoreEventRecordTrendChart.vue";
|
||||||
import StoreEventRecordNumberValueChart from "@/pages/store/component/StoreEventRecordNumberValueChart.vue";
|
import StoreEventRecordNumberValueChart from "@/pages/store/component/StoreEventRecordNumberValueChart.vue";
|
||||||
import StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.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 { permissionCodes } from "@/utils/permission";
|
||||||
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
||||||
|
|
||||||
@@ -41,14 +49,15 @@ defineOptions({
|
|||||||
name: "StoreDataCharts"
|
name: "StoreDataCharts"
|
||||||
});
|
});
|
||||||
|
|
||||||
type ChartMode = "event" | "customer";
|
type ChartMode = "event" | "customer" | "point";
|
||||||
|
|
||||||
// ---------- 图表切换 ----------
|
// ---------- 图表切换 ----------
|
||||||
|
|
||||||
const chartMode = ref<ChartMode>("event");
|
const chartMode = ref<ChartMode>("event");
|
||||||
const chartModeOptions = [
|
const chartModeOptions = [
|
||||||
{ value: "event" as ChartMode, label: "事件登记图表" },
|
{ value: "event" as ChartMode, label: "事件登记图表" },
|
||||||
{ value: "customer" as ChartMode, label: "客户分析图表" }
|
{ value: "customer" as ChartMode, label: "客户分析图表" },
|
||||||
|
{ value: "point" as ChartMode, label: "积分分析图表" }
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---------- 数据源 ----------
|
// ---------- 数据源 ----------
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<template>
|
||||||
|
<section class="chart-section">
|
||||||
|
<div class="title">
|
||||||
|
<h2>每日积分变动趋势</h2>
|
||||||
|
</div>
|
||||||
|
<div ref="chartElement" class="chart" />
|
||||||
|
<div v-if="loading" class="center-tips">
|
||||||
|
<t-loading size="1.5rem" text="加载中" />
|
||||||
|
</div>
|
||||||
|
<chart-range-slider
|
||||||
|
v-if="showSlider"
|
||||||
|
:model-value="sliderValue"
|
||||||
|
:max="maxSliderValue"
|
||||||
|
:min-range="5"
|
||||||
|
:label="rangeText"
|
||||||
|
@change="handleSliderChange"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Time } from "timi-web";
|
||||||
|
import type { EChartsOption } from "echarts";
|
||||||
|
import GaoPointChartAPI from "@/api/GaoPointChartAPI";
|
||||||
|
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
|
||||||
|
import type { GaoPointDailyStatView } from "@/types/GaoChart";
|
||||||
|
import { buildStoreBaseChartOption, useStoreChart, useStoreChartWindow } from "@/pages/store/component/useStoreChart";
|
||||||
|
import { buildRestDayMarkArea, useRestDayCalendar } from "@/pages/store/component/useRestDayCalendar";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "StorePointDailyTrendChart"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dateTextList: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dateTextList = computed(() => props.dateTextList);
|
||||||
|
const chartWidth = ref(window.innerWidth);
|
||||||
|
const statList = ref<GaoPointDailyStatView[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
let statRequestId = 0;
|
||||||
|
const { restDaySet } = useRestDayCalendar(dateTextList);
|
||||||
|
const {
|
||||||
|
rangeText,
|
||||||
|
showSlider,
|
||||||
|
sliderValue,
|
||||||
|
maxSliderValue,
|
||||||
|
visibleDateTextList,
|
||||||
|
handleSliderChange
|
||||||
|
} = useStoreChartWindow(dateTextList);
|
||||||
|
const chartOption = computed<EChartsOption>(() => {
|
||||||
|
const labelList = visibleDateTextList.value.map(item => item.slice(5));
|
||||||
|
return {
|
||||||
|
...buildStoreBaseChartOption(visibleDateTextList.value),
|
||||||
|
grid: {
|
||||||
|
top: 32,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: labelList,
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: { show: true, length: 0, interval: 0 },
|
||||||
|
axisLabel: { color: "#52607a", interval: "auto", hideOverlap: true }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "获得积分",
|
||||||
|
type: "bar",
|
||||||
|
stack: "stock-movement",
|
||||||
|
markArea: buildRestDayMarkArea(visibleDateTextList.value, restDaySet.value, chartWidth.value),
|
||||||
|
data: visibleDateTextList.value.map(dateText => Math.abs(getNumber(getDailyItem(dateText)?.earnedPoints)))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "消耗积分",
|
||||||
|
type: "bar",
|
||||||
|
stack: "stock-movement",
|
||||||
|
data: visibleDateTextList.value.map(dateText => -Math.abs(getNumber(getDailyItem(dateText)?.consumedPoints)))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "获得与消耗差值",
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
data: visibleDateTextList.value.map(dateText => getNumber(getDailyItem(dateText)?.netChangePoints))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const { chartElement } = useStoreChart(chartOption);
|
||||||
|
let resizeObserver: ResizeObserver | undefined;
|
||||||
|
|
||||||
|
watch(() => props.dateTextList, async () => {
|
||||||
|
await loadDailyStat();
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resizeObserver = new window.ResizeObserver(updateChartWidth);
|
||||||
|
if (chartElement.value) {
|
||||||
|
resizeObserver.observe(chartElement.value);
|
||||||
|
updateChartWidth();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
resizeObserver = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadDailyStat(): Promise<void> {
|
||||||
|
if (!props.dateTextList.length) {
|
||||||
|
statRequestId++;
|
||||||
|
statList.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++statRequestId;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await GaoPointChartAPI.dailyTrend({
|
||||||
|
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
|
||||||
|
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1]))
|
||||||
|
});
|
||||||
|
if (requestId === statRequestId) {
|
||||||
|
statList.value = list;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === statRequestId) {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDailyItem(dateText: string): GaoPointDailyStatView | undefined {
|
||||||
|
const dateValue = Number(dateText.replaceAll("-", ""));
|
||||||
|
return statList.value.find(item => item.dateValue === dateValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNumber(value?: number): number {
|
||||||
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChartWidth(): void {
|
||||||
|
chartWidth.value = chartElement.value?.clientWidth || window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateStart(dateText: string): Date {
|
||||||
|
const [year, month, day] = dateText.split("-").map(Number);
|
||||||
|
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateEnd(dateText: string): Date {
|
||||||
|
const [year, month, day] = dateText.split("-").map(Number);
|
||||||
|
return new Date(year, month - 1, day, 23, 59, 59, 999);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.chart-section {
|
||||||
|
padding: 1rem;
|
||||||
|
position: relative;
|
||||||
|
background: rgba(255, 255, 255, .92);
|
||||||
|
box-shadow: var(--td-shadow-1);
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 .875rem 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: .6875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 17rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-tips {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.chart-section {
|
||||||
|
background: rgba(15, 23, 42, .88);
|
||||||
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
h2 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<date-range-chart-card
|
||||||
|
title="积分兑换排行"
|
||||||
|
:option="chartOption"
|
||||||
|
:loading="loading"
|
||||||
|
height="20rem"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Time } from "timi-web";
|
||||||
|
import type { EChartsOption } from "echarts";
|
||||||
|
import GaoPointChartAPI from "@/api/GaoPointChartAPI";
|
||||||
|
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
|
||||||
|
import type { GaoPointRedemptionProductRankView } from "@/types/GaoChart";
|
||||||
|
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "StorePointRedemptionRankChart"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dateTextList: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rankList = ref<GaoPointRedemptionProductRankView[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
let rankRequestId = 0;
|
||||||
|
const chartOption = computed<EChartsOption>(() => {
|
||||||
|
const list = rankList.value.slice(0, 10);
|
||||||
|
return {
|
||||||
|
...buildHorizontalBarOption(list.map(getProductName)),
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "兑换积分",
|
||||||
|
type: "bar",
|
||||||
|
barMaxWidth: 20,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: "right",
|
||||||
|
color: "#52607a",
|
||||||
|
formatter: "{c}"
|
||||||
|
},
|
||||||
|
data: list.map(item => getNumber(item.pointCost))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.dateTextList, async () => {
|
||||||
|
await loadRankList();
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadRankList(): Promise<void> {
|
||||||
|
if (!props.dateTextList.length) {
|
||||||
|
rankRequestId++;
|
||||||
|
rankList.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++rankRequestId;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await GaoPointChartAPI.redemptionProductRank({
|
||||||
|
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
|
||||||
|
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1]))
|
||||||
|
});
|
||||||
|
if (requestId === rankRequestId) {
|
||||||
|
rankList.value = list.slice(0, 10);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === rankRequestId) {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProductName(item: GaoPointRedemptionProductRankView): string {
|
||||||
|
return item.productName || "未命名产品";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateStart(dateText: string): Date {
|
||||||
|
const [year, month, day] = dateText.split("-").map(Number);
|
||||||
|
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateEnd(dateText: string): Date {
|
||||||
|
const [year, month, day] = dateText.split("-").map(Number);
|
||||||
|
return new Date(year, month - 1, day, 23, 59, 59, 999);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { PermissionAPI } from "timi-web-api/api/PermissionAPI";
|
import { PermissionAPI } from "timi-web-api/api/PermissionAPI";
|
||||||
import { RoleAPI } from "timi-web-api/api/RoleAPI";
|
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 { ModuleCode, PermissionPayload, Role as UserRole } from "timi-web-api/types";
|
||||||
import type {
|
import type {
|
||||||
Permission,
|
Permission,
|
||||||
@@ -58,6 +61,7 @@ function createEmptyPermission(): Permission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const usePermissionSettingsStore = defineStore("permission-settings", () => {
|
export const usePermissionSettingsStore = defineStore("permission-settings", () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
const roleList = ref<Role[]>([]);
|
const roleList = ref<Role[]>([]);
|
||||||
const permissionList = ref<Permission[]>([]);
|
const permissionList = ref<Permission[]>([]);
|
||||||
const manageableRoleList = ref<Role[]>([]);
|
const manageableRoleList = ref<Role[]>([]);
|
||||||
@@ -72,6 +76,24 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
|
|||||||
return roots.flatMap(item => buildRoleTree(item.id, 0, new Set<string>()));
|
return roots.flatMap(item => buildRoleTree(item.id, 0, new Set<string>()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadRoleList(): Promise<Role[]> {
|
||||||
|
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<void> {
|
async function loadAll(force = false): Promise<void> {
|
||||||
@@ -87,16 +109,7 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
loadTask = (async () => {
|
loadTask = (async () => {
|
||||||
const [roleResult, permissionResult] = await Promise.all([
|
const [roleResult, permissionResult] = await Promise.all([
|
||||||
RoleAPI.list({
|
loadRoleList(),
|
||||||
index: 0,
|
|
||||||
size: pageSize,
|
|
||||||
equalsExample: {
|
|
||||||
moduleCode: moduleCode
|
|
||||||
},
|
|
||||||
orderMap: {
|
|
||||||
code: "ASC"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
PermissionAPI.list({
|
PermissionAPI.list({
|
||||||
index: 0,
|
index: 0,
|
||||||
size: pageSize,
|
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;
|
permissionList.value = permissionResult.list;
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
})();
|
})();
|
||||||
@@ -125,6 +141,9 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return createEmptyRole();
|
return createEmptyRole();
|
||||||
}
|
}
|
||||||
|
if (!userStore.hasRoles(coreRoleCodes.admin) && !roleList.value.some(item => item.id === id)) {
|
||||||
|
throw new Error("角色不在本店可管理范围内");
|
||||||
|
}
|
||||||
|
|
||||||
const role = normalizeRole(await RoleAPI.detail(id));
|
const role = normalizeRole(await RoleAPI.detail(id));
|
||||||
upsertLocalRole(role);
|
upsertLocalRole(role);
|
||||||
|
|||||||
@@ -11,6 +11,26 @@ export type GaoProductChartReq = {
|
|||||||
topN?: number;
|
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 = {
|
export type GaoSalesOrderChartReq = {
|
||||||
storeId?: string;
|
storeId?: string;
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user