441 lines
13 KiB
Vue
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>
|
|
|