add point

This commit is contained in:
Timi
2026-08-11 22:25:30 +08:00
parent e612cfd382
commit 9f60a36c6c
21 changed files with 2928 additions and 36 deletions
+357
View File
@@ -0,0 +1,357 @@
<template>
<div v-can="permissionCodes.pointRule.read" class="point-rule-list">
<div class="filter-menu">
<t-dropdown-menu>
<t-dropdown-item
v-if="isGlobalManager"
v-model="selectedStoreId"
:label="selectedStoreName"
:options="storeOptions"
@change="selectStore"
/>
<t-dropdown-item
v-model="statusFilter"
label="规则状态"
:options="statusOptions"
@change="selectStatus"
/>
</t-dropdown-menu>
</div>
<t-pull-down-refresh
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="reload"
@scrolltolower="load"
>
<t-cell-group v-if="pageResult.list.length" class="rule-list" theme="card">
<t-cell
v-for="item in pageResult.list"
:key="item.id"
class="rule-item"
:title="item.name || '未命名规则'"
:description="getRuleDescription(item)"
:note="getRuleNote(item)"
:arrow="canUpdate"
:hover="canUpdate"
@click="toEdit(item)"
>
<template #leftIcon>
<div class="priority" v-text="item.priority ?? 0" />
</template>
<template #extra>
<t-tag
:theme="getStatusTheme(item.status)"
variant="light-outline"
:content="getStatusText(item.status)"
/>
</template>
</t-cell>
</t-cell-group>
<div v-else class="empty" v-text="isLoading ? '正在加载积分规则' : '暂无积分规则'" />
<div class="load-state" v-text="loadStateText" />
</t-pull-down-refresh>
<t-button v-can="permissionCodes.pointRule.create" class="create" theme="primary" @click="toCreate">
<template #icon>
<t-icon name="add" />
</template>
新增积分规则
</t-button>
</div>
</template>
<script setup lang="ts">
import { Toast, type DropdownValue } from "tdesign-mobile-vue";
import EventAPI from "@/api/EventAPI";
import PointAPI from "@/api/PointAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { EventItem } from "@/types/EventItem";
import type { PointRule, PointRuleStatus } from "@/types/Point";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { useUserStore, type Page, type PageResult } from "timi-web";
const allStoresValue = "__ALL_STORES__";
defineOptions({
name: "PointRuleList"
});
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
// ---------- 筛选与门店 ----------
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const canUpdate = computed(() => userStore.hasPermissions(permissionCodes.pointRule.update));
const selectedStoreId = ref(allStoresValue);
const statusFilter = ref("ALL");
const statusOptions = [
{ label: "全部状态", value: "ALL" },
{ label: "草稿", value: "DRAFT" },
{ label: "启用", value: "ACTIVE" },
{ label: "停用", value: "INACTIVE" },
{ label: "已删除", value: "DELETED" }
];
const storeOptions = computed(() => [
{ label: "全部门店", value: allStoresValue },
...currentStoreStore.storeList
.map(item => ({
label: item.name || item.code || "未命名门店",
value: item.id || ""
}))
.filter(item => item.value)
]);
const selectedStoreName = computed(() => storeOptions.value.find(item => item.value === selectedStoreId.value)?.label || "全部门店");
async function selectStore(value: DropdownValue): Promise<void> {
if (Array.isArray(value)) {
return;
}
selectedStoreId.value = String(value);
await loadEventList();
await reload();
}
async function selectStatus(value: DropdownValue): Promise<void> {
if (Array.isArray(value)) {
return;
}
statusFilter.value = String(value);
await reload();
}
// ---------- 规则列表 ----------
const page = reactive<Page<PointRule>>({
index: 0,
size: 20
});
const pageResult = reactive<PageResult<PointRule>>({
total: 0,
list: []
});
const isLoading = ref(false);
const isRefreshing = ref(false);
const isNoMore = ref(false);
let loadToken = 0;
const loadStateText = computed(() => {
if (isLoading.value) {
return "正在加载更多";
}
if (isNoMore.value) {
return pageResult.list.length ? `共 ${pageResult.total} 条规则` : "";
}
return "上拉加载更多";
});
async function reload(): Promise<void> {
isRefreshing.value = true;
try {
page.index = 0;
pageResult.total = 0;
pageResult.list = [];
isNoMore.value = false;
await load();
} finally {
isRefreshing.value = false;
}
}
async function load(): Promise<void> {
if (isLoading.value || isNoMore.value) {
return;
}
const token = loadToken + 1;
loadToken = token;
isLoading.value = true;
try {
const result = await PointAPI.ruleList(buildRequest(), buildListOptions());
if (token !== loadToken) {
return;
}
const list = result.list || [];
pageResult.list = pageResult.list.concat(list);
pageResult.total = result.total || 0;
page.index += 1;
isNoMore.value = pageResult.list.length >= pageResult.total || list.length < page.size;
} catch {
Toast.error("积分规则加载失败");
} finally {
if (token === loadToken) {
isLoading.value = false;
}
}
}
function buildRequest(): Page<PointRule> {
return {
index: page.index,
size: page.size,
equalsExample: statusFilter.value === "ALL" ? undefined : {
status: statusFilter.value as PointRuleStatus
}
};
}
function buildListOptions(): { allStores?: boolean; storeId?: string } | undefined {
if (!isGlobalManager.value) {
return undefined;
}
return selectedStoreId.value === allStoresValue ? { allStores: true } : { storeId: selectedStoreId.value };
}
// ---------- 事件名称 ----------
const eventList = ref<EventItem[]>([]);
const eventNameMap = computed(() => new Map(eventList.value.filter(item => item.id).map(item => [item.id as string, item.name])));
async function loadEventList(): Promise<void> {
try {
const options = buildListOptions();
const result = await EventAPI.list({ index: 0, size: 1000 }, options);
eventList.value = result.list || [];
} catch {
eventList.value = [];
}
}
// ---------- 展示与导航 ----------
function getRuleDescription(rule: PointRule): string {
const eventName = rule.eventId ? eventNameMap.value.get(rule.eventId) || `事件 ${rule.eventId}` : "未绑定事件";
const triggerText = getTriggerText(rule);
const pointText = rule.pointMode === "SOURCE_VALUE" ? "使用登记时填写的积分" : `${rule.pointValue ?? 0} 分`;
return `${eventName}|${triggerText}|${pointText}`;
}
function getRuleNote(rule: PointRule): string {
const priority = `优先级 ${rule.priority ?? 0}`;
return rule.exclusive ? `${priority}|命中后停止匹配` : priority;
}
function getTriggerText(rule: PointRule): string {
const map: Record<string, string> = {
EVERY_TIME: "每次登记",
FIRST_TIME: "首次登记",
AFTER_COUNT_EVERY_TIME: `达到 ${rule.conditionJson?.threshold ?? 0} 次后每次登记`,
SOURCE_VALUE: "登记时自定义积分"
};
return map[rule.triggerType || ""] || "未知触发类型";
}
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";
}
async function toCreate(): Promise<void> {
await router.push({
name: "PointRuleCreate",
query: selectedStoreId.value === allStoresValue ? undefined : { storeId: selectedStoreId.value }
});
}
async function toEdit(rule: PointRule): Promise<void> {
if (!canUpdate.value || !rule.id) {
return;
}
await router.push(`/management/points/rules/${rule.id}/edit`);
}
async function preparePage(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
selectedStoreId.value = allStoresValue;
} else {
const store = await currentStoreStore.loadLoginStore();
selectedStoreId.value = store?.id || "";
}
await loadEventList();
await reload();
}
onMounted(async () => {
try {
await preparePage();
} catch {
Toast.error("积分规则页面加载失败");
}
});
</script>
<style scoped lang="less">
.point-rule-list {
padding: calc(var(--app-nav-height) + 4rem) 0 6rem;
.filter-menu {
top: var(--app-nav-height);
left: 0;
right: 0;
z-index: 20;
position: fixed;
background: var(--td-bg-color-container);
}
.rule-list {
margin: 0 1rem;
.rule-item {
.priority {
width: 2rem;
height: 2rem;
display: flex;
color: #2563eb;
background: #dbeafe;
border-radius: .75rem;
align-items: center;
font-size: .875rem;
justify-content: center;
}
}
}
.empty {
color: #52607a;
padding: 2rem 1rem;
text-align: center;
}
.create {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
.point-rule-list {
.empty {
color: #94a3b8;
}
.rule-list .priority {
color: #bfdbfe;
background: rgba(30, 64, 175, .45);
}
}
}
</style>