From 9c5306d8e4de449212389ee9c36fd8edfccd2fd2 Mon Sep 17 00:00:00 2001 From: Timi Date: Tue, 1 Sep 2026 16:27:53 +0800 Subject: [PATCH] update EventRecordEdit.vue style --- src/assets/styles/global.less | 2 +- src/components/CustomerSelector.vue | 2 +- src/components/EventSelector.vue | 67 ++- src/pages/customer/CustomerList.vue | 6 +- src/pages/event/EventList.vue | 2 +- src/pages/event/EventRecordEdit.vue | 239 +++++------ src/pages/event/EventRecordList.vue | 6 +- src/pages/event/EventRecordRank.vue | 4 +- .../event/component/EventOptionEditor.vue | 2 +- .../event/component/PackagePurchaseForm.vue | 137 ++++++ .../event/component/useEventRecordPhoto.ts | 92 ++++ .../component/usePackagePurchaseRecord.ts | 165 +++++++ src/pages/notify/NotifyList.vue | 2 +- src/pages/notify/UserNotifyList.vue | 2 +- src/pages/package/CustomerPackageDelivery.vue | 4 +- src/pages/package/CustomerPackageList.vue | 20 +- src/pages/package/CustomerPackagePurchase.vue | 405 ------------------ src/pages/package/FundRecordList.vue | 2 +- src/pages/package/PackageConfigEdit.vue | 4 +- src/pages/package/PackageConfigList.vue | 2 +- src/pages/package/PackageDeliveryList.vue | 2 +- src/pages/point/PointAccountList.vue | 6 +- src/pages/point/PointLedgerList.vue | 2 +- src/pages/point/PointRuleList.vue | 2 +- src/pages/store/StoreDiaryList.vue | 4 +- src/pages/store/StoreList.vue | 4 +- src/pages/tag/TagList.vue | 10 +- src/pages/user/UserList.vue | 4 +- src/router/routes/packageRoutes.ts | 3 +- src/utils/package.ts | 2 +- 30 files changed, 583 insertions(+), 621 deletions(-) create mode 100644 src/pages/event/component/PackagePurchaseForm.vue create mode 100644 src/pages/event/component/useEventRecordPhoto.ts create mode 100644 src/pages/event/component/usePackagePurchaseRecord.ts delete mode 100644 src/pages/package/CustomerPackagePurchase.vue diff --git a/src/assets/styles/global.less b/src/assets/styles/global.less index 4c5d5dd..5150856 100644 --- a/src/assets/styles/global.less +++ b/src/assets/styles/global.less @@ -84,7 +84,7 @@ html.theme-dark body { gap: 1rem; } -.load-state { +.center-tips { color: var(--tui-gray); padding: .75rem; text-align: center; diff --git a/src/components/CustomerSelector.vue b/src/components/CustomerSelector.vue index 5b2c5ff..ace3375 100644 --- a/src/components/CustomerSelector.vue +++ b/src/components/CustomerSelector.vue @@ -261,7 +261,7 @@ onUnmounted(() => { background: rgba(15, 23, 42, .96); } - .load-state { + .center-tips { color: #94a3b8; } } diff --git a/src/components/EventSelector.vue b/src/components/EventSelector.vue index 3380a08..07b8e86 100644 --- a/src/components/EventSelector.vue +++ b/src/components/EventSelector.vue @@ -2,26 +2,35 @@

-
暂无可用登记事件
@@ -35,12 +44,16 @@ interface Props { label?: string; requiredMark?: boolean; disabled?: boolean; + selectionMode?: SelectionMode; } +type SelectionMode = "single" | "multiple"; + const props = withDefaults(defineProps(), { label: "登记事件", requiredMark: false, - disabled: false + disabled: false, + selectionMode: "multiple" }); const modelValue = defineModel({ required: true }); @@ -63,6 +76,10 @@ function toggleEvent(item: EventItem): void { if (props.disabled || !eventId) { return; } + if (props.selectionMode === "single") { + modelValue.value = [eventId]; + return; + } if (modelValue.value.includes(eventId)) { modelValue.value = modelValue.value.filter(id => id !== eventId); return; @@ -94,6 +111,26 @@ function toggleEvent(item: EventItem): void { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .item-wrapper { + width: 100%; + min-width: 0; + position: relative; + + .package-badge { + top: -.375rem; + right: -.375rem; + z-index: 1; + padding: .125rem .375rem; + position: absolute; + max-width: 50%; + font-size: .6875rem; + line-height: 1.25; + background: var(--td-error-color); + border-radius: .35rem; + pointer-events: none; + } + } + .item { gap: .5rem; width: 100%; diff --git a/src/pages/customer/CustomerList.vue b/src/pages/customer/CustomerList.vue index 9b07c16..a91fc7e 100644 --- a/src/pages/customer/CustomerList.vue +++ b/src/pages/customer/CustomerList.vue @@ -66,8 +66,8 @@ -
-
+
+
{ .items { - .load-state, + .center-tips, .item, .empty-block { color: #e2e8f0; diff --git a/src/pages/event/EventList.vue b/src/pages/event/EventList.vue index ea5cc84..b76de86 100644 --- a/src/pages/event/EventList.vue +++ b/src/pages/event/EventList.vue @@ -69,7 +69,7 @@
-
+
diff --git a/src/pages/event/EventRecordEdit.vue b/src/pages/event/EventRecordEdit.vue index 81e1996..4af42af 100644 --- a/src/pages/event/EventRecordEdit.vue +++ b/src/pages/event/EventRecordEdit.vue @@ -50,17 +50,19 @@ class="event-selector" :event-list="eventList" :disabled="isEditMode" + :selection-mode="isPackagePurchaseSelected ? 'single' : 'multiple'" required-mark @update:model-value="handleEventSelectionChange" /> -
+
+ @@ -111,7 +120,7 @@ - + @@ -201,7 +210,7 @@ + + diff --git a/src/pages/event/component/useEventRecordPhoto.ts b/src/pages/event/component/useEventRecordPhoto.ts new file mode 100644 index 0000000..f95883e --- /dev/null +++ b/src/pages/event/component/useEventRecordPhoto.ts @@ -0,0 +1,92 @@ +import { computed, ref } from "vue"; +import { AttachmentAPI, type Attachment } from "timi-web-api"; +import type { PhotoItem } from "@/types/Photo"; +import { getAttachmentReadURL } from "@/utils/attachment"; +import { compressUploadImageList } from "@/utils/imageCompression"; + +export function useEventRecordPhoto() { + const recordPhotoInputRef = ref(); + const photoItemList = ref([]); + const isPhotoUploading = ref(false); + const photoList = computed({ + get() { + return photoItemList.value.map(item => item.url); + }, + set(value) { + const urlSet = new Set(value); + photoItemList.value = photoItemList.value.filter(item => urlSet.has(item.url)); + } + }); + + function capturePhoto(): void { + recordPhotoInputRef.value?.click(); + } + + async function handleRecordPhotoSelected(event: Event): Promise { + 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({ + tempFileId: item.id, + url: AttachmentAPI.getTempReadURL(item.id) + }); + } + } finally { + isPhotoUploading.value = false; + input.value = ""; + } + } + + function setExistingPhotoList(attachmentList?: Attachment[]): void { + photoItemList.value = (attachmentList || []) + .filter(item => item.attachType === "PHOTO" || !item.attachType) + .filter(item => !!item.id) + .map(item => ({ + attachment: item, + previewUrl: getAttachmentReadURL(item.id || ""), + url: getAttachmentReadURL(item.id || "", 90) + })); + } + + function buildTempFileIdList(): string[] { + return photoItemList.value.map(item => item.tempFileId).filter((item): item is string => !!item); + } + + function buildPhotoAttachmentList(): Array> { + return photoItemList.value.map((item) => { + if (item.attachment) { + return { + ...item.attachment, + attachType: item.attachType || item.attachment.attachType || "PHOTO" + }; + } + + return { + tempFileId: item.tempFileId, + attachType: item.attachType || "PHOTO" + }; + }); + } + + return { + recordPhotoInputRef, + photoItemList, + isPhotoUploading, + photoList, + capturePhoto, + handleRecordPhotoSelected, + setExistingPhotoList, + buildTempFileIdList, + buildPhotoAttachmentList + }; +} diff --git a/src/pages/event/component/usePackagePurchaseRecord.ts b/src/pages/event/component/usePackagePurchaseRecord.ts new file mode 100644 index 0000000..a727b9f --- /dev/null +++ b/src/pages/event/component/usePackagePurchaseRecord.ts @@ -0,0 +1,165 @@ +import { computed, ref, type Ref } from "vue"; +import PackageAPI from "@/api/PackageAPI"; +import type { Customer } from "@/types/Customer"; +import type { EventItem } from "@/types/EventItem"; +import type { PackagePurchaseItem } from "@/types/Package"; +import { isPackagePurchaseEvent } from "@/utils/event"; +import { createPackageRequestNo, formatPackageAmountInput, normalizePackageQuantity, parsePackageAmount } from "@/utils/package"; +import { permissionCodes } from "@/utils/permission"; +import { Message } from "@/utils/message"; + +interface UsePackagePurchaseRecordOptions { + eventList: Ref; + selectedEventIdList: Ref; + selectedCustomerIdList: Ref; + selectedCustomerList: Ref; + note: Ref; + hasPermission: (permissionCode: string) => boolean; + buildRegisteredAt: () => number; + buildTempFileIdList: () => string[]; +} + +export function usePackagePurchaseRecord(options: UsePackagePurchaseRecordOptions) { + const selectedPackageEvent = computed(() => options.selectedEventIdList.value + .map(eventId => options.eventList.value.find(item => item.id === eventId)) + .find((item): item is EventItem => !!item && isPackagePurchaseEvent(item))); + const isPackagePurchaseSelected = computed(() => !!selectedPackageEvent.value); + const selectedPackageConfig = computed(() => selectedPackageEvent.value?.packageConfig); + const isSelectedPackageConfigSaleable = computed(() => { + const config = selectedPackageConfig.value; + if (!config || config.status !== "ACTIVE") { + return false; + } + const now = Date.now(); + return (config.beginAt === undefined || config.beginAt === null || config.beginAt <= now) + && (config.endAt === undefined || config.endAt === null || now <= config.endAt); + }); + const canPriceOverride = computed(() => options.hasPermission(permissionCodes.customerPackage.priceOverride)); + const saleAmountText = ref(""); + const packagePurchaseItemList = ref([]); + + function handleEventSelectionChange(eventIdList: string[]): void { + const packageEvent = eventIdList + .map(eventId => options.eventList.value.find(item => item.id === eventId)) + .find(item => isPackagePurchaseEvent(item)); + if (packageEvent?.id) { + if (!options.hasPermission(permissionCodes.customerPackage.create)) { + Message.warning("当前账号没有套餐购买权限"); + return; + } + options.selectedEventIdList.value = [packageEvent.id]; + keepSingleCustomer(); + applySelectedPackageEvent(packageEvent); + return; + } + options.selectedEventIdList.value = eventIdList; + resetPackagePurchaseForm(); + } + + function applySelectedPackageEvent(event: EventItem): void { + const config = event.packageConfig; + if (!config) { + resetPackagePurchaseForm(); + return; + } + + saleAmountText.value = formatPackageAmountInput(config.standardAmount); + packagePurchaseItemList.value = (config.itemList || []).map((item, index) => ({ + productName: item.productName, + unit: item.unit, + totalQuantity: normalizePackageQuantity(item.defaultQuantity) || "", + sort: item.sort ?? index, + remark: item.remark ?? null + })); + } + + function resetPackagePurchaseForm(): void { + saleAmountText.value = ""; + packagePurchaseItemList.value = []; + } + + async function submitPackagePurchase(): Promise { + const eventId = selectedPackageEvent.value?.id; + const customerId = options.selectedCustomerList.value[0]?.id; + const saleAmount = parsePackageAmount(saleAmountText.value); + if (!eventId || !customerId || saleAmount === undefined) { + return false; + } + + await PackageAPI.purchase({ + requestNo: createPackageRequestNo(), + eventId, + customerId, + saleAmount, + purchasedAt: options.buildRegisteredAt(), + itemList: packagePurchaseItemList.value.map((item, index) => ({ + productName: item.productName.trim(), + unit: item.unit.trim(), + totalQuantity: normalizePackageQuantity(item.totalQuantity)!, + sort: item.sort ?? index, + remark: item.remark ?? null + })), + attachmentList: options.buildTempFileIdList().map(tempFileId => ({ + tempFileId, + attachType: "PHOTO" + })), + remark: options.note.value.trim() || null + }); + return true; + } + + function validatePackagePurchaseForm(): boolean { + if (!options.hasPermission(permissionCodes.customerPackage.create)) { + Message.warning("当前账号没有套餐购买权限"); + return false; + } + if (options.selectedCustomerList.value.length < 1 || 1 < options.selectedCustomerList.value.length || !options.selectedCustomerList.value[0]?.id) { + Message.warning("套餐购买只能选择一个客户"); + return false; + } + if (!selectedPackageConfig.value) { + Message.warning("当前购买事件未绑定有效套餐配置"); + return false; + } + if (!isSelectedPackageConfigSaleable.value) { + Message.warning("事件绑定的套餐当前不可购买,请检查套餐状态和有效期"); + return false; + } + if (parsePackageAmount(saleAmountText.value) === undefined) { + Message.warning("成交价必须是合法的元金额,最多两位小数"); + return false; + } + if (!packagePurchaseItemList.value.length) { + Message.warning("购买权益至少需要一项"); + return false; + } + for (const item of packagePurchaseItemList.value) { + const quantity = normalizePackageQuantity(item.totalQuantity); + if (!item.productName.trim() || !item.unit.trim() || !quantity || quantity === "0") { + Message.warning("产品名称、单位和正数量不能为空"); + return false; + } + } + return true; + } + + function keepSingleCustomer(): void { + const customer = options.selectedCustomerList.value[0]; + options.selectedCustomerList.value = customer ? [customer] : []; + options.selectedCustomerIdList.value = customer?.id ? [customer.id] : []; + } + + return { + selectedPackageEvent, + isPackagePurchaseSelected, + selectedPackageConfig, + canPriceOverride, + saleAmountText, + packagePurchaseItemList, + handleEventSelectionChange, + applySelectedPackageEvent, + resetPackagePurchaseForm, + submitPackagePurchase, + validatePackagePurchaseForm + }; +} diff --git a/src/pages/notify/NotifyList.vue b/src/pages/notify/NotifyList.vue index b9d29d3..f0315d7 100644 --- a/src/pages/notify/NotifyList.vue +++ b/src/pages/notify/NotifyList.vue @@ -53,7 +53,7 @@ -
+
diff --git a/src/pages/notify/UserNotifyList.vue b/src/pages/notify/UserNotifyList.vue index 7102962..19fa76e 100644 --- a/src/pages/notify/UserNotifyList.vue +++ b/src/pages/notify/UserNotifyList.vue @@ -60,7 +60,7 @@ v-else-if="!isLoading" description="暂无站内通知" /> -
+
diff --git a/src/pages/package/CustomerPackageDelivery.vue b/src/pages/package/CustomerPackageDelivery.vue index 767c763..5eae656 100644 --- a/src/pages/package/CustomerPackageDelivery.vue +++ b/src/pages/package/CustomerPackageDelivery.vue @@ -6,7 +6,7 @@
本次发放权益只能提交不超过可发数量的产品
-
+
@@ -202,7 +202,7 @@ onUnmounted(() => navBarStore.clearRight()); margin-left: .5rem; } - .item-card { + .item { gap: .5rem; padding: .75rem; margin-bottom: .75rem; diff --git a/src/pages/package/CustomerPackageList.vue b/src/pages/package/CustomerPackageList.vue index c8ac0a9..57daf86 100644 --- a/src/pages/package/CustomerPackageList.vue +++ b/src/pages/package/CustomerPackageList.vue @@ -31,21 +31,17 @@
-
+
- - - diff --git a/src/pages/package/FundRecordList.vue b/src/pages/package/FundRecordList.vue index aa56c66..6aa47e5 100644 --- a/src/pages/package/FundRecordList.vue +++ b/src/pages/package/FundRecordList.vue @@ -26,7 +26,7 @@
-
+
diff --git a/src/pages/package/PackageConfigEdit.vue b/src/pages/package/PackageConfigEdit.vue index ecb2ee6..1458da3 100644 --- a/src/pages/package/PackageConfigEdit.vue +++ b/src/pages/package/PackageConfigEdit.vue @@ -25,7 +25,7 @@
产品权益至少一项,数量最多三位小数
-
+
删除
@@ -337,7 +337,7 @@ onUnmounted(() => navBarStore.clearRight()); font-size: .8125rem; } - .item-card { + .item { gap: .5rem; padding: .75rem; margin-bottom: .75rem; diff --git a/src/pages/package/PackageConfigList.vue b/src/pages/package/PackageConfigList.vue index b74eb97..03f14d8 100644 --- a/src/pages/package/PackageConfigList.vue +++ b/src/pages/package/PackageConfigList.vue @@ -38,7 +38,7 @@
-
+
diff --git a/src/pages/package/PackageDeliveryList.vue b/src/pages/package/PackageDeliveryList.vue index fda1aae..596f171 100644 --- a/src/pages/package/PackageDeliveryList.vue +++ b/src/pages/package/PackageDeliveryList.vue @@ -22,7 +22,7 @@
-
+
diff --git a/src/pages/point/PointAccountList.vue b/src/pages/point/PointAccountList.vue index 062be9d..4f4c3b7 100644 --- a/src/pages/point/PointAccountList.vue +++ b/src/pages/point/PointAccountList.vue @@ -35,7 +35,7 @@ -
+
@@ -249,7 +249,7 @@ onMounted(async () => { } } - .load-state { + .center-tips { color: #52607a; padding: 1rem; font-size: .8125rem; @@ -261,7 +261,7 @@ onMounted(async () => { .point-account-list { .description, .zero, - .load-state { + .center-tips { color: #94a3b8; } diff --git a/src/pages/point/PointLedgerList.vue b/src/pages/point/PointLedgerList.vue index 6c44fee..415a847 100644 --- a/src/pages/point/PointLedgerList.vue +++ b/src/pages/point/PointLedgerList.vue @@ -113,7 +113,7 @@ -
+
diff --git a/src/pages/point/PointRuleList.vue b/src/pages/point/PointRuleList.vue index 61daf6b..44c7cb9 100644 --- a/src/pages/point/PointRuleList.vue +++ b/src/pages/point/PointRuleList.vue @@ -51,7 +51,7 @@ -
+
diff --git a/src/pages/store/StoreDiaryList.vue b/src/pages/store/StoreDiaryList.vue index 5f5ff70..0244fb8 100644 --- a/src/pages/store/StoreDiaryList.vue +++ b/src/pages/store/StoreDiaryList.vue @@ -40,7 +40,7 @@ -
+
@@ -462,7 +462,7 @@ onUnmounted(() => navBarStore.clearRight()); } } - .load-state { + .center-tips { padding: 1rem; color: var(--td-text-color-placeholder); font-size: .8125rem; diff --git a/src/pages/store/StoreList.vue b/src/pages/store/StoreList.vue index f011c4f..9a9c492 100644 --- a/src/pages/store/StoreList.vue +++ b/src/pages/store/StoreList.vue @@ -44,8 +44,8 @@ -
-
+
+
-
+
{ text-align: center; } - .load-state { - color: #52607a; - padding: .75rem 1rem 1rem; - text-align: center; - } - } .edit-dialog { @@ -360,7 +354,7 @@ onUnmounted(() => { .tag-list { .empty, - .load-state { + .center-tips { color: #94a3b8; } } diff --git a/src/pages/user/UserList.vue b/src/pages/user/UserList.vue index 7569427..cb5b805 100644 --- a/src/pages/user/UserList.vue +++ b/src/pages/user/UserList.vue @@ -42,8 +42,8 @@ -
-
+
+