Files
gao-customer-h5/src/pages/event/EventList.vue
T
2026-09-04 23:07:59 +08:00

425 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="{ value: [permissionCodes.event.read, permissionCodes.user.read], mode: 'and' }"
class="registration-event-list"
>
<t-pull-down-refresh
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="fetch"
>
<t-cell-group theme="card">
<t-swipe-cell
v-entrance-transition="index * 30"
v-for="(item, index) in visibleEventList"
:key="item.id"
class="item"
>
<t-cell
class="cell"
:arrow="!!item.id"
:hover="!!item.id"
@click="toEventItem(item)"
>
<template #title>
<div class="header">
<div class="color" :style="{ background: item.color || defaultColor }" />
<div class="title" v-text="item.name || '未命名事件'" />
<t-tag
class="status"
:theme="getStatusTag(item).theme"
variant="outline"
:content="getStatusTag(item).text"
/>
<t-tag v-if="item.requirePhoto" theme="warning" variant="outline">需拍照</t-tag>
<t-tag v-if="item.requireVerification" theme="warning" variant="outline">需核销</t-tag>
<t-tag v-if="item.bizType === 'PACKAGE_PURCHASE'" theme="primary" variant="outline">套餐购买</t-tag>
<t-tag v-if="item.bizType === 'LEGACY_CONSUMPTION'" theme="default" variant="outline">历史消费</t-tag>
</div>
</template>
<template #description>
<div class="description">
<div class="line">
<span>可登记角色</span>
<t-tag
v-for="roleCode in item.roleCodeList"
:key="roleCode"
theme="primary"
variant="light-outline"
:content="getRoleName(roleCode)"
/>
<span v-if="!(item.roleCodeList || []).length" class="muted" v-text="'未配置'" />
</div>
<div class="line" v-text="getLimitText(item)" />
<div class="line" v-text="`${getEventBizTypeText(item.bizType)}|${getValueDefinitionText(item)}`" />
<div v-if="item.bizType === 'PACKAGE_PURCHASE'" class="line" v-text="`绑定套餐:${item.packageConfig?.name || '未配置'}`" />
</div>
</template>
</t-cell>
<template v-if="canDeleteEvent && item.status !== 'DELETED'" #right>
<t-button
class="delete"
theme="danger"
:loading="isRemoving && removingEventId === item.id"
@click.stop="openRemoveDialog(item)"
>
删除
</t-button>
</template>
</t-swipe-cell>
</t-cell-group>
<div v-if="!visibleEventList.length" class="empty-block" v-text="emptyText" />
<div v-if="visibleEventList.length" class="center-tips" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
</t-pull-down-refresh>
</div>
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
<event-remove-dialog
v-model:visible="removeDialogVisible"
:event="removeTarget"
:loading="isRemoving"
@confirm="removeEvent"
/>
</template>
<script setup lang="ts">
import { Message } from "@/utils/message";
import { type Page, type PageResult, type Role, useUserStore } from "timi-web-api";
import EventAPI from "@/api/EventAPI";
import EventRemoveDialog from "@/pages/event/component/EventRemoveDialog.vue";
import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem, EventLimitType, EventStatus } from "@/types/EventItem";
import type { NavBarAction } from "@/types/NavBarAction";
import { getEventBizTypeText, isRetiredEvent } from "@/utils/event";
import { getEventValueUnitText } from "@/utils/eventValue";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({
name: "eventList"
});
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const eventListOptions = computed(() => ({ storeId: currentStoreStore.currentStoreId }));
const canCreateEvent = computed(() => userStore.hasPermissions(permissionCodes.event.create));
const canUpdateEventItem = computed(() => userStore.hasPermissions(permissionCodes.event.update));
const canDeleteEvent = computed(() => userStore.hasPermissions(permissionCodes.event.delete));
const canShowActionMenu = computed(() => canCreateEvent.value || canUpdateEventItem.value);
const actionList = computed<NavBarAction[]>(() => {
const list: NavBarAction[] = [];
if (canCreateEvent.value) {
list.push({
action: toCreate,
theme: "primary",
icon: "add",
key: "create",
text: "新增登记事件"
});
}
if (canUpdateEventItem.value) {
list.push({
action: toSort,
icon: "order",
key: "sort",
text: "排序"
});
}
return list;
});
// ---------- 列表数据 ----------
const defaultColor = "#2f88ff";
const roleOptions = ref<Role[]>([]);
const isRefreshing = ref(false);
const isLoading = ref(false);
const isNoMore = ref(false);
const isRemoving = ref(false);
const removeDialogVisible = ref(false);
const removeTarget = ref<EventItem>();
const removingEventId = ref("");
const page = reactive<Page<EventItem>>({
index: 0,
size: 15,
orderMap: {
sort: "ASC",
createdAt: "ASC"
}
});
const pageResult = reactive<PageResult<EventItem>>({
total: 0,
list: []
});
const visibleEventList = computed(() => pageResult.list.filter(item => !isRetiredEvent(item)));
const emptyText = computed(() => {
if (isLoading.value) {
return "正在加载登记事件";
}
return "暂无登记事件";
});
type StatusTag = {
text: string;
theme: "default" | "primary" | "danger";
};
const fixedStatusTagMap: Partial<Record<EventStatus, StatusTag>> = {
DRAFT: { text: "草稿", theme: "default" },
INACTIVE: { text: "不可登记", theme: "default" },
DELETED: { text: "已删除", theme: "default" }
};
const activeStatusTag: StatusTag = { text: "可登记", theme: "primary" };
const notEffectiveStatusTag: StatusTag = { text: "未生效", theme: "default" };
const expiredStatusTag: StatusTag = { text: "已过期", theme: "danger" };
async function toEventItem(item: EventItem): Promise<void> {
if (!item.id) {
return;
}
await router.push(`/management/registration-events/${item.id}`);
}
// ---------- 删除 ----------
function openRemoveDialog(eventItem: EventItem): void {
const eventId = eventItem.id;
if (!eventId || eventItem.status === "DELETED" || isRemoving.value) {
return;
}
removeTarget.value = eventItem;
removeDialogVisible.value = true;
}
async function removeEvent(eventItem: EventItem): Promise<void> {
const eventId = eventItem.id;
if (!eventId || eventItem.status === "DELETED" || isRemoving.value) {
return;
}
isRemoving.value = true;
removingEventId.value = eventId;
removeDialogVisible.value = false;
removeTarget.value = undefined;
try {
await EventAPI.remove(eventId);
pageResult.list = pageResult.list.filter(item => item.id !== eventId);
pageResult.total = Math.max(0, pageResult.total - 1);
isNoMore.value = pageResult.list.length >= pageResult.total;
Message.success("登记事件已删除");
} catch {
Message.error("登记事件删除失败");
} finally {
isRemoving.value = false;
removingEventId.value = "";
}
}
// ---------- 展示文案 ----------
function getStatusTag(item: EventItem): StatusTag {
const status = item.status || "ACTIVE";
const fixedStatusTag = fixedStatusTagMap[status];
if (fixedStatusTag) {
return fixedStatusTag;
}
const now = Date.now();
if (item.beginAt !== undefined && now < item.beginAt) {
return notEffectiveStatusTag;
}
if (item.endAt !== undefined && item.endAt < now) {
return expiredStatusTag;
}
return activeStatusTag;
}
function getLimitText(item: EventItem): string {
const limitValue = resolveLimitValue(item.limitValue);
const limitTextMap: Record<EventLimitType, string> = {
NONE: "不限次数",
DAILY_HALF_DAY: `上午/下午各 ${limitValue} 次`,
INTERVAL_HOUR: `每隔 ${limitValue} 小时`,
NATURAL_DAY: `每个自然日 ${limitValue} 次`,
NATURAL_MONTH: `每个自然月 ${limitValue} 次`,
TOTAL_LIMIT: `事件总上限 ${limitValue} 次`,
CUSTOMER_TOTAL_LIMIT: `单客户总上限 ${limitValue} 次`
};
return limitTextMap[item.limitType || "NONE"];
}
function getValueDefinitionText(item: EventItem): string {
if (item.valueType !== "NUMBER") {
return "不携带数值";
}
const unitText = getEventValueUnitText(item.valueUnit);
return `整数数值|${unitText}${item.requireValue ? "|登记必填" : "|登记可选"}`;
}
function resolveLimitValue(value?: number): number {
if (value !== undefined && Number.isFinite(value) && 0 < value) {
return Math.floor(value);
}
return 1;
}
function getRoleName(code: string): string {
const role = roleOptions.value.find(item => item.code === code);
return role?.name || code;
}
// ---------- 页面数据 ----------
async function loadStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
return;
}
await currentStoreStore.loadLoginStore();
}
async function fetch(): Promise<void> {
if (isLoading.value || isNoMore.value) {
return;
}
isLoading.value = true;
try {
const result = await EventAPI.list(page, eventListOptions.value);
pageResult.total = result.total;
pageResult.list = pageResult.list.concat(result.list || []);
page.index += 1;
isNoMore.value = (result.list || []).length < page.size || pageResult.total <= pageResult.list.length;
} finally {
isLoading.value = false;
}
}
async function refresh(): Promise<void> {
isRefreshing.value = true;
try {
page.index = 0;
pageResult.total = 0;
pageResult.list = [];
isNoMore.value = false;
await fetch();
} finally {
isRefreshing.value = false;
}
}
async function loadData(): Promise<void> {
await loadStoreScope();
const [eventResult, roles] = await Promise.all([
EventAPI.list(page, eventListOptions.value),
ManagedUserAPI.roleList()
]);
roleOptions.value = roles || [];
pageResult.total = eventResult.total;
pageResult.list = eventResult.list || [];
page.index = 1;
isNoMore.value = pageResult.list.length >= pageResult.total || pageResult.list.length < page.size;
}
async function toCreate(): Promise<void> {
await router.push({
path: "/management/registration-events/create",
query: currentStoreStore.currentStoreId ? { storeId: currentStoreStore.currentStoreId } : undefined
});
}
async function toSort(): Promise<void> {
await router.push("/management/registration-events/sort");
}
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setCustomRightVisible(canShowActionMenu.value);
try {
await loadData();
} catch {
Message.error("登记事件加载失败");
}
});
onUnmounted(() => {
navBarStore.clearRight();
});
</script>
<style scoped lang="less">
.registration-event-list {
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
.item {
.delete {
height: 100%;
padding: 0 1rem;
border-radius: 0;
}
.header {
gap: .5rem;
width: 100%;
display: flex;
margin-top: .25rem;
.color {
width: 1.5rem;
height: 1.5rem;
border-radius: .4rem;
}
}
.description {
margin-top: .5rem;
.line {
gap: .5rem;
display: flex;
align-items: center;
.muted {
color: #52607a;
}
}
}
}
.empty-block {
color: #52607a;
padding: 2rem 1rem;
text-align: center;
}
}
:global(html.theme-dark) {
.registration-event-list {
.item {
background: rgba(15, 23, 42, .88);
border-color: rgba(148, 163, 184, .16);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .28);
.content {
p,
.meta span {
color: #94a3b8;
}
}
}
.empty-block {
color: #94a3b8;
}
}
}
</style>