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 {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding-top: 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
.date-summary {
|
||||
|
||||
@@ -92,12 +92,10 @@ const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
const authorizationRoleList = ref<Role[]>([]);
|
||||
const directRoleIdSet = ref(new Set<string>());
|
||||
const manageableRoleIdSet = ref(new Set<string>());
|
||||
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
||||
const roleCount = computed(() => authorizationRoleList.value.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[]>(() => {
|
||||
if (isSystemAdmin.value) {
|
||||
@@ -147,36 +145,15 @@ function canAssignRole(role: Pick<Role, "protectedRole">): boolean {
|
||||
return isSystemAdmin.value || role.protectedRole !== true;
|
||||
}
|
||||
|
||||
function getRoleScopeLabel(role: Pick<Role, "id">): 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<Role, "ownerType">): 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<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>
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ function clearPermissionState(): void {
|
||||
// ---------- 授权范围 ----------
|
||||
|
||||
async function loadAuthorizationScope(): Promise<void> {
|
||||
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
||||
candidateRoleList.value = permissionStore.roleList;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -330,7 +330,7 @@ async function fillForm(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
function mergeRoleList(...roleLists: Role[][]): Role[] {
|
||||
const roleMap = new Map<string, Role>();
|
||||
for (const role of roleLists.flat()) {
|
||||
roleMap.set(role.id, role);
|
||||
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"));
|
||||
}
|
||||
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 isRefreshing = ref(false);
|
||||
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> {
|
||||
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 refreshAuthorizationScope();
|
||||
if (!displayRoleList.value.length) {
|
||||
displayRoleList.value = getDisplayRoleList();
|
||||
@@ -163,23 +157,8 @@ async function refreshAuthorizationScope(): Promise<void> {
|
||||
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<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>();
|
||||
}
|
||||
displayRoleList.value = permissionStore.roleList;
|
||||
deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -217,7 +217,7 @@ function clearPermissionState(): void {
|
||||
// ---------- 授权范围 ----------
|
||||
|
||||
async function loadAuthorizationScope(): Promise<void> {
|
||||
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
||||
candidateRoleList.value = permissionStore.roleList;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -584,6 +584,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
@@ -15,11 +15,17 @@
|
||||
<store-event-structure-chart v-entrance-transition="280" :date-text-list="dateTextList" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="chartMode === 'customer'">
|
||||
<store-customer-growth-chart v-entrance-transition="70" :date-text-list="dateTextList" />
|
||||
<store-customer-age-chart v-entrance-transition="140" />
|
||||
<store-customer-disease-word-cloud-chart v-entrance-transition="210" />
|
||||
<store-customer-gender-chart v-entrance-transition="280" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -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<ChartMode>("event");
|
||||
const chartModeOptions = [
|
||||
{ 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 { 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<Role[]>([]);
|
||||
const permissionList = ref<Permission[]>([]);
|
||||
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>()));
|
||||
});
|
||||
|
||||
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> {
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user