Files
gao-customer-h5/src/pages/event/EventList.vue
T
2026-08-22 17:50:31 +08:00

354 lines
9.1 KiB
Vue

<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-cell
class="item"
v-for="item in visibleEventList"
:key="item.id"
:arrow="canUpdateEventItem"
:hover="canUpdateEventItem"
@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>
</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="getValueDefinitionText(item)" />
</div>
</template>
</t-cell>
</t-cell-group>
<div v-if="!visibleEventList.length" class="empty-block" v-text="emptyText" />
<div v-if="visibleEventList.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
</t-pull-down-refresh>
</div>
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
</template>
<script setup lang="ts">
import { Message } from "@/utils/message";
import { type Page, type PageResult, type Role, useUserStore } from "timi-web";
import EventAPI from "@/api/EventAPI";
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 { 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 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 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);
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" };
function toEventItem(item: EventItem): void {
if (!canUpdateEventItem.value) {
return;
}
if (!item.id) {
return;
}
router.push(`/management/registration-events/${item.id}/edit`);
}
// ---------- 展示文案 ----------
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 {
.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>