fix event record

This commit is contained in:
Timi
2026-08-07 19:39:48 +08:00
parent 16fa5fdd36
commit 45ea8b46aa
9 changed files with 205 additions and 53 deletions
+15 -9
View File
@@ -254,9 +254,9 @@ async function applyIntroducer(code: string): Promise<void> {
const matched = await CustomerAPI.findByCode(code);
if (!matched?.id) {
formData.introducerCustomerId = "";
formData.introducerCustomer = undefined;
introducerCustomerId.value = "";
scannerVisible.value = false;
scannerVisible.value = false;
Toast.warning(`未匹配到 ${code} 编码介绍人,请选择或扫码已有客户`);
return;
}
@@ -267,8 +267,9 @@ async function applyIntroducer(code: string): Promise<void> {
}
async function handleIntroducerChange(id: string): Promise<void> {
const customer = await CustomerAPI.findById(id);
introducerCustomerId.value = customer!.id!;
const customer = id ? await CustomerAPI.findById(id) : undefined;
introducerCustomerId.value = customer?.id || "";
formData.introducerCustomerId = customer?.id || "";
formData.introducerCustomer = customer;
}
@@ -349,7 +350,7 @@ async function loadDetail() {
Object.assign(formData, customer);
formData.storeId = customer.storeId || formData.storeId;
selectedAreas.value = normalizeBodyAreaValue(customer.conditioningPart);
introducerCustomerId.value = customer.introducerCustomer?.id || "";
introducerCustomerId.value = customer.introducerCustomerId || customer.introducerCustomer?.id || "";
} catch (err) {
console.warn("客户详情加载失败", err);
Toast.error("客户详情加载失败");
@@ -411,18 +412,23 @@ async function doSubmit(): Promise<void> {
isSubmitting.value = true;
try {
formData.conditioningPart = selectedAreas.value;
formData.attachmentList = photoTempFileId.value ? [{
const payload: Customer = { ...formData };
payload.conditioningPart = selectedAreas.value;
payload.introducerCustomerId = introducerCustomerId.value || "";
payload.attachmentList = photoTempFileId.value ? [{
attachType: "PHOTO",
tempFileId: photoTempFileId.value
}] : undefined;
delete payload.introducerCustomer;
if (isCreateMode.value) {
const id = await CustomerAPI.create(formData);
await CustomerAPI.create(payload);
Toast.success("创建成功");
await router.replace(`/management/customers/${id}`);
const redirect = String(route.query.redirect || "");
await router.replace(redirect || "/management/customers");
return;
} else {
await CustomerAPI.update(formData);
await CustomerAPI.update(payload);
Toast.success("保存成功");
const redirect = String(route.query.redirect || "");
if (redirect) {
+15 -1
View File
@@ -1,6 +1,11 @@
<template>
<div v-can="permissionCodes.eventRecord.read" class="registration-record-detail">
<customer-profile-card v-if="record?.customer" class="customer-profile" :customer="record.customer" />
<customer-profile-card
v-if="record?.customer"
class="customer-profile"
:customer="record.customer"
@click="toCustomerDetail"
/>
<t-cell-group v-if="record" title="登记信息" theme="card">
<t-cell title="登记事件" :note="record.event?.name || '未知事件'" />
<t-cell title="登记时间" :note="record.registeredAt ? Time.toPassedDateTime(record.registeredAt) : '-'" />
@@ -80,6 +85,15 @@ async function toEdit(): Promise<void> {
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}`);
}
watch(() => route.params.id, async () => {
await loadRecord();
}, { immediate: true });
+2 -2
View File
@@ -432,8 +432,8 @@ function openRegisteredAtPicker(): void {
registeredAtPickerVisible.value = true;
}
function confirmRegisteredAt(): void {
registeredAtText.value = registeredAtDraftText.value;
function confirmRegisteredAt(value?: string | number): void {
registeredAtText.value = String(value || registeredAtDraftText.value);
registeredAtPickerVisible.value = false;
}
+67 -14
View File
@@ -63,7 +63,7 @@
class="cell"
:title="getRecordEventName(item)"
:description="getRecordCustomerName(item)"
:note="item.registeredAt ? Time.toPassedDateTime(item.registeredAt) : '-'"
:note="item.registeredAt ? Time.toDateTime(item.registeredAt) : '-'"
arrow
hover
@click="toDetail(item.id || '')"
@@ -131,12 +131,13 @@
<script setup lang="ts">
import { DialogPlugin, Toast, type DropdownValue } from "tdesign-mobile-vue";
import { AttachmentAPI, Time, Toolkit, type PageResult, useUserStore } from "timi-web";
import { Time, Toolkit, type PageResult, useUserStore } from "timi-web";
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 } from "@/types/EventRecord";
import { getCustomerPhotoURL } from "@/utils/customer";
import { permissionCodes } from "@/utils/permission";
type DateFilterType = "today" | "yesterday" | "beforeYesterday" | "lastWeek" | "all" | "custom";
@@ -146,6 +147,7 @@ defineOptions({
});
const pageSize = 20;
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
@@ -160,9 +162,9 @@ const dateOptions: Array<{ label: string; value: DateFilterType }> = [
// ---------- 筛选 ----------
const keyword = ref("");
const dateFilterType = ref<DateFilterType>("today");
const dateRange = ref(buildPresetRange("today"));
const keyword = ref(getQueryText("keyword"));
const dateFilterType = ref<DateFilterType>(parseDateFilterType(route.query.dateType));
const dateRange = ref(parseDateRange(dateFilterType.value));
const actionFilter = ref<string[]>([]);
const eventList = ref<EventItem[]>([]);
const startVisible = ref(false);
@@ -204,6 +206,7 @@ watch(keyword, () => {
}
window.clearTimeout(keywordTimer);
keywordTimer = window.setTimeout(async () => {
await replaceFilterQuery();
await reloadRecords();
}, 300);
});
@@ -212,6 +215,7 @@ watch(actionFilter, async () => {
if (!initialized) {
return;
}
await replaceFilterQuery();
await reloadRecords();
});
@@ -225,6 +229,7 @@ watch(dateRange, async (value) => {
return;
}
await replaceFilterQuery();
await reloadRecords();
}, { deep: true });
@@ -299,7 +304,10 @@ const deletingId = ref("");
async function loadActions(): Promise<void> {
eventList.value = await EventAPI.listValid();
actionFilter.value = eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
const eventIdList = parseEventIdList(route.query.eventIds);
actionFilter.value = eventIdList.length
? eventIdList
: eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
}
async function selectDateFilter(type: DropdownValue): Promise<void> {
@@ -310,7 +318,10 @@ async function selectDateFilter(type: DropdownValue): Promise<void> {
dateFilterType.value = String(type) as DateFilterType;
if (dateFilterType.value !== "custom") {
dateRange.value = buildPresetRange(dateFilterType.value);
await replaceFilterQuery();
await reloadRecords();
} else {
await replaceFilterQuery();
}
}
@@ -336,7 +347,10 @@ async function toDetail(id: string): Promise<void> {
if (!id) {
return;
}
await router.push(`/management/registration-event-records/${id}`);
await router.push({
path: `/management/registration-event-records/${id}`,
query: buildFilterQuery()
});
}
async function toCreate(): Promise<void> {
@@ -385,22 +399,19 @@ async function removeRecord(id: string): Promise<void> {
// ---------- 格式化 ----------
function getRecordEventName(record: EventRecordListItemView): string {
return record.eventName || "未知事件";
return record.event?.name || "未知事件";
}
function getRecordCustomerName(record: EventRecordListItemView): string {
return record.customerName || "未知客户";
return record.customer?.name || "未知客户";
}
function getRecordPhotoURL(record: EventRecordListItemView): string {
if (!record.customerPhotoAttachmentId) {
return "";
}
return AttachmentAPI.getReadURL(record.customerPhotoAttachmentId);
return getCustomerPhotoURL(record.customer);
}
function getRecordCustomerInitial(record: EventRecordListItemView): string {
return (record.customerName || "客").slice(0, 1);
return getRecordCustomerName(record).slice(0, 1);
}
function buildRangeReq(): Omit<EventRecordPage, "index" | "size"> {
@@ -414,6 +425,48 @@ function buildRangeReq(): Omit<EventRecordPage, "index" | "size"> {
};
}
function getQueryText(key: string): string {
const value = route.query[key];
return Array.isArray(value) ? String(value[0] || "") : String(value || "");
}
function parseDateFilterType(value: unknown): DateFilterType {
const text = Array.isArray(value) ? String(value[0] || "") : String(value || "");
return dateOptions.some(item => item.value === text) ? text as DateFilterType : "today";
}
function parseDateRange(type: DateFilterType): [string, string] {
if (type !== "custom") {
return buildPresetRange(type);
}
const beginAt = getQueryText("beginAt");
const endAt = getQueryText("endAt");
return [beginAt, endAt];
}
function parseEventIdList(value: unknown): string[] {
const text = Array.isArray(value) ? String(value[0] || "") : String(value || "");
return text.split(",").map(item => item.trim()).filter(Boolean);
}
function buildFilterQuery(): Record<string, string> {
return {
keyword: keyword.value.trim(),
dateType: dateFilterType.value,
beginAt: dateRange.value[0] || "",
endAt: dateRange.value[1] || "",
eventIds: actionFilter.value.join(",")
};
}
async function replaceFilterQuery(): Promise<void> {
await router.replace({
name: "EventRecordList",
query: buildFilterQuery()
});
}
function buildPresetRange(type: Exclude<DateFilterType, "custom">): [string, string] {
if (type === "all") {
return ["", ""];
@@ -61,7 +61,7 @@
<div class="actions">
<t-button
v-if="editingId"
v-can="permissionCodes.store.delete"
v-can="deletePermissionCode"
class="action"
theme="danger"
variant="outline"
@@ -82,16 +82,19 @@
<script setup lang="ts">
import { DialogPlugin, Toast, type CalendarProps, type TDate } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import StoreAPI from "@/api/StoreAPI";
import type { Store, StoreBusinessDay, StoreBusinessDayOpenStatus } from "@/types/Store";
import { permissionCodes } from "@/utils/permission";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({
name: "StoreBusinessDayCalendar"
});
const route = useRoute();
const userStore = useUserStore();
const storeId = computed(() => String(route.params.id || ""));
const isStoreManagerScope = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager) && !userStore.hasRoles(gaoRoleCodes.globalManager));
// ---------- 门店信息 ----------
@@ -176,7 +179,14 @@ const formModel = reactive({
openStatus: "OPEN" as StoreBusinessDayOpenStatus,
reason: "正常休息"
});
const savePermissionCode = computed(() => editingId.value ? permissionCodes.store.update : permissionCodes.store.create);
const savePermissionCode = computed(() => {
if (isStoreManagerScope.value) {
return permissionCodes.store.update;
}
return editingId.value ? permissionCodes.store.update : permissionCodes.store.create;
});
const deletePermissionCode = computed(() => isStoreManagerScope.value ? permissionCodes.store.update : permissionCodes.store.delete);
function openDayEditor(day?: StoreBusinessDay, dateValue = toDateValue(new Date())): void {
editingId.value = day?.id || "";
+35 -15
View File
@@ -1,13 +1,13 @@
<template>
<div v-can="savePermissionCode" class="store-edit">
<t-form class="form" label-align="left">
<t-form-item label="编码">
<t-form-item v-if="!isStoreManagerScope" label="编码">
<t-input v-model.trim="formModel.code" placeholder="门店编码" borderless />
</t-form-item>
<t-form-item label="名称">
<t-input v-model.trim="formModel.name" placeholder="门店名称" borderless />
</t-form-item>
<t-form-item label="启用" content-align="right">
<t-form-item v-if="!isStoreManagerScope" label="启用" content-align="right">
<t-switch v-model="formModel.status" :custom-value="['ACTIVE', 'INACTIVE']" />
</t-form-item>
<t-form-item label="电话">
@@ -25,7 +25,7 @@
</t-cell-group>
<div class="actions">
<t-button
v-if="!isCreateMode"
v-if="!isCreateMode && !isStoreManagerScope"
v-can="permissionCodes.store.delete"
class="action"
theme="danger"
@@ -44,9 +44,10 @@
<script setup lang="ts">
import { DialogPlugin, Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import StoreAPI from "@/api/StoreAPI";
import type { Store, StoreStatus } from "@/types/Store";
import { permissionCodes } from "@/utils/permission";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({
name: "StoreEdit"
@@ -54,7 +55,9 @@ defineOptions({
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const routeId = computed(() => String(route.params.id || ""));
const isStoreManagerScope = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager) && !userStore.hasRoles(gaoRoleCodes.globalManager));
// ---------- 路由权限 ----------
@@ -105,30 +108,47 @@ async function saveStore(): Promise<void> {
isSaving.value = true;
try {
const store: Store = {
id: formModel.id || undefined,
code: formModel.code,
name: formModel.name,
status: formModel.status,
telephone: formModel.telephone || undefined,
address: formModel.address || undefined,
remark: formModel.remark || undefined,
sort: sortValue.value
};
const store = buildStorePayload();
if (isCreateMode.value) {
await StoreAPI.create(store);
} else {
await StoreAPI.update(store);
}
Toast.success("门店已保存");
if (isStoreManagerScope.value) {
return;
}
await router.replace("/management/stores");
} finally {
isSaving.value = false;
}
}
function buildStorePayload(): Store {
if (isStoreManagerScope.value) {
return {
id: formModel.id || undefined,
name: formModel.name,
telephone: formModel.telephone || undefined,
address: formModel.address || undefined,
remark: formModel.remark || undefined
};
}
return {
id: formModel.id || undefined,
code: formModel.code,
name: formModel.name,
status: formModel.status,
telephone: formModel.telephone || undefined,
address: formModel.address || undefined,
remark: formModel.remark || undefined,
sort: sortValue.value
};
}
function validateForm(): boolean {
if (!formModel.code) {
if (!isStoreManagerScope.value && !formModel.code) {
Toast.warning("请填写门店编码");
return false;
}
+23 -4
View File
@@ -48,7 +48,7 @@
:title="item.title"
arrow
hover
@click="go(item.path)"
@click="go(item)"
>
<template #leftIcon>
<t-icon :name="item.icon" size="1.75rem" />
@@ -87,6 +87,7 @@ type MenuItem = {
path: string;
title: string;
visible: boolean;
action?: "store";
};
type MenuGroup = {
@@ -108,6 +109,7 @@ const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value));
const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const canLoadLoginStore = computed(() => userStore.hasRoles([gaoRoleCodes.storeManager, gaoRoleCodes.employee], "or"));
const canViewStore = computed(() => canSelectStore.value || canLoadLoginStore.value);
const canManageStore = computed(() => userStore.hasRoles([gaoRoleCodes.globalManager, gaoRoleCodes.storeManager], "or"));
const canManagePermission = computed(() => (
userStore.hasRoles(coreRoleCodes.admin) || userStore.hasRoles(coreRoleCodes.userRoleAdmin)
) && userStore.hasPermissions([
@@ -166,7 +168,8 @@ const menuGroupList = computed<MenuGroup[]>(() => [
icon: "shop",
path: "/management/stores",
title: "门店管理",
visible: canSelectStore.value && userStore.hasPermissions(permissionCodes.store.read)
action: "store",
visible: canManageStore.value && userStore.hasPermissions(permissionCodes.store.read)
},
{
icon: "usergroup",
@@ -235,8 +238,24 @@ function buildUserAvatar(user?: UserAvatarSource): string {
return UserAPI.getAvatarURL(user) || buildFallbackAvatar(currentUser.value?.name, "#0f766e");
}
async function go(path: string): Promise<void> {
await router.push(path);
async function go(item: MenuItem | string): Promise<void> {
if (typeof item === "string") {
await router.push(item);
return;
}
if (item.action === "store" && !canSelectStore.value) {
const store = await currentStoreStore.loadLoginStore();
if (!store?.id) {
Toast.warning("未绑定所属门店");
return;
}
await router.push(`/management/stores/${store.id}`);
return;
}
await router.push(item.path);
}
onMounted(async () => {