Files
gao-customer-h5/src/pages/point/PointRuleDetail.vue
T
2026-08-24 16:16:43 +08:00

361 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-can="permissionCodes.pointRule.read" class="point-rule-detail">
<section v-if="rule" class="rule-header" v-entrance-transition>
<div class="point-icon" :class="{ consume: isConsumeRule }">
<t-icon name="money" size="1.75rem" />
</div>
<div class="header-content">
<div class="title-row">
<h2 v-text="rule.name || '未命名规则'" />
<t-tag :theme="getStatusTheme(rule.status)" variant="light-outline" :content="getStatusText(rule.status)" />
</div>
<div class="point-value" v-text="rulePointText" />
</div>
</section>
<t-cell-group v-if="rule" title="基本信息" theme="card" v-entrance-transition="70">
<t-cell title="所属门店" :note="storeName" />
<t-cell
title="登记事件"
:note="event?.name || '未绑定事件'"
:arrow="canReadEvent && !!event?.id"
:hover="canReadEvent && !!event?.id"
@click="toEventDetail"
/>
<t-cell title="规则名称" :note="rule.name || '未命名规则'" />
<t-cell title="规则状态">
<template #note>
<t-tag :theme="getStatusTheme(rule.status)" variant="light-outline" :content="getStatusText(rule.status)" />
</template>
</t-cell>
<t-cell title="规则备注" :note="Text.display(rule.remark)" />
</t-cell-group>
<t-cell-group v-if="rule" title="积分配置" theme="card" v-entrance-transition="140">
<t-cell title="触发类型" :note="getTriggerText(rule.triggerType)" />
<t-cell title="积分值模式" :note="getModeText(rule.pointMode)" />
<t-cell v-if="rule.pointMode === 'FIXED'" title="固定积分" :note="formatPointValue(rule.pointValue)" />
<t-cell v-if="rule.pointMode === 'SOURCE_VALUE'" title="自定义积分范围" :note="sourceValueRangeText" />
<t-cell v-if="rule.triggerType === 'AFTER_COUNT_EVERY_TIME'" title="触发阈值" :note="getThresholdText(rule)" />
<t-cell v-if="rule.triggerType === 'FIRST_TIME'" title="匹配范围" note="每个客户的首次登记" />
<t-cell v-if="rule.conditionJson?.window" title="累计窗口" :note="getWindowText(rule)" />
<t-cell v-if="rule.conditionJson?.sourceEventId" title="来源事件">
<template #note>
<span v-text="sourceEvent?.name || rule.conditionJson.sourceEventId" />
</template>
</t-cell>
</t-cell-group>
<t-cell-group
v-if="tierTextList.length"
title="积分分档"
theme="card"
v-entrance-transition="210"
>
<t-cell v-for="(text, index) in tierTextList" :key="index" :title="`第 ${index + 1} 档`" :note="text" />
</t-cell-group>
<t-cell-group v-if="rule" title="其他配置" theme="card" v-entrance-transition="280">
<t-cell title="规则优先级" :note="formatNumber(rule.priority)" />
<t-cell title="命中后停止匹配" :note="rule.exclusive ? '是' : '否'" />
<t-cell title="生效开始" :note="formatDate(rule.beginAt)" />
<t-cell title="生效结束" :note="formatDate(rule.endAt)" />
<t-cell title="创建时间" :note="formatDate(rule.createdAt, '未知时间')" />
<t-cell title="更新时间" :note="formatDate(rule.updatedAt, '未知时间')" />
</t-cell-group>
<t-empty v-else :description="isLoading ? '正在加载积分规则' : '积分规则不存在'" />
</div>
</template>
<script setup lang="ts">
import { Text, Time, useUserStore } from "timi-web";
import EventAPI from "@/api/EventAPI";
import PointAPI from "@/api/PointAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem } from "@/types/EventItem";
import type { PointRule, PointRuleStatus, PointRuleMode, PointRuleTriggerType } from "@/types/Point";
import { formatEventNumberValue } from "@/utils/eventValue";
import { Message } from "@/utils/message";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({
name: "PointRuleDetail"
});
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
// ---------- 权限与门店 ----------
const canUpdate = computed(() => userStore.hasPermissions(permissionCodes.pointRule.update));
const canReadEvent = computed(() => userStore.hasPermissions(permissionCodes.event.read));
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const storeName = computed(() => {
const storeId = rule.value?.storeId;
if (!storeId) {
return "未绑定门店";
}
const store = currentStoreStore.storeList.find(item => item.id === storeId)
|| (currentStoreStore.loginStore?.id === storeId ? currentStoreStore.loginStore : undefined);
return store?.name || store?.code || storeId;
});
// ---------- 规则资料 ----------
const rule = ref<PointRule>();
const event = ref<EventItem>();
const sourceEvent = ref<EventItem>();
const isLoading = ref(false);
const condition = computed(() => rule.value?.conditionJson);
const isConsumeRule = computed(() => (rule.value?.pointValue ?? 0) < 0);
const rulePointText = computed(() => {
if (rule.value?.pointMode === "TIER") {
return condition.value?.tiers?.length ? `${condition.value.tiers.length} 档积分` : "分档积分";
}
if (rule.value?.pointMode === "SOURCE_VALUE") {
return "按登记时填写的积分变化";
}
return formatPointValue(rule.value?.pointValue);
});
const sourceValueRangeText = computed(() => {
const minText = condition.value?.min === undefined ? "不限" : formatPointValue(condition.value.min);
const maxText = condition.value?.max === undefined ? "不限" : formatPointValue(condition.value.max);
return `最小值:${minText};最大值:${maxText}`;
});
const tierTextList = computed(() => (condition.value?.tiers || []).map(tier => {
const minText = formatEventNumberValue(sourceEvent.value, tier.min);
const maxText = tier.max === undefined || tier.max === null ? "以上" : formatEventNumberValue(sourceEvent.value, tier.max);
return `${minText}~${maxText}:${formatPointValue(tier.pointValue)}`;
}));
async function loadStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
return;
}
await currentStoreStore.loadLoginStore();
}
async function loadOptionalEvent(eventId: string, message: string): Promise<EventItem | undefined> {
try {
return await EventAPI.detail(eventId);
} catch (error) {
console.warn(message, error);
return undefined;
}
}
async function loadRule(): Promise<void> {
const ruleId = String(route.params.id || "");
if (!ruleId) {
rule.value = undefined;
event.value = undefined;
sourceEvent.value = undefined;
return;
}
rule.value = undefined;
event.value = undefined;
sourceEvent.value = undefined;
isLoading.value = true;
try {
await loadStoreScope();
rule.value = await PointAPI.ruleDetail(ruleId);
if (!canReadEvent.value || !rule.value) {
return;
}
const eventId = rule.value.eventId || "";
const sourceEventId = rule.value.conditionJson?.sourceEventId || "";
const [eventResult, sourceEventResult] = await Promise.all([
eventId ? loadOptionalEvent(eventId, "积分规则登记事件加载失败") : Promise.resolve(undefined),
sourceEventId && sourceEventId !== eventId
? loadOptionalEvent(sourceEventId, "积分规则来源事件加载失败")
: Promise.resolve(undefined)
]);
event.value = eventResult;
sourceEvent.value = sourceEventResult || (sourceEventId === eventId ? eventResult : undefined);
} catch (error) {
rule.value = undefined;
event.value = undefined;
sourceEvent.value = undefined;
console.warn("积分规则详情加载失败", error);
Message.error("积分规则加载失败");
} finally {
isLoading.value = false;
}
}
// ---------- 展示 ----------
function getStatusText(status?: PointRuleStatus): string {
return ({
DRAFT: "草稿",
ACTIVE: "启用",
INACTIVE: "停用",
DELETED: "已删除"
} as Record<string, string>)[status || ""] || "未知";
}
function getStatusTheme(status?: PointRuleStatus): "default" | "success" | "warning" | "danger" {
if (status === "ACTIVE") {
return "success";
}
if (status === "INACTIVE") {
return "warning";
}
if (status === "DELETED") {
return "danger";
}
return "default";
}
function getTriggerText(triggerType?: PointRuleTriggerType): string {
return ({
EVERY_TIME: "每次登记",
FIRST_TIME: "首次登记",
AFTER_COUNT_EVERY_TIME: "达到阈值后每次登记",
SOURCE_VALUE: "登记时自定义积分",
CUSTOMER_EVENT_VALUE_TIER: "按来源事件累计分档"
} as Record<string, string>)[triggerType || ""] || "未知触发类型";
}
function getModeText(pointMode?: PointRuleMode): string {
return ({
FIXED: "固定积分",
SOURCE_VALUE: "使用登记时填写的积分",
TIER: "分档积分"
} as Record<string, string>)[pointMode || ""] || "未知积分模式";
}
function formatPointValue(value?: number | null): string {
if (value === undefined || value === null) {
return "未配置";
}
if (value === 0) {
return "不产生积分流水";
}
return `${0 < value ? "发放" : "消耗"} ${Math.abs(value)} 分`;
}
function getThresholdText(value: PointRule): string {
const threshold = value.conditionJson?.threshold;
return threshold === undefined || threshold === null ? "未配置" : `${threshold} 次`;
}
function getWindowText(value: PointRule): string {
const window = value.conditionJson?.window;
if (!window) {
return "未配置";
}
return `${window.amount} ${window.unit === "YEAR" ? "年" : window.unit}`;
}
function formatNumber(value?: number): string {
return value === undefined || value === null ? "未配置" : String(value);
}
function formatDate(timestamp?: number, emptyText = "不限"): string {
return timestamp === undefined || timestamp === null ? emptyText : Time.toDateTime(timestamp);
}
// ---------- 导航 ----------
async function toEdit(): Promise<void> {
const ruleId = String(route.params.id || "");
if (!canUpdate.value || !ruleId) {
return;
}
await router.push({
name: "PointRuleEdit",
params: { id: ruleId },
query: { transitionDepth: String(Number(route.meta.depth ?? 0) + 1) }
});
}
async function toEventDetail(): Promise<void> {
if (!canReadEvent.value || !event.value?.id) {
return;
}
await router.push({
name: "EventDetail",
params: { id: event.value.id },
query: { transitionDepth: String(Number(route.meta.depth ?? 0) + 1) }
});
}
watch(() => route.params.id, async () => {
await loadRule();
}, { immediate: true });
onMounted(() => {
navBarStore.clearRight();
if (canUpdate.value) {
navBarStore.setRightText("编辑", "var(--td-brand-color)", toEdit);
}
});
onUnmounted(() => {
navBarStore.clearRight();
});
</script>
<style scoped lang="less">
.point-rule-detail {
gap: 1rem;
min-height: 100vh;
padding: calc(var(--app-nav-height) + 1rem) 0 1rem;
display: flex;
flex-direction: column;
.rule-header {
gap: .75rem;
margin: 0 1rem;
padding: 1rem;
display: flex;
background: var(--td-bg-color-container);
box-shadow: var(--td-shadow-1);
border-radius: 1.4rem;
.point-icon {
width: 3.5rem;
height: 3.5rem;
color: var(--td-success-color);
flex: 0 0 auto;
display: grid;
place-items: center;
background: rgba(34, 197, 94, .12);
border-radius: .7rem;
&.consume {
color: var(--td-error-color);
background: rgba(239, 68, 68, .12);
}
}
.header-content {
min-width: 0;
flex: 1;
}
.title-row {
gap: .5rem;
display: flex;
align-items: center;
justify-content: space-between;
h2 {
margin: 0;
font-size: 1.125rem;
}
}
.point-value {
margin-top: .5rem;
color: var(--td-text-color-secondary);
}
}
}
</style>