update EventRecordEdit.vue style

This commit is contained in:
Timi
2026-09-01 16:27:53 +08:00
parent bfea58f714
commit 9c5306d8e4
30 changed files with 583 additions and 621 deletions
@@ -6,7 +6,7 @@
</t-cell-group>
<section v-if="customerPackage" class="items" v-entrance-transition="70">
<div class="section-title"><strong>本次发放权益</strong><span>只能提交不超过可发数量的产品</span></div>
<div v-for="item in customerPackage.itemList || []" :key="item.id" class="item-card">
<div v-for="item in customerPackage.itemList || []" :key="item.id" class="item">
<div class="item-title"><strong v-text="`${item.productNameSnapshot}(${item.unitSnapshot})`" /><span v-text="`可发 ${getPackageQuantityText(item.availableQuantity)}`" /></div>
<t-input v-model.trim="quantityMap[item.id || '']" type="number" borderless placeholder="本次发放数量,不填表示不发放" />
</div>
@@ -202,7 +202,7 @@ onUnmounted(() => navBarStore.clearRight());
margin-left: .5rem;
}
.item-card {
.item {
gap: .5rem;
padding: .75rem;
margin-bottom: .75rem;
+1 -19
View File
@@ -31,21 +31,17 @@
</t-cell>
</t-cell-group>
<div v-else class="empty" v-text="loading ? '正在加载客户套餐' : '暂无客户套餐'" />
<div class="load-state" v-text="loadStateText" />
<div class="center-tips" v-text="loadStateText" />
</t-pull-down-refresh>
</div>
<nav-bar-action-menu v-if="canCreate" :actions="actionList" />
</template>
<script setup lang="ts">
import { type DropdownValue } from "tdesign-mobile-vue";
import { Time } from "timi-web";
import { useUserStore } from "timi-web-api";
import PackageAPI from "@/api/PackageAPI";
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { CustomerPackageStatus, GaoCustomerPackage, PaymentStatus } from "@/types/Package";
import type { NavBarAction } from "@/types/NavBarAction";
import { getErrorMessage } from "@/utils/error";
import { formatPackageAmount, getCustomerPackageStatusText, getPaymentStatusText } from "@/utils/package";
import { Message } from "@/utils/message";
@@ -55,15 +51,6 @@ defineOptions({ name: "CustomerPackageList" });
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.customerPackage.create));
const actionList = computed<NavBarAction[]>(() => canCreate.value ? [{
key: "purchase",
text: "购买套餐",
icon: "add",
theme: "primary",
action: toPurchase
}] : []);
const keyword = ref("");
const status = ref("ALL");
const paymentStatus = ref("ALL");
@@ -137,10 +124,6 @@ async function selectPaymentStatus(value: DropdownValue): Promise<void> {
await reload();
}
async function toPurchase(): Promise<void> {
await router.push("/management/customer-packages/purchase");
}
async function toDetail(item: GaoCustomerPackage): Promise<void> {
if (item.id) {
await router.push(`/management/customer-packages/${item.id}`);
@@ -153,7 +136,6 @@ function getStatusTheme(statusValue?: CustomerPackageStatus): "success" | "warni
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setCustomRightVisible(canCreate.value);
try {
await reload();
} catch (error) {
@@ -1,405 +0,0 @@
<template>
<div v-can="permissionCodes.customerPackage.create" class="customer-package-purchase">
<t-form class="form" label-align="left" label-width="6rem" v-entrance-transition>
<t-form-item v-if="isGlobalManager" label="所属门店" arrow @click="openStorePicker">
<span class="picker-text" v-text="selectedStoreName" />
</t-form-item>
<t-form-item label="客户" required-mark arrow @click="customerSelectorVisible = true">
<span class="picker-text" v-text="selectedCustomer?.name || selectedCustomer?.code || '请选择客户'" />
</t-form-item>
<customer-profile-card v-if="selectedCustomer" class="customer-profile" :customer="selectedCustomer" />
<t-form-item label="购买事件" required-mark arrow @click="openEventPicker">
<span class="picker-text" v-text="selectedEvent?.name || '请选择 PACKAGE_PURCHASE 事件'" />
</t-form-item>
<div v-if="selectedConfig" class="config-summary">
<strong v-text="selectedConfig.name || '未命名套餐'" />
<span v-text="`标准价:${formatPackageAmount(selectedConfig.standardAmount)}|${getPackageConfigStatusText(selectedConfig.status)}`" />
</div>
<div v-if="!eventList.length" class="option-tip">
<div v-if="!eventList.length">当前门店没有可用的 PACKAGE_PURCHASE 购买事件,请先新增并启用购买事件。</div>
</div>
<div v-if="selectedEvent && !selectedConfig" class="option-tip">当前购买事件未返回绑定套餐,无法购买,请联系管理员检查事件配置。</div>
<div v-if="selectedConfig && !isSelectedConfigSaleable" class="option-tip">事件绑定的套餐当前不可购买,请检查套餐状态和有效期。</div>
<t-form-item label="成交价(元)" required-mark>
<t-input v-model.trim="saleAmountText" type="number" align="right" borderless :readonly="!canPriceOverride" placeholder="请输入成交价" />
</t-form-item>
<t-form-item label="购买时间" arrow @click="openPurchaseTime">
<span class="picker-text" v-text="purchasedAtText || '使用当前时间'" />
</t-form-item>
<t-form-item label="备注">
<t-textarea v-model.trim="remark" borderless autosize maxlength="500" placeholder="购买备注" />
</t-form-item>
</t-form>
<section class="items" v-entrance-transition="70">
<div class="section-title"><strong>购买权益明细</strong><span v-if="!canPriceOverride">当前账号无定制权限,明细不可修改</span></div>
<div v-for="(item, index) in purchaseItems" :key="item.clientKey" class="item-card">
<strong v-text="`权益 ${index + 1}`" />
<t-input v-model.trim="item.productName" borderless readonly placeholder="产品名称" />
<t-input v-model.trim="item.unit" borderless readonly placeholder="单位" />
<t-input v-model.trim="item.totalQuantity" type="number" borderless :readonly="!canPriceOverride" placeholder="总权益数量" />
<span v-if="item.remark" class="item-remark" v-text="`备注:${item.remark}`" />
</div>
<div v-if="!purchaseItems.length" class="empty" v-text="'选择购买事件后加载绑定套餐权益明细'" />
</section>
<package-photo-field v-model="photoItems" :required="selectedEvent?.requirePhoto === true" title="购买照片" />
<customer-selector v-model="selectedCustomerId" v-model:visible="customerSelectorVisible" :store-id="formStoreId" @change="handleCustomerChange" />
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker v-model="storeDraft" :columns="storePickerColumns" title="选择门店" @cancel="storePickerVisible = false" @confirm="confirmStore" />
</t-popup>
<t-popup v-model="eventPickerVisible" placement="bottom">
<t-picker v-model="eventDraft" :columns="eventPickerColumns" title="选择购买事件" @cancel="eventPickerVisible = false" @confirm="confirmEvent" />
</t-popup>
<t-popup v-model="purchasePickerVisible" placement="bottom">
<t-date-time-picker v-model="purchaseDraft" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="购买时间" @cancel="purchasePickerVisible = false" @confirm="confirmPurchaseTime" />
</t-popup>
</div>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import { useUserStore } from "timi-web-api";
import CustomerAPI from "@/api/CustomerAPI";
import EventAPI from "@/api/EventAPI";
import PackageAPI from "@/api/PackageAPI";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import PackagePhotoField from "@/components/PackagePhotoField.vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { Customer } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem";
import type { GaoPackageConfig, PackageAttachment, PackagePurchaseItem } from "@/types/Package";
import type { PhotoItem } from "@/types/Photo";
import { createPackageRequestNo, formatPackageAmount, formatPackageAmountInput, getPackageConfigStatusText, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
type PickerValue = string | number;
type PurchaseFormItem = PackagePurchaseItem & { clientKey: string };
defineOptions({ name: "CustomerPackagePurchase" });
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
const navBarStore = useNavBarStore();
const currentStoreStore = useCurrentStoreStore();
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const canPriceOverride = computed(() => userStore.hasPermissions(permissionCodes.customerPackage.priceOverride));
const formStoreId = ref("");
const selectedCustomerId = ref("");
const selectedCustomer = ref<Customer>();
const selectedEventId = ref("");
const eventList = ref<EventItem[]>([]);
const purchaseItems = ref<PurchaseFormItem[]>([]);
const selectedEvent = computed(() => eventList.value.find(item => item.id === selectedEventId.value));
const selectedConfig = computed<GaoPackageConfig | undefined>(() => selectedEvent.value?.packageConfig);
const isSelectedConfigSaleable = computed(() => {
const config = selectedConfig.value;
if (!config || config.status !== "ACTIVE") {
return false;
}
const now = Date.now();
const beginAt = config.beginAt;
const endAt = config.endAt;
return (beginAt === undefined || beginAt === null || beginAt <= now)
&& (endAt === undefined || endAt === null || now <= endAt);
});
const saleAmountText = ref("");
const purchasedAtText = ref("");
const purchaseDraft = ref("");
const remark = ref("");
const photoItems = ref<PhotoItem[]>([]);
const isSaving = ref(false);
const customerSelectorVisible = ref(false);
const storePickerVisible = ref(false);
const eventPickerVisible = ref(false);
const purchasePickerVisible = ref(false);
const storeDraft = ref<PickerValue[]>([]);
const eventDraft = ref<PickerValue[]>([]);
const storePickerColumns = computed(() => [[...currentStoreStore.storeList].map(item => ({ label: item.name || item.code || item.id || "未知门店", value: item.id || "" }))]);
const eventPickerColumns = computed(() => [[...eventList.value].map(item => ({ label: item.name || "未命名事件", value: item.id || "" }))]);
const selectedStoreName = computed(() => currentStoreStore.storeList.find(item => item.id === formStoreId.value)?.name || formStoreId.value || "请选择门店");
async function loadOptions(): Promise<void> {
eventList.value = formStoreId.value
? (await EventAPI.listValid(formStoreId.value)).filter(item => item.bizType === "PACKAGE_PURCHASE" && !!item.packageConfigId)
: [];
if (!eventList.value.some(item => item.id === selectedEventId.value)) {
selectedEventId.value = "";
resetPurchaseConfig();
}
}
async function handleCustomerChange(id: string): Promise<void> {
selectedCustomerId.value = id;
selectedCustomer.value = id ? await CustomerAPI.detail(id) : undefined;
}
function openStorePicker(): void {
storeDraft.value = [formStoreId.value];
storePickerVisible.value = true;
}
function openEventPicker(): void {
if (!eventList.value.length) {
Message.warning("当前门店没有 PACKAGE_PURCHASE 类型购买事件,请先启用购买事件");
return;
}
eventDraft.value = [selectedEventId.value];
eventPickerVisible.value = true;
}
async function confirmStore(value: Array<string | number>): Promise<void> {
const storeId = String(value[0] || "");
if (storeId && storeId !== formStoreId.value) {
formStoreId.value = storeId;
selectedCustomerId.value = "";
selectedCustomer.value = undefined;
await loadOptions();
}
storePickerVisible.value = false;
}
function resetPurchaseConfig(): void {
saleAmountText.value = "";
purchaseItems.value = [];
}
function applySelectedEvent(): void {
const config = selectedConfig.value;
if (!config) {
resetPurchaseConfig();
return;
}
saleAmountText.value = formatPackageAmountInput(config.standardAmount);
purchaseItems.value = (config.itemList || []).map((item, index) => ({
clientKey: window.crypto.randomUUID(),
productName: item.productName,
unit: item.unit,
totalQuantity: normalizePackageQuantity(item.defaultQuantity) || "",
sort: item.sort ?? index,
remark: item.remark ?? null
}));
}
function confirmEvent(value: Array<string | number>): void {
selectedEventId.value = String(value[0] || "");
applySelectedEvent();
eventPickerVisible.value = false;
}
function openPurchaseTime(): void {
purchaseDraft.value = purchasedAtText.value || Time.formatDateTime(new Date());
purchasePickerVisible.value = true;
}
function confirmPurchaseTime(value: string | number): void {
purchasedAtText.value = String(value);
purchasePickerVisible.value = false;
}
function resolveTimestamp(value: string): number | undefined {
if (!value) {
return undefined;
}
const timestamp = new Date(value.replace(" ", "T")).getTime();
return Number.isFinite(timestamp) ? timestamp : undefined;
}
function validateForm(): boolean {
if (!selectedCustomerId.value) {
Message.warning("请选择客户");
return false;
}
if (!selectedEventId.value || !selectedEvent.value) {
Message.warning("请选择 PACKAGE_PURCHASE 类型购买事件");
return false;
}
if (!selectedConfig.value) {
Message.warning("当前购买事件未绑定有效套餐配置");
return false;
}
if (!isSelectedConfigSaleable.value) {
Message.warning("事件绑定的套餐当前不可购买,请检查套餐状态和有效期");
return false;
}
if (parsePackageAmount(saleAmountText.value) === undefined) {
Message.warning("成交价必须是合法的元金额,最多两位小数");
return false;
}
if (!purchaseItems.value.length) {
Message.warning("购买权益至少需要一项");
return false;
}
for (const item of purchaseItems.value) {
if (!item.productName.trim() || !item.unit.trim() || !normalizePackageQuantity(item.totalQuantity) || normalizePackageQuantity(item.totalQuantity) === "0") {
Message.warning("产品名称、单位和正数量不能为空");
return false;
}
}
if (selectedEvent.value.requirePhoto && !photoItems.value.some(item => item.tempFileId)) {
Message.warning("当前购买事件要求上传照片");
return false;
}
return true;
}
function buildAttachments(): PackageAttachment[] {
return photoItems.value.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: "PHOTO", name: item.attachment?.name }] : []);
}
async function save(): Promise<void> {
if (isSaving.value || !validateForm()) {
return;
}
isSaving.value = true;
try {
const result = await PackageAPI.purchase({
requestNo: createPackageRequestNo(),
eventId: selectedEventId.value,
customerId: selectedCustomerId.value,
saleAmount: parsePackageAmount(saleAmountText.value)!,
purchasedAt: resolveTimestamp(purchasedAtText.value),
itemList: purchaseItems.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: buildAttachments(),
remark: remark.value.trim() || null
});
Message.success("套餐购买已提交");
if (result.id) {
await router.replace(`/management/customer-packages/${result.id}`);
return;
}
await router.replace("/management/customer-packages");
} catch (error) {
Message.error(getErrorMessage(error, "套餐购买失败,请刷新后重试"));
} finally {
isSaving.value = false;
}
}
async function loadData(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
} else {
await currentStoreStore.loadLoginStore();
}
formStoreId.value = currentStoreStore.currentStoreId;
if (!formStoreId.value) {
const store = await currentStoreStore.loadLoginStore();
formStoreId.value = store?.id || "";
}
await loadOptions();
const queryEventId = String(route.query.eventId || "");
if (queryEventId && eventList.value.some(item => item.id === queryEventId)) {
selectedEventId.value = queryEventId;
applySelectedEvent();
}
const queryCustomerId = String(route.query.customerId || "");
if (queryCustomerId) {
await handleCustomerChange(queryCustomerId);
}
}
onMounted(async () => {
if (userStore.hasPermissions(permissionCodes.customerPackage.create)) {
navBarStore.setRightText("提交购买", "var(--td-brand-color)", save);
}
try {
await loadData();
} catch (error) {
Message.error(getErrorMessage(error, "购买页面数据加载失败"));
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.customer-package-purchase {
gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
display: flex;
flex-direction: column;
.form {
border-radius: .75rem;
}
.picker-text {
width: 100%;
text-align: right;
}
.customer-profile {
margin: 0 1rem;
}
.items,
.package-photo-field {
padding: 0 1rem;
}
.section-title {
color: var(--td-text-color-secondary);
margin-bottom: .5rem;
font-size: .8125rem;
}
.section-title span {
margin-left: .5rem;
}
.item-card {
gap: .5rem;
padding: .75rem;
margin-bottom: .75rem;
display: grid;
background: var(--td-bg-color-container);
box-shadow: var(--td-shadow-1);
border-radius: .75rem;
}
.config-summary {
gap: .25rem;
padding: .75rem 1rem;
display: flex;
color: var(--td-text-color-secondary);
font-size: .8125rem;
flex-direction: column;
background: var(--td-bg-color-secondarycontainer);
strong {
color: var(--td-text-color-primary);
font-size: .9375rem;
}
}
.item-remark {
color: var(--td-text-color-secondary);
font-size: .8125rem;
}
.empty {
color: var(--td-text-color-secondary);
padding: 1rem;
text-align: center;
}
.option-tip {
padding: .75rem 1rem;
color: var(--td-text-color-secondary);
font-size: .8125rem;
line-height: 1.5;
background: var(--td-bg-color-secondarycontainer);
}
}
</style>
+1 -1
View File
@@ -26,7 +26,7 @@
</t-cell>
</t-cell-group>
<div v-else class="empty" v-text="loading ? '正在加载资金流水' : '暂无资金流水'" />
<div class="load-state" v-text="loadStateText" />
<div class="center-tips" v-text="loadStateText" />
</t-pull-down-refresh>
</div>
</template>
+2 -2
View File
@@ -25,7 +25,7 @@
</t-form>
<section class="items" v-entrance-transition="70">
<div class="section-title"><strong>产品权益</strong><span>至少一项,数量最多三位小数</span></div>
<div v-for="(item, index) in form.itemList" :key="item.clientKey" class="item-card">
<div v-for="(item, index) in form.itemList" :key="item.clientKey" class="item">
<div class="item-head"><span v-text="`权益 ${index + 1}`" /><t-button theme="danger" variant="text" size="small" @click="removeItem(index)">删除</t-button></div>
<t-input v-model="item.productName" borderless placeholder="产品名称" />
<t-input v-model.trim="item.unit" borderless placeholder="单位,例如 盒、次" />
@@ -337,7 +337,7 @@ onUnmounted(() => navBarStore.clearRight());
font-size: .8125rem;
}
.item-card {
.item {
gap: .5rem;
padding: .75rem;
margin-bottom: .75rem;
+1 -1
View File
@@ -38,7 +38,7 @@
</t-swipe-cell>
</t-cell-group>
<div v-else class="empty" v-text="loading ? '正在加载套餐配置' : '暂无套餐配置'" />
<div class="load-state" v-text="loadStateText" />
<div class="center-tips" v-text="loadStateText" />
</t-pull-down-refresh>
</div>
<nav-bar-action-menu v-if="canCreate" :actions="actionList" />
+1 -1
View File
@@ -22,7 +22,7 @@
</t-cell>
</t-cell-group>
<div v-else class="empty" v-text="loading ? '正在加载发放记录' : '暂无发放记录'" />
<div class="load-state" v-text="loadStateText" />
<div class="center-tips" v-text="loadStateText" />
</t-pull-down-refresh>
</div>
</template>