add event verify

This commit is contained in:
Timi
2026-08-29 15:35:28 +08:00
parent 24cf6fc714
commit 904af04cdd
9 changed files with 233 additions and 16 deletions
+1
View File
@@ -44,6 +44,7 @@
<t-cell v-if="event.valueType === 'NUMBER'" title="数值单位" :note="getValueUnitText(event.valueUnit)" />
<t-cell v-if="event.valueType === 'NUMBER'" title="登记必填数值" :note="event.requireValue ? '是' : '否'" />
<t-cell title="需要拍照" :note="event.requirePhoto ? '是' : '否'" />
<t-cell title="需要核销" :note="event.requireVerification ? '是' : '否'" />
<t-cell title="次数约束" :note="getLimitText(event)" />
</t-cell-group>
<t-cell-group v-entrance-transition="210" v-if="event" title="生效时间" theme="card">
+6
View File
@@ -79,6 +79,9 @@
<t-form-item label="需要拍照" content-align="right">
<t-switch v-model="formModel.requirePhoto" />
</t-form-item>
<t-form-item label="需要核销" content-align="right">
<t-switch v-model="formModel.requireVerification" />
</t-form-item>
<t-form-item label="状态" arrow @click="openStatusPicker">
<div class="picker-text" v-text="getStatusText(formModel.status)" />
</t-form-item>
@@ -265,6 +268,7 @@ const formModel = reactive({
requireValue: false,
status: "ACTIVE" as EventStatus,
requirePhoto: false,
requireVerification: false,
limitType: "NONE" as EventLimitType,
limitValueText: "",
beginAtText: "",
@@ -633,6 +637,7 @@ async function saveEventItem(): Promise<void> {
requireValue: isNumberValueType(formModel.valueType) && formModel.requireValue,
status: formModel.status,
requirePhoto: formModel.requirePhoto,
requireVerification: formModel.requireVerification,
limitType: formModel.limitType,
limitValue: resolveLimitValue(),
beginAt: resolveTimestamp(formModel.beginAtText),
@@ -726,6 +731,7 @@ async function fillForm(): Promise<void> {
requireValue: current.requireValue ?? false,
status: current.status === "DRAFT" ? "DRAFT" : current.status === "INACTIVE" ? "INACTIVE" : "ACTIVE",
requirePhoto: current.requirePhoto ?? false,
requireVerification: current.requireVerification ?? false,
limitType: current.limitType || "NONE",
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
+1
View File
@@ -30,6 +30,7 @@
:content="getStatusTag(item).text"
/>
<t-tag v-if="item.requirePhoto" theme="warning" variant="outline">需拍照</t-tag>
<t-tag v-if="item.requireVerification" theme="warning" variant="outline">需核销</t-tag>
</div>
</template>
<template #description>
+80 -2
View File
@@ -15,6 +15,13 @@
:hover="canReadEvent && !!(record.event?.id || record.eventId)"
@click="toEventDetail"
/>
<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 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)" />
@@ -26,7 +33,17 @@
<t-cell title="登记人" :note="Text.display(record.operator?.nick || record.operator?.name)" />
<t-cell title="备注" :note="Text.display(record.remark)" />
</t-cell-group>
<div v-if="record" class="photo-wrapper" v-entrance-transition="140">
<t-button
v-if="canVerifyRecord && record?.verificationStatus === 'PENDING'"
class="verify-button cell-button"
theme="primary"
:loading="isVerifying"
@click="confirmVerifyRecord"
v-entrance-transition="140"
>
核销登记记录
</t-button>
<div v-if="record" class="photo-wrapper" v-entrance-transition="210">
<photo-scroller
:model-value="recordPhotoList"
:photo-items="recordPhotoItemList"
@@ -37,11 +54,12 @@
title="登记照片"
/>
</div>
<t-empty v-else :description="isLoading ? '正在加载登记事件记录' : '登记事件记录不存在'" />
<t-empty v-if="!record" :description="isLoading ? '正在加载登记事件记录' : '登记事件记录不存在'" />
</div>
</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";
@@ -53,6 +71,8 @@ import type { PhotoItem } from "@/types/Photo";
import type { PointLedgerChangeType } from "@/types/Point";
import { getAttachmentReadURL } from "@/utils/attachment";
import { formatEventNumberValue } from "@/utils/eventValue";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
defineOptions({
@@ -68,8 +88,10 @@ const userStore = useUserStore();
const record = ref<EventRecord>();
const isLoading = ref(false);
const isVerifying = ref(false);
const canUpdateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.update));
const canVerifyRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.verify));
const canReadEvent = computed(() => userStore.hasPermissions(permissionCodes.event.read));
const recordPhotoItemList = computed(() => (record.value?.attachmentList || [])
.filter(item => !!item.id)
@@ -121,6 +143,19 @@ 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));
function getPointValueText(value?: number | null, changeType?: PointLedgerChangeType): string {
@@ -161,6 +196,44 @@ async function loadRecord(): Promise<void> {
}
}
async function confirmVerifyRecord(): Promise<void> {
const currentRecord = record.value;
if (!currentRecord?.id || currentRecord.verificationStatus !== "PENDING") {
return;
}
const dialog = DialogPlugin.confirm({
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> {
if (!id || isVerifying.value) {
return;
}
isVerifying.value = true;
try {
await EventRecordAPI.verify(id);
record.value = await EventRecordAPI.detail(id);
Message.success("登记记录已核销");
} catch (error) {
Message.error(getErrorMessage(error, "登记记录核销失败"));
} finally {
isVerifying.value = false;
}
}
// ---------- 导航 ----------
async function toEdit(): Promise<void> {
@@ -239,6 +312,11 @@ onUnmounted(() => {
box-shadow: var(--td-shadow-1);
border-radius: 1.4rem;
}
.verify-button {
width: calc(100% - 2rem);
margin: 0 1rem;
}
}
:global(html.theme-dark) {
+125 -14
View File
@@ -23,6 +23,11 @@
:options="actionOptions"
multiple
/>
<t-dropdown-item
v-model="verificationFilter"
:label="verificationFilterLabel"
:options="verificationOptions"
/>
</t-dropdown-menu>
</div>
</div>
@@ -64,12 +69,22 @@
<t-cell
class="cell"
:title="getRecordEventName(item)"
:description="getRecordCustomerName(item)"
:note="getRecordRegisteredAt(item)"
arrow
hover
@click="toDetail(item.id || '')"
>
<template #description>
<div class="description-line">
<t-tag
v-if="item.verificationStatus && item.verificationStatus !== 'NOT_REQUIRED'"
:theme="getVerificationTag(item).theme"
variant="light-outline"
:content="getVerificationTag(item).text"
/>
<span v-text="getRecordCustomerName(item)" />
</div>
</template>
<template #leftIcon>
<t-image
v-if="getRecordPhotoURL(item)"
@@ -81,15 +96,27 @@
<div v-else class="avatar fallback" v-text="getRecordCustomerInitial(item)" />
</template>
</t-cell>
<template v-if="canDeleteRecord" #right>
<t-button
class="delete"
theme="danger"
:loading="isDeleting && deletingId === item.id"
@click.stop="confirmRemoveRecord(item)"
>
删除
</t-button>
<template v-if="canVerifyRecord || canDeleteRecord" #right>
<div class="actions">
<t-button
v-if="canVerifyRecord && item.verificationStatus === 'PENDING'"
class="action verify"
theme="primary"
:loading="isVerifying && verifyingId === item.id"
@click.stop="confirmVerifyRecord(item)"
>
核销
</t-button>
<t-button
v-if="canDeleteRecord"
class="action delete"
theme="danger"
:loading="isDeleting && deletingId === item.id"
@click.stop="confirmRemoveRecord(item)"
>
删除
</t-button>
</div>
</template>
</t-swipe-cell>
</t-cell-group>
@@ -133,7 +160,7 @@ import EventAPI from "@/api/EventAPI";
import EventRecordAPI from "@/api/EventRecordAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem } from "@/types/EventItem";
import type { EventRecordListItemView, EventRecordPage, EventRecordPageResult } from "@/types/EventRecord";
import type { EventRecordListItemView, EventRecordPage, EventRecordPageResult, VerificationStatus } from "@/types/EventRecord";
import type { NavBarAction } from "@/types/NavBarAction";
import { getCustomerPhotoURL } from "@/utils/customer";
import { getErrorMessage } from "@/utils/error";
@@ -142,6 +169,7 @@ import { permissionCodes } from "@/utils/permission";
import { Message } from "@/utils/message.ts";
type DateFilterType = "today" | "yesterday" | "beforeYesterday" | "lastWeek" | "all" | "custom";
type VerificationFilter = "ALL" | VerificationStatus;
defineOptions({
name: "EventRecordList"
@@ -170,6 +198,13 @@ const dateRange = ref(parseDateRange(dateFilterType.value));
const startDraft = ref("");
const endDraft = ref("");
const actionFilter = ref<string[]>([]);
const verificationOptions: Array<{ label: string; value: VerificationFilter }> = [
{ label: "全部核销状态", value: "ALL" },
{ label: "待核销", value: "PENDING" },
{ label: "已核销", value: "VERIFIED" },
{ label: "无需核销", value: "NOT_REQUIRED" }
];
const verificationFilter = ref<VerificationFilter>(parseVerificationFilter(route.query.verificationStatus));
const eventList = ref<EventItem[]>([]);
const startVisible = ref(false);
const endVisible = ref(false);
@@ -193,6 +228,7 @@ const eventFilterLabel = computed(() => {
.map(item => item.name || "未命名事件")
.join("、");
});
const verificationFilterLabel = computed(() => verificationOptions.find(item => item.value === verificationFilter.value)?.label || "全部核销状态");
const selectedNumberEvent = computed(() => {
if (actionFilter.value.length !== 1) {
return undefined;
@@ -202,6 +238,7 @@ const selectedNumberEvent = computed(() => {
});
const canExportRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.export));
const canDeleteRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.delete));
const canVerifyRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.verify));
const canShowActionMenu = computed(() => canCreateRecord.value || canExportRecord.value);
const actionList = computed<NavBarAction[]>(() => {
@@ -246,6 +283,14 @@ watch(actionFilter, async () => {
await reloadRecords();
});
watch(verificationFilter, async () => {
if (!initialized) {
return;
}
await replaceFilterQuery();
await reloadRecords();
});
watch(dateRange, async (value) => {
if (!initialized || dateFilterType.value !== "custom") {
return;
@@ -335,6 +380,8 @@ async function loadRecords(): Promise<void> {
const isDeleting = ref(false);
const deletingId = ref("");
const isVerifying = ref(false);
const verifyingId = ref("");
async function loadActions(): Promise<void> {
eventList.value = await EventAPI.listValid();
@@ -419,6 +466,46 @@ async function exportExcel(): Promise<void> {
}
}
async function confirmVerifyRecord(record: EventRecordListItemView): Promise<void> {
if (!record.id || record.verificationStatus !== "PENDING") {
return;
}
const recordId = record.id;
const dialog = DialogPlugin.confirm({
title: "核销登记记录",
content: `确定核销 ${getRecordCustomerName(record)} 的 ${getRecordEventName(record)} 记录吗?`,
cancelBtn: "取消",
confirmBtn: "核销",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await verifyRecord(recordId);
}
});
}
async function verifyRecord(id: string): Promise<void> {
if (isVerifying.value) {
return;
}
isVerifying.value = true;
verifyingId.value = id;
try {
await EventRecordAPI.verify(id);
await reloadRecords();
Message.success("登记记录已核销");
} catch (error) {
Message.error(getErrorMessage(error, "登记记录核销失败"));
} finally {
isVerifying.value = false;
verifyingId.value = "";
}
}
async function toDetail(id: string): Promise<void> {
if (!id) {
return;
@@ -501,6 +588,12 @@ function getRecordCustomerInitial(record: EventRecordListItemView): string {
return getRecordCustomerName(record).slice(0, 1);
}
function getVerificationTag(record: EventRecordListItemView): { text: string; theme: "warning" | "success" } {
return record.verificationStatus === "VERIFIED"
? { text: "已核销", theme: "success" }
: { text: "待核销", theme: "warning" };
}
function getRecordRegisteredAt(record: EventRecordListItemView): string {
if (!record.registeredAt) {
return "-";
@@ -519,7 +612,8 @@ function buildRangeReq(): Omit<EventRecordPage, "index" | "size"> {
endAt,
customerId: customerId.value || undefined,
keyword: keyword.value.trim() || undefined,
eventIdList: actionFilter.value.length === 1 ? actionFilter.value : selectedAllEvent ? undefined : actionFilter.value
eventIdList: actionFilter.value.length === 1 ? actionFilter.value : selectedAllEvent ? undefined : actionFilter.value,
verificationStatus: verificationFilter.value === "ALL" ? undefined : verificationFilter.value
};
}
@@ -548,6 +642,11 @@ function parseEventIdList(value: unknown): string[] {
return text.split(",").map(item => item.trim()).filter(Boolean);
}
function parseVerificationFilter(value: unknown): VerificationFilter {
const text = Array.isArray(value) ? String(value[0] || "") : String(value || "");
return verificationOptions.some(item => item.value === text) ? text as VerificationFilter : "ALL";
}
function buildFilterQuery(transitionDepth = getQueryText("transitionDepth")): Record<string, string> {
const query = {
keyword: keyword.value.trim(),
@@ -556,7 +655,8 @@ function buildFilterQuery(transitionDepth = getQueryText("transitionDepth")): Re
endAt: dateRange.value[1] || "",
customerId: customerId.value,
customerName: getQueryText("customerName"),
eventIds: actionFilter.value.join(",")
eventIds: actionFilter.value.join(","),
verificationStatus: verificationFilter.value === "ALL" ? "" : verificationFilter.value
};
if (isValidTransitionDepth(transitionDepth)) {
return {
@@ -767,9 +867,20 @@ onUnmounted(() => {
width: 3.5rem;
height: 3.5rem;
}
.description-line {
gap: .5rem;
display: flex;
align-items: center;
}
}
.delete {
.actions {
display: flex;
height: 100%;
}
.action {
height: 100%;
padding: 0 1rem;
border-radius: 0;