add event record verify photo

This commit is contained in:
Timi
2026-08-30 01:40:51 +08:00
parent e1a3df43a9
commit a8958f51f6
3 changed files with 275 additions and 48 deletions
+197
View File
@@ -0,0 +1,197 @@
<template>
<t-dialog
class="event-record-verify-dialog"
v-model:visible="visible"
title="核销登记记录"
:confirm-btn="{ content: '核销', disabled: isPhotoUploading || isSubmitting }"
cancel-btn="取消"
:before-close="beforeClose"
>
<div class="content">
<div v-text="confirmText" />
<div v-if="canUpdate" class="photo-part">
<photo-scroller
title="核销照片(可选)"
:model-value="photoList"
v-model:photo-items="photoItemList"
:edit-mode="true"
:loading="isPhotoUploading"
:show-remove="false"
:show-viewer-actions="false"
@add="capturePhoto"
/>
</div>
</div>
</t-dialog>
<input
ref="photoInputRef"
class="file-input"
type="file"
accept="image/*"
multiple
@change="handlePhotoSelected"
>
</template>
<script setup lang="ts">
import { AttachmentAPI, type Attachment } from "timi-web-api";
import PhotoScroller from "@/components/PhotoScroller.vue";
import type { EventRecord } from "@/types/EventRecord";
import type { PhotoItem } from "@/types/Photo";
import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error";
import { compressUploadImageList } from "@/utils/imageCompression";
import { Message } from "@/utils/message";
defineOptions({
name: "EventRecordVerifyDialog"
});
interface Props {
canUpdate?: boolean;
record?: EventRecord;
submit: (record: EventRecord, attachmentList: Attachment[]) => Promise<void>;
visible?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
canUpdate: false,
visible: false
});
const emit = defineEmits<{
"update:visible": [visible: boolean];
verified: [record: EventRecord];
}>();
const photoInputRef = ref<HTMLInputElement>();
const photoItemList = ref<PhotoItem[]>([]);
const isPhotoUploading = ref(false);
const isSubmitting = ref(false);
const visible = computed({
get: () => props.visible,
set: (value: boolean) => emit("update:visible", value)
});
const photoList = computed<string[]>(() => photoItemList.value.map(item => item.url));
const confirmText = computed(() => `确定核销 ${props.record?.customer?.name || "未知客户"} 的 ${props.record?.event?.name || "未知事件"} 记录吗?`);
watch([() => props.visible, () => props.record?.id], ([isVisible]) => {
if (isVisible) {
photoItemList.value = buildPhotoItemList(props.record?.attachmentList);
}
}, { immediate: true });
function buildPhotoItemList(attachmentList?: Attachment[]): PhotoItem[] {
return (attachmentList || [])
.filter(item => !!item.id)
.map(item => ({
attachment: item,
attachType: item.attachType || "PHOTO",
previewUrl: getAttachmentReadURL(item.id || ""),
url: getAttachmentReadURL(item.id || "", 90)
}));
}
function capturePhoto(): void {
photoInputRef.value?.click();
}
async function handlePhotoSelected(event: Event): Promise<void> {
const input = event.target as HTMLInputElement;
const fileList = Array.from(input.files || []);
if (!fileList.length) {
return;
}
isPhotoUploading.value = true;
try {
const compressedFileList = await compressUploadImageList(fileList);
const tempFileList = await AttachmentAPI.upload(compressedFileList);
for (const item of tempFileList) {
if (!item.id) {
continue;
}
photoItemList.value.push({
attachType: "PHOTO",
tempFileId: item.id,
url: AttachmentAPI.getTempReadURL(item.id)
});
}
} catch (error) {
Message.error(getErrorMessage(error, "登记照片上传失败"));
} finally {
isPhotoUploading.value = false;
input.value = "";
}
}
async function beforeClose(trigger: "confirm" | "cancel" | "overlay" | "close-btn"): Promise<void> {
if (trigger !== "confirm") {
return;
}
if (!props.record?.id) {
Message.warning("登记事件记录不存在");
throw new Error("登记事件记录不存在");
}
if (isPhotoUploading.value || isSubmitting.value) {
Message.warning("照片正在上传,请稍候");
throw new Error("照片正在上传");
}
isSubmitting.value = true;
try {
await props.submit(props.record, buildAttachmentList());
Message.success("登记记录已核销");
emit("verified", props.record);
} catch (error) {
Message.error(getErrorMessage(error, "登记记录核销失败"));
throw error;
} finally {
isSubmitting.value = false;
}
}
function buildAttachmentList(): Attachment[] {
return photoItemList.value.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 [];
});
}
</script>
<style scoped lang="less">
.event-record-verify-dialog {
.content {
text-align: left;
}
.photo-part {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--td-component-border);
}
}
.file-input {
display: none;
}
:global(html.theme-dark) {
.event-record-verify-dialog {
.photo-part {
border-color: var(--td-component-border);
}
}
}
</style>
+29 -24
View File
@@ -56,14 +56,21 @@
</div> </div>
<t-empty v-if="!record" :description="isLoading ? '正在加载登记事件记录' : '登记事件记录不存在'" /> <t-empty v-if="!record" :description="isLoading ? '正在加载登记事件记录' : '登记事件记录不存在'" />
</div> </div>
<event-record-verify-dialog
v-model:visible="verifyDialogVisible"
:record="record"
:can-update="canUpdateRecord"
:submit="submitVerifyRecord"
@verified="handleVerifySuccess"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DialogPlugin } from "tdesign-mobile-vue";
import { PassTimeLabel, Text } from "timi-web"; import { PassTimeLabel, Text } from "timi-web";
import { useUserStore, type Attachment } from "timi-web-api"; import { useUserStore, type Attachment } from "timi-web-api";
import EventRecordAPI from "@/api/EventRecordAPI"; import EventRecordAPI from "@/api/EventRecordAPI";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue"; import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import EventRecordVerifyDialog from "@/components/EventRecordVerifyDialog.vue";
import PhotoScroller from "@/components/PhotoScroller.vue"; import PhotoScroller from "@/components/PhotoScroller.vue";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { EventRecord } from "@/types/EventRecord"; import type { EventRecord } from "@/types/EventRecord";
@@ -71,8 +78,6 @@ import type { PhotoItem } from "@/types/Photo";
import type { PointLedgerChangeType } from "@/types/Point"; import type { PointLedgerChangeType } from "@/types/Point";
import { getAttachmentReadURL } from "@/utils/attachment"; import { getAttachmentReadURL } from "@/utils/attachment";
import { formatEventNumberValue } from "@/utils/eventValue"; import { formatEventNumberValue } from "@/utils/eventValue";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
defineOptions({ defineOptions({
@@ -89,6 +94,7 @@ const userStore = useUserStore();
const record = ref<EventRecord>(); const record = ref<EventRecord>();
const isLoading = ref(false); const isLoading = ref(false);
const isVerifying = ref(false); const isVerifying = ref(false);
const verifyDialogVisible = ref(false);
const canUpdateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.update)); const canUpdateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.update));
const canVerifyRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.verify)); const canVerifyRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.verify));
@@ -198,42 +204,41 @@ async function loadRecord(): Promise<void> {
async function confirmVerifyRecord(): Promise<void> { async function confirmVerifyRecord(): Promise<void> {
const currentRecord = record.value; const currentRecord = record.value;
if (!currentRecord?.id || currentRecord.verificationStatus !== "PENDING") { if (!currentRecord?.id || currentRecord.verificationStatus !== "PENDING" || isVerifying.value) {
return; return;
} }
const dialog = DialogPlugin.confirm({ verifyDialogVisible.value = true;
title: "核销登记记录",
content: `确定核销 ${currentRecord.customer?.name || "未知客户"} 的 ${currentRecord.event?.name || "未知事件"} 记录吗?`,
cancelBtn: "取消",
confirmBtn: "核销",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await verifyRecord(currentRecord.id || "");
}
});
} }
async function verifyRecord(id: string): Promise<void> { async function submitVerifyRecord(record: EventRecord, attachmentList: Attachment[]): Promise<void> {
if (!id || isVerifying.value) { const id = record.id;
return; if (!id) {
throw new Error("登记事件记录不存在");
} }
isVerifying.value = true; isVerifying.value = true;
try { try {
if (canUpdateRecord.value) {
await EventRecordAPI.update({
id,
remark: record.remark || "",
attachmentList
});
}
await EventRecordAPI.verify(id); await EventRecordAPI.verify(id);
record.value = await EventRecordAPI.detail(id);
Message.success("登记记录已核销");
} catch (error) {
Message.error(getErrorMessage(error, "登记记录核销失败"));
} finally { } finally {
isVerifying.value = false; isVerifying.value = false;
} }
} }
async function handleVerifySuccess(): Promise<void> {
verifyDialogVisible.value = false;
if (record.value?.id) {
record.value = await EventRecordAPI.detail(record.value.id);
}
}
// ---------- 导航 ---------- // ---------- 导航 ----------
async function toEdit(): Promise<void> { async function toEdit(): Promise<void> {
+48 -23
View File
@@ -102,7 +102,7 @@
v-if="canVerifyRecord && item.verificationStatus === 'PENDING'" v-if="canVerifyRecord && item.verificationStatus === 'PENDING'"
class="action verify bg-orange white" class="action verify bg-orange white"
variant="text" variant="text"
:loading="isVerifying && verifyingId === item.id" :loading="(isVerifying && verifyingId === item.id) || (isLoadingVerifyRecord && verifyLoadingId === item.id)"
@click.stop="confirmVerifyRecord(item)" @click.stop="confirmVerifyRecord(item)"
> >
核销 核销
@@ -125,6 +125,13 @@
</div> </div>
</t-pull-down-refresh> </t-pull-down-refresh>
</div> </div>
<event-record-verify-dialog
v-model:visible="verifyDialogVisible"
:record="verifyTarget"
:can-update="canUpdateRecord"
:submit="submitVerifyRecord"
@verified="handleVerifySuccess"
/>
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" /> <nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
<t-popup v-model="startVisible" placement="bottom"> <t-popup v-model="startVisible" placement="bottom">
<t-date-time-picker <t-date-time-picker
@@ -155,12 +162,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { DialogPlugin, type DropdownValue } from "tdesign-mobile-vue"; import { DialogPlugin, type DropdownValue } from "tdesign-mobile-vue";
import { Time, WebToolkit } from "timi-web"; import { Time, WebToolkit } from "timi-web";
import { useUserStore } from "timi-web-api"; import { useUserStore, type Attachment } from "timi-web-api";
import EventAPI from "@/api/EventAPI"; import EventAPI from "@/api/EventAPI";
import EventRecordAPI from "@/api/EventRecordAPI"; import EventRecordAPI from "@/api/EventRecordAPI";
import EventRecordVerifyDialog from "@/components/EventRecordVerifyDialog.vue";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import type { EventRecordListItemView, EventRecordPage, EventRecordPageResult, VerificationStatus } from "@/types/EventRecord"; import type { EventRecord, EventRecordListItemView, EventRecordPage, EventRecordPageResult, VerificationStatus } from "@/types/EventRecord";
import type { NavBarAction } from "@/types/NavBarAction"; import type { NavBarAction } from "@/types/NavBarAction";
import { getCustomerPhotoURL } from "@/utils/customer"; import { getCustomerPhotoURL } from "@/utils/customer";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
@@ -239,6 +247,7 @@ const selectedNumberEvent = computed(() => {
const canExportRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.export)); const canExportRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.export));
const canDeleteRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.delete)); const canDeleteRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.delete));
const canVerifyRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.verify)); const canVerifyRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.verify));
const canUpdateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.update));
const canShowActionMenu = computed(() => canCreateRecord.value || canExportRecord.value); const canShowActionMenu = computed(() => canCreateRecord.value || canExportRecord.value);
const actionList = computed<NavBarAction[]>(() => { const actionList = computed<NavBarAction[]>(() => {
@@ -382,6 +391,10 @@ const isDeleting = ref(false);
const deletingId = ref(""); const deletingId = ref("");
const isVerifying = ref(false); const isVerifying = ref(false);
const verifyingId = ref(""); const verifyingId = ref("");
const isLoadingVerifyRecord = ref(false);
const verifyLoadingId = ref("");
const verifyDialogVisible = ref(false);
const verifyTarget = ref<EventRecord>();
async function loadActions(): Promise<void> { async function loadActions(): Promise<void> {
eventList.value = await EventAPI.listValid(); eventList.value = await EventAPI.listValid();
@@ -467,45 +480,57 @@ async function exportExcel(): Promise<void> {
} }
async function confirmVerifyRecord(record: EventRecordListItemView): Promise<void> { async function confirmVerifyRecord(record: EventRecordListItemView): Promise<void> {
if (!record.id || record.verificationStatus !== "PENDING") { if (!record.id || record.verificationStatus !== "PENDING" || isVerifying.value || isLoadingVerifyRecord.value) {
return; return;
} }
const recordId = record.id; const recordId = record.id;
const dialog = DialogPlugin.confirm({ isLoadingVerifyRecord.value = true;
title: "核销登记记录", verifyLoadingId.value = recordId;
content: `确定核销 ${getRecordCustomerName(record)} 的 ${getRecordEventName(record)} 记录吗?`, try {
cancelBtn: "取消", verifyTarget.value = await EventRecordAPI.detail(recordId);
confirmBtn: "核销", verifyDialogVisible.value = true;
onCancel() { } catch (error) {
dialog.destroy(); Message.error(getErrorMessage(error, "登记记录加载失败"));
}, } finally {
async onConfirm() { isLoadingVerifyRecord.value = false;
dialog.destroy(); verifyLoadingId.value = "";
await verifyRecord(recordId);
} }
});
} }
async function verifyRecord(id: string): Promise<void> { async function submitVerifyRecord(record: EventRecord, attachmentList: Attachment[]): Promise<void> {
if (isVerifying.value) { const id = record.id;
return; if (!id) {
throw new Error("登记事件记录不存在");
} }
isVerifying.value = true; isVerifying.value = true;
verifyingId.value = id; verifyingId.value = id;
try { try {
if (canUpdateRecord.value) {
await EventRecordAPI.update({
id,
remark: record.remark || "",
attachmentList
});
}
await EventRecordAPI.verify(id); await EventRecordAPI.verify(id);
await reloadRecords();
Message.success("登记记录已核销");
} catch (error) {
Message.error(getErrorMessage(error, "登记记录核销失败"));
} finally { } finally {
isVerifying.value = false; isVerifying.value = false;
verifyingId.value = ""; verifyingId.value = "";
} }
} }
async function handleVerifySuccess(): Promise<void> {
verifyDialogVisible.value = false;
verifyTarget.value = undefined;
try {
await reloadRecords();
} catch (error) {
Message.error(getErrorMessage(error, "登记记录刷新失败"));
}
}
async function toDetail(id: string): Promise<void> { async function toDetail(id: string): Promise<void> {
if (!id) { if (!id) {
return; return;