Files
gao-customer-h5/src/pages/event/EventRecordDetail.vue
T
2026-09-01 01:14:54 +08:00

441 lines
13 KiB
Vue

<template>
<div v-can="permissionCodes.eventRecord.read" class="event-record-detail">
<customer-profile-card
v-entrance-transition
v-if="record?.customer"
class="customer-profile"
:customer="record.customer"
@click="toCustomerDetail"
/>
<t-cell-group v-entrance-transition="70" v-if="record" title="登记信息" theme="card">
<t-cell
title="登记事件"
:note="record.event?.name || '未知事件'"
:arrow="canReadEvent && !!(record.event?.id || record.eventId)"
:hover="canReadEvent && !!(record.event?.id || record.eventId)"
@click="toEventDetail"
/>
<t-cell title="业务类型" :note="getEventBizTypeText(record.event?.bizType)" />
<t-cell v-if="record.numberValue !== undefined && record.numberValue !== null" title="事件数值" :note="getNumberValueText(record)" />
<t-cell v-if="record.pointValue !== undefined && record.pointValue !== null" title="提交积分值" :note="getPointValueText(record.pointValue, pointLedgerChangeType)" />
<t-cell v-if="record.earnedPointValue !== undefined && record.earnedPointValue !== null" title="本次积分变化" :note="getPointValueText(record.earnedPointValue, pointLedgerChangeType)" />
<t-cell title="登记时间">
<template #note>
<pass-time-label :timestamp="record.registeredAt" underline />
</template>
</t-cell>
<t-cell title="登记人" :note="Text.display(record.operator?.nick || record.operator?.name)" />
<t-cell title="备注" :note="Text.display(record.remark)" />
</t-cell-group>
<t-cell-group
v-if="record?.verificationStatus && record.verificationStatus !== 'NOT_REQUIRED'"
title="核销信息"
theme="card"
v-entrance-transition="140"
>
<t-cell title="核销状态" :note="getVerificationStatusText(record.verificationStatus)" />
<t-cell v-if="record.verificationStatus === 'VERIFIED'" title="核销人" :note="Text.display(record.verifier?.nick || record.verifier?.name)" />
<t-cell v-if="record.verificationStatus === 'VERIFIED'" title="核销时间">
<template #note>
<pass-time-label :timestamp="record.verifiedAt" underline />
</template>
</t-cell>
</t-cell-group>
<card-container
v-if="recordOptionEvent"
description="登记选项"
v-entrance-transition="210"
>
<event-option-selector
class="option-selector"
:model-value="recordOptionSelectionMap"
:event-list="[recordOptionEvent]"
:selected-event-id-list="recordOptionEventIdList"
readonly
/>
</card-container>
<t-button
v-if="canVerifyRecord && record?.verificationStatus === 'PENDING'"
class="verify-button cell-button bg-orange white"
variant="text"
:loading="isVerifying"
@click="confirmVerifyRecord"
v-entrance-transition="280"
>
核销登记记录
</t-button>
<div v-if="record" class="photo-wrapper" v-entrance-transition="350">
<photo-scroller
:model-value="recordPhotoList"
:photo-items="recordPhotoItemList"
:show-viewer-actions="canUpdateRecord"
:show-add="false"
:show-remove="false"
:submit="submitPhotoItems"
title="登记照片"
/>
</div>
<t-button
v-if="record && canDeleteRecord"
class="delete-button cell-button"
theme="danger"
:loading="isDeleting"
:disabled="isDeleting"
@click="confirmRemoveRecord"
v-entrance-transition="420"
content="删除登记记录"
/>
<t-empty v-if="!record" :description="isLoading ? '正在加载登记事件记录' : '登记事件记录不存在'" />
</div>
<event-record-verify-dialog
v-model:visible="verifyDialogVisible"
:record="record"
:can-update="canUpdateRecord"
:submit="submitVerifyRecord"
@verified="handleVerifySuccess"
/>
</template>
<script setup lang="ts">
import { DialogPlugin } from "tdesign-mobile-vue";
import { PassTimeLabel, Text } from "timi-web";
import { useUserStore, type Attachment } from "timi-web-api";
import EventRecordAPI from "@/api/EventRecordAPI";
import CardContainer from "@/components/CardContainer.vue";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import EventOptionSelector from "@/components/EventOptionSelector.vue";
import EventRecordVerifyDialog from "@/components/EventRecordVerifyDialog.vue";
import PhotoScroller from "@/components/PhotoScroller.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventRecord } from "@/types/EventRecord";
import type { PhotoItem } from "@/types/Photo";
import type { PointLedgerChangeType } from "@/types/Point";
import { getAttachmentReadURL } from "@/utils/attachment";
import { buildEventOptionSelectionMap } from "@/utils/eventOption";
import { getEventBizTypeText, isPackagePurchaseEvent } from "@/utils/event";
import { getErrorMessage } from "@/utils/error";
import { formatEventNumberValue } from "@/utils/eventValue";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "EventRecordDetail"
});
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
// ---------- 记录数据 ----------
const record = ref<EventRecord>();
const isLoading = ref(false);
const isVerifying = ref(false);
const isDeleting = ref(false);
const verifyDialogVisible = ref(false);
const isPackagePurchaseRecord = computed(() => isPackagePurchaseEvent(record.value?.event));
const canUpdateRecord = computed(() => !!record.value && !isPackagePurchaseRecord.value && userStore.hasPermissions(permissionCodes.eventRecord.update));
const canVerifyRecord = computed(() => !!record.value && !isPackagePurchaseRecord.value && userStore.hasPermissions(permissionCodes.eventRecord.verify));
const canDeleteRecord = computed(() => !!record.value && !isPackagePurchaseRecord.value && userStore.hasPermissions(permissionCodes.eventRecord.delete));
const canReadEvent = computed(() => userStore.hasPermissions(permissionCodes.event.read));
const recordPhotoItemList = computed(() => (record.value?.attachmentList || [])
.filter(item => !!item.id)
.map(item => ({
attachment: item,
attachType: item.attachType || "PHOTO",
previewUrl: getAttachmentReadURL(item.id || ""),
url: getAttachmentReadURL(item.id || "", 64)
})));
const recordPhotoList = computed(() => recordPhotoItemList.value.map(item => item.url));
async function submitPhotoItems(photoItems: PhotoItem[]): Promise<void> {
const currentRecord = record.value;
if (!currentRecord?.id || isPackagePurchaseRecord.value) {
throw new Error("登记记录不存在");
}
const attachmentList = buildPhotoAttachmentList(photoItems);
await EventRecordAPI.update({
id: currentRecord.id,
remark: currentRecord.remark || "",
attachmentList
});
record.value = {
...currentRecord,
attachmentList
};
}
function buildPhotoAttachmentList(photoItems: PhotoItem[]): Attachment[] {
return photoItems.flatMap(item => {
if (item.attachment) {
return [{
...item.attachment,
attachType: item.attachType || item.attachment.attachType || "PHOTO"
}];
}
if (item.tempFileId) {
return [{
attachType: item.attachType || "PHOTO",
tempFileId: item.tempFileId
}];
}
return [];
});
}
function getNumberValueText(value: EventRecord): string {
return formatEventNumberValue(value.event, value.numberValue);
}
function getVerificationStatusText(status?: EventRecord["verificationStatus"]): string {
if (status === "PENDING") {
return "待核销";
}
if (status === "VERIFIED") {
return "已核销";
}
if (status === "NOT_REQUIRED") {
return "无需核销";
}
return "未知";
}
const pointLedgerChangeType = computed(() => record.value?.changeType || parsePointLedgerChangeType(route.query.changeType));
const recordOptionEvent = computed(() => {
const event = record.value?.event;
return event?.id && event.optionGroupList?.some(group => group.optionList?.length) ? event : undefined;
});
const recordOptionEventIdList = computed(() => recordOptionEvent.value?.id ? [String(recordOptionEvent.value.id)] : []);
const recordOptionSelectionMap = computed(() => buildEventOptionSelectionMap(
recordOptionEventIdList.value[0] || "",
record.value?.selectedOptionGroupList
));
function getPointValueText(value?: number | null, changeType?: PointLedgerChangeType): string {
if (value === undefined || value === null) {
return "-";
}
if (changeType === "CONSUME") {
return `消耗 ${Math.abs(value)} 分`;
}
if (changeType) {
return `获得 ${Math.abs(value)} 分`;
}
return `${value} 分`;
}
function parsePointLedgerChangeType(value: unknown): PointLedgerChangeType | undefined {
const text = Array.isArray(value) ? String(value[0] || "") : String(value || "");
return ["EARN", "CONSUME", "REVOKE", "ADJUST"].includes(text) ? text as PointLedgerChangeType : undefined;
}
// ---------- 页面数据 ----------
async function loadRecord(): Promise<void> {
const recordId = String(route.params.id || "");
if (!recordId) {
record.value = undefined;
return;
}
isLoading.value = true;
try {
record.value = await EventRecordAPI.detail(recordId);
} catch (err) {
record.value = undefined;
console.warn("登记事件记录加载失败", err);
} finally {
isLoading.value = false;
}
}
async function confirmVerifyRecord(): Promise<void> {
const currentRecord = record.value;
if (!currentRecord?.id || currentRecord.verificationStatus !== "PENDING" || isVerifying.value) {
return;
}
verifyDialogVisible.value = true;
}
async function submitVerifyRecord(record: EventRecord, attachmentList: Attachment[]): Promise<void> {
const id = record.id;
if (!id || isPackagePurchaseRecord.value) {
throw new Error("登记事件记录不存在");
}
isVerifying.value = true;
try {
if (canUpdateRecord.value) {
await EventRecordAPI.update({
id,
remark: record.remark || "",
attachmentList
});
}
await EventRecordAPI.verify(id);
} finally {
isVerifying.value = false;
}
}
async function handleVerifySuccess(): Promise<void> {
verifyDialogVisible.value = false;
if (record.value?.id) {
record.value = await EventRecordAPI.detail(record.value.id);
}
}
// ---------- 删除 ----------
function confirmRemoveRecord(): void {
const currentRecord = record.value;
if (!currentRecord?.id || !canDeleteRecord.value || isDeleting.value) {
return;
}
const customerName = currentRecord.customer?.name || "未知客户";
const eventName = currentRecord.event?.name || "未知事件";
const recordId = currentRecord.id;
const dialog = DialogPlugin.confirm({
title: "删除登记记录",
content: `确定删除 ${customerName} 的 ${eventName} 记录吗?删除后无法恢复。`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await removeRecord(recordId);
}
});
}
async function removeRecord(id: string): Promise<void> {
if (isDeleting.value || isPackagePurchaseRecord.value) {
return;
}
isDeleting.value = true;
try {
await EventRecordAPI.remove(id);
Message.success("登记记录已删除");
await router.replace({ name: "EventRecordList" });
} catch (error) {
const message = getErrorMessage(error, "登记记录删除失败");
if (message.includes("积分余额不足") || message.includes("余额不足")) {
Message.warning("积分余额不足,请先人工调整积分后再删除登记记录");
return;
}
Message.error(message);
} finally {
isDeleting.value = false;
}
}
// ---------- 导航 ----------
async function toEdit(): Promise<void> {
const recordId = String(route.params.id || "");
if (!recordId || !canUpdateRecord.value) {
return;
}
await router.push(`/management/registration-event-records/${recordId}/edit`);
}
async function toCustomerDetail(): Promise<void> {
const customerId = record.value?.customer?.id;
if (!customerId) {
return;
}
await router.push(`/management/customers/${customerId}`);
}
async function toEventDetail(): Promise<void> {
const eventId = record.value?.event?.id || record.value?.eventId;
if (!canReadEvent.value || !eventId) {
return;
}
await router.push({
name: "EventDetail",
params: { id: eventId },
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
});
}
function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0]
: route.query.transitionDepth;
const depth = Number(transitionDepth);
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
return depth;
}
return Number(route.meta.depth ?? 0);
}
watch(() => route.params.id, async () => {
await loadRecord();
}, { immediate: true });
function updateNavBarAction(): void {
navBarStore.clearRight();
if (canUpdateRecord.value) {
navBarStore.setRightText("编辑", "var(--td-brand-color)", toEdit);
}
}
watch(canUpdateRecord, updateNavBarAction, { immediate: true });
onUnmounted(() => {
navBarStore.clearRight();
});
</script>
<style scoped lang="less">
.event-record-detail {
gap: 1rem;
min-height: 100vh;
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
display: flex;
flex-direction: column;
.customer-profile {
margin: 0 1rem;
}
.photo-wrapper {
width: calc(100% - 2rem);
margin: 0 1rem;
padding: 1rem;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.4rem;
}
.verify-button {
width: calc(100% - 2rem);
margin: 0 1rem;
}
.option-selector {
padding-left: 1rem;
}
}
:global(html.theme-dark) {
.registration-record-detail {
.photo-panel {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}
}
}
</style>