Files
gao-customer-h5/src/pages/management/RegistrationEventRecordList.vue
T
2026-08-03 19:20:51 +08:00

651 lines
16 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 ref="pageRef" v-can="gaoPermissionCodes.registrationEventRecord.read" class="registration-record-list">
<div class="search-bar">
<t-input
v-model.trim="keyword"
class="search-input"
type="search"
borderless
placeholder="搜索客户、编码、事件、备注、登记人"
/>
<div class="filter-line">
<t-dropdown-menu class="filter-menu">
<t-dropdown-item
v-model="dateFilterType"
:label="dateFilterLabel"
:options="dateOptions"
@change="selectDateFilter"
/>
<t-dropdown-item
v-model="actionFilter"
:label="eventFilterLabel"
options-columns="2"
:options="actionOptions"
multiple
/>
</t-dropdown-menu>
</div>
</div>
<t-cell-group v-if="isCustomRange" class="time-range" theme="card">
<t-cell
title="开始时间"
:note="dateRange[0] || '-'"
@click="startVisible = true"
/>
<t-cell
title="结束时间"
:note="dateRange[1] || '-'"
@click="endVisible = true"
/>
</t-cell-group>
<div class="summary">
<div v-text="summaryText" />
<div v-text="summaryBehaviorText" />
<div v-text="summaryTotalText" />
</div>
<t-cell-group
v-if="recordList.length"
class="record-list"
theme="card"
>
<t-swipe-cell
v-for="item in recordList"
:key="item.id"
class="item"
>
<t-cell
class="cell"
:title="getRecordRegistrationEventName(item)"
:description="getRecordCustomerName(item)"
:note="formatTime(item.registeredAt)"
arrow
hover
@click="goDetail(item.id || '')"
>
<template #leftIcon>
<t-image
v-if="getRecordPhotoURL(item)"
:src="getRecordPhotoURL(item)"
:alt="getRecordRegistrationEventName(item)"
fit="cover"
class="photo"
/>
<div v-else class="photo fallback" v-text="getRecordCustomerInitial(item)" />
</template>
</t-cell>
<template v-if="canDeleteRecord" #right>
<t-button
class="delete"
theme="danger"
:loading="deletingId === item.id"
@click.stop="confirmRemoveRecord(item)"
>
删除
</t-button>
</template>
</t-swipe-cell>
</t-cell-group>
<div v-else class="empty-block" v-text="loading ? '正在加载登记事件记录' : '暂无登记事件记录'" />
<div v-if="recordList.length" class="load-state" v-text="loadStateText" />
</div>
<t-button
v-can="gaoPermissionCodes.registrationEventRecord.export"
class="export"
theme="primary"
:loading="exporting"
@click="exportExcel"
>
<template #icon>
<t-icon name="download" />
</template>
导出 Excel
</t-button>
<t-popup v-model="startVisible" placement="bottom">
<t-date-time-picker
:default-value="dateRange[0]"
format="YYYY-MM-DD HH:mm"
:mode="['date', 'minute']"
title="开始时间"
v-model="dateRange[0]"
@confirm="startVisible = false"
/>
</t-popup>
<t-popup v-model="endVisible" placement="bottom">
<t-date-time-picker
:default-value="dateRange[1]"
:mode="['date', 'minute']"
format="YYYY-MM-DD HH:mm"
title="结束时间"
v-model="dateRange[1]"
@confirm="endVisible = false"
/>
</t-popup>
</template>
<script setup lang="ts">
import { DialogPlugin, Toast, type DropdownValue } from "tdesign-mobile-vue";
import { AttachmentAPI, Scroller, Time, Toolkit, useUserStore } from "timi-web";
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
import RegistrationEventRecordAPI from "@/api/RegistrationEventRecordAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { RegistrationEvent } from "@/types/RegistrationEvent";
import type { RegistrationEventRecordListItemView, RegistrationEventRecordRangeReq } from "@/types/RegistrationEventRecord";
import { gaoPermissionCodes } from "@/utils/gaoPermission";
type DateFilterType = "today" | "yesterday" | "beforeYesterday" | "lastWeek" | "all" | "custom";
defineOptions({
name: "RegistrationEventRecordList"
});
const pageSize = 20;
const scrollListenerName = "RegistrationEventRecordList";
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const dateOptions: Array<{ label: string; value: DateFilterType }> = [
{ label: "今天", value: "today" },
{ label: "昨天", value: "yesterday" },
{ label: "前天", value: "beforeYesterday" },
{ label: "上周", value: "lastWeek" },
{ label: "全部", value: "all" },
{ label: "自定义范围", value: "custom" }
];
// ---------- 筛选 ----------
const keyword = ref("");
const dateFilterType = ref<DateFilterType>("today");
const dateRange = ref(buildPresetRange("today"));
const actionFilter = ref<string[]>([]);
const registrationEventList = ref<RegistrationEvent[]>([]);
const startVisible = ref(false);
const endVisible = ref(false);
const pageRef = ref<HTMLElement>();
let keywordTimer: number | undefined;
let initialized = false;
function getPageScrollElement(): HTMLElement | null {
return pageRef.value?.closest(".pages") || null;
}
const canCreateRecord = computed(() => userStore.hasPermissions(gaoPermissionCodes.registrationEventRecord.create));
const isCustomRange = computed(() => dateFilterType.value === "custom");
const dateFilterLabel = computed(() => dateOptions.find(item => item.value === dateFilterType.value)?.label || "今天");
const actionOptions = computed(() => registrationEventList.value.map(item => ({
label: item.name || "未命名事件",
value: item.id || ""
})).filter(item => item.value));
const eventFilterLabel = computed(() => {
if (!actionFilter.value.length || actionFilter.value.length === registrationEventList.value.length) {
return "全部事件";
}
return registrationEventList.value
.filter(item => item.id && actionFilter.value.includes(item.id))
.map(item => item.name || "未命名事件")
.join("、");
});
const summaryBehaviorText = computed(() => `事件:${eventFilterLabel.value}`);
const canDeleteRecord = computed(() => userStore.hasPermissions(gaoPermissionCodes.registrationEventRecord.delete));
const summaryText = computed(() => {
if (!isCustomRange.value) {
const current = dateOptions.find(item => item.value === dateFilterType.value);
return `范围:${current?.label || "今天"}|${dateRange.value[0] || "不限"}`;
}
return `范围:${dateRange.value[0] || "-"} ~ ${dateRange.value[1] || "-"}`;
});
watch(keyword, () => {
if (!initialized) {
return;
}
window.clearTimeout(keywordTimer);
keywordTimer = window.setTimeout(async () => {
await reloadRecords();
}, 300);
});
watch(actionFilter, async () => {
if (!initialized) {
return;
}
await reloadRecords();
});
watch(dateRange, async (value) => {
if (!initialized || dateFilterType.value !== "custom") {
return;
}
if (value[0] && value[1] && value[1] < value[0]) {
dateRange.value = [value[1], value[0]];
return;
}
await reloadRecords();
}, { deep: true });
// ---------- 列表分页 ----------
const pageIndex = ref(0);
const totalCount = ref(0);
const recordList = ref<RegistrationEventRecordListItemView[]>([]);
const loading = ref(false);
const noMore = ref(false);
let loadToken = 0;
const summaryTotalText = computed(() => `共 ${totalCount.value} 条数据,已加载 ${recordList.value.length} 条`);
const loadStateText = computed(() => {
if (loading.value) {
return "正在加载更多";
}
return noMore.value ? "没有更多记录" : "上拉加载更多";
});
async function reloadRecords(): Promise<void> {
pageIndex.value = 0;
totalCount.value = 0;
recordList.value = [];
noMore.value = false;
await loadRecords();
}
async function loadRecords(): Promise<void> {
if (loading.value || noMore.value) {
return;
}
const token = loadToken + 1;
loadToken = token;
loading.value = true;
try {
const result = await RegistrationEventRecordAPI.rangePage({
index: pageIndex.value,
size: pageSize,
equalsExample: buildRangeReq()
});
if (token !== loadToken) {
return;
}
recordList.value = recordList.value.concat(result.list || []);
totalCount.value = result.total || 0;
pageIndex.value += 1;
noMore.value = recordList.value.length >= totalCount.value || (result.list || []).length < pageSize;
} finally {
if (token === loadToken) {
loading.value = false;
}
}
}
// ---------- 操作 ----------
const exporting = ref(false);
const deletingId = ref("");
async function loadActions(): Promise<void> {
registrationEventList.value = await RegistrationEventAPI.enabledList();
actionFilter.value = registrationEventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
}
async function selectDateFilter(type: DropdownValue): Promise<void> {
if (Array.isArray(type)) {
return;
}
dateFilterType.value = String(type) as DateFilterType;
if (dateFilterType.value !== "custom") {
dateRange.value = buildPresetRange(dateFilterType.value);
await reloadRecords();
}
}
async function exportExcel(): Promise<void> {
if (exporting.value) {
return;
}
exporting.value = true;
try {
const blob = await RegistrationEventRecordAPI.rangeExport(buildRangeReq());
Toolkit.downloadBlob(blob, `登记事件记录-${Time.formatDate(new Date())}.xlsx`);
showToast("导出已开始");
} finally {
exporting.value = false;
}
}
async function goDetail(id: string): Promise<void> {
if (!id) {
return;
}
await router.push(`/management/registration-event-records/${id}`);
}
async function goCreate(): Promise<void> {
await router.push("/management/registration-event-records/create");
}
async function confirmRemoveRecord(record: RegistrationEventRecordListItemView): Promise<void> {
if (!record.id) {
return;
}
const recordId = record.id;
const dialog = DialogPlugin.confirm({
title: "删除登记记录",
content: `确定删除 ${getRecordCustomerName(record)} 的 ${getRecordRegistrationEventName(record)} 记录吗?删除后无法恢复。`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await removeRecord(recordId);
}
});
}
async function removeRecord(id: string): Promise<void> {
if (deletingId.value) {
return;
}
deletingId.value = id;
try {
await RegistrationEventRecordAPI.remove(id);
recordList.value = recordList.value.filter(item => item.id !== id);
totalCount.value = Math.max(0, totalCount.value - 1);
showToast("登记记录已删除");
} finally {
deletingId.value = "";
}
}
// ---------- 格式化 ----------
function getRecordRegistrationEventName(record: RegistrationEventRecordListItemView): string {
return record.registrationEventName || "未知事件";
}
function getRecordCustomerName(record: RegistrationEventRecordListItemView): string {
return record.customerName || "未知客户";
}
function getRecordPhotoURL(record: RegistrationEventRecordListItemView): string {
if (!record.customerPhotoAttachmentId) {
return "";
}
return AttachmentAPI.getReadURL(record.customerPhotoAttachmentId);
}
function getRecordCustomerInitial(record: RegistrationEventRecordListItemView): string {
return (record.customerName || "客").slice(0, 1);
}
function formatTime(value?: number): string {
if (!value) {
return "-";
}
const date = Time.toDateObject(value);
if (!date) {
return "-";
}
return Time.formatDateTime(date);
}
function buildRangeReq(): RegistrationEventRecordRangeReq {
const [beginRegisteredAt, endRegisteredAt] = buildUnixRange();
const selectedAllEvent = !actionFilter.value.length || actionFilter.value.length === registrationEventList.value.length;
return {
beginRegisteredAt,
endRegisteredAt,
keyword: keyword.value.trim() || undefined,
registrationEventIdList: selectedAllEvent ? undefined : actionFilter.value
};
}
function buildPresetRange(type: Exclude<DateFilterType, "custom">): [string, string] {
if (type === "all") {
return ["", ""];
}
if (type === "lastWeek") {
const current = new Date();
current.setHours(0, 0, 0, 0);
const day = current.getDay() || 7;
const thisWeekMonday = new Date(current);
thisWeekMonday.setDate(current.getDate() - day + 1);
const lastWeekMonday = new Date(thisWeekMonday);
lastWeekMonday.setDate(thisWeekMonday.getDate() - 7);
const lastWeekSunday = new Date(thisWeekMonday);
lastWeekSunday.setDate(thisWeekMonday.getDate() - 1);
return [`${Time.formatDate(lastWeekMonday)} 00:00`, `${Time.formatDate(lastWeekSunday)} 23:59`];
}
const offsetMap: Record<Exclude<DateFilterType, "all" | "custom" | "lastWeek">, number> = {
today: 0,
yesterday: 1,
beforeYesterday: 2
};
const date = new Date();
date.setHours(0, 0, 0, 0);
date.setDate(date.getDate() - offsetMap[type]);
const value = `${Time.formatDate(date)} 00:00`;
return [value, `${Time.formatDate(date)} 23:59`];
}
function buildUnixRange(): [number, number] {
if (dateFilterType.value === "all") {
return [0, Time.toTimestamp(new Date())];
}
return [
Time.toTimestamp(parseDateTime(dateRange.value[0], false)),
Time.toTimestamp(parseDateTime(dateRange.value[1], true))
];
}
function parseDateTime(value: string, isEnd: boolean): Date {
if (!value) {
return isEnd ? new Date() : new Date(0);
}
const [dateText, timeText = isEnd ? "23:59" : "00:00"] = value.split(" ");
const [year, month, day] = dateText.split("-").map(Number);
const [hour, minute] = timeText.split(":").map(Number);
return new Date(year, month - 1, day, hour || 0, minute || 0, isEnd ? 59 : 0, 0);
}
function showToast(message: string, theme: "success" | "warning" = "success"): void {
Toast({
message,
theme
});
}
onMounted(async () => {
navBarStore.clearRight();
if (canCreateRecord.value) {
navBarStore.setRightText("添加", "var(--td-brand-color)", goCreate);
}
await loadActions();
initialized = true;
await reloadRecords();
Scroller.addBottomListener(scrollListenerName, loadRecords, {
canLoad: () => !loading.value && !noMore.value,
getElement: getPageScrollElement
});
});
onUnmounted(() => {
navBarStore.clearRight();
window.clearTimeout(keywordTimer);
Scroller.removeListener(scrollListenerName);
});
</script>
<style scoped lang="less">
.registration-record-list {
gap: 1rem;
display: flex;
padding: calc(var(--app-nav-height)) 0 6rem 0;
flex-direction: column;
.search-bar {
top: var(--app-nav-height);
left: 0;
right: 0;
z-index: 10;
display: flex;
padding: .75rem 1rem;
position: sticky;
background: #F5F5F8;
box-shadow: var(--td-shadow-1);
flex-direction: column;
.search-input {
--td-input-vertical-padding: 8px;
border: 1px solid rgba(148, 163, 184, .28);
z-index: 1;
background: #f8fbff;
border-radius: 1rem;
}
.filter-menu {
--td-spacer-1: 6px;
width: 100%;
overflow: hidden;
margin-top: -1rem;
padding-top: .8rem;
background: #fff;
border: 1px solid rgba(148, 163, 184, .18);
border-radius: 0 0 1rem 1rem;
&:after {
display: none;
}
:deep(.t-dropdown-menu__item) {
min-width: 0;
}
:deep(.t-dropdown-menu__title) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.summary {
color: #52607a;
font-size: .8125rem;
text-align: center;
}
.record-list {
.item {
&:last-child {
:deep(.t-cell:after) {
display: none;
}
}
.cell {
padding: .5rem;
:deep(.t-cell__title) {
flex: 1 1 0;
min-width: 0;
}
:deep(.t-cell__title-text) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.t-cell__note) {
flex: 0 0 auto;
white-space: nowrap;
}
.photo {
width: 3.5rem;
height: 3.5rem;
border-radius: .5rem;
&.fallback {
color: #2563eb;
display: flex;
font-size: 1.25rem;
background: #dbeafe;
align-items: center;
font-weight: 600;
justify-content: center;
}
}
}
.delete {
height: 100%;
padding: 0 1rem;
border-radius: 0;
}
}
}
.empty-block,
.load-state {
color: #52607a;
text-align: center;
}
}
.export {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
z-index: 20;
position: fixed;
box-shadow: 0 -.375rem 1.25rem rgba(15, 23, 42, .08);
backdrop-filter: blur(.75rem);
}
:global(html.theme-dark) {
.registration-record-list {
.search-bar {
background: #10161f;
.search-input {
background: rgba(15, 23, 42, .72);
border-color: rgba(148, 163, 184, .16);
}
.filter-menu {
background: #172033;
border-color: rgba(148, 163, 184, .16);
}
}
.empty-block,
.load-state,
.summary {
color: #94a3b8;
}
}
.bottom-bar {
background: rgba(15, 23, 42, .88);
box-shadow: 0 -.375rem 1.25rem rgba(2, 6, 23, .32);
}
}
</style>