diff --git a/src/components/BirthdatePicker.vue b/src/components/BirthdatePicker.vue index fb96332..1ee4505 100644 --- a/src/components/BirthdatePicker.vue +++ b/src/components/BirthdatePicker.vue @@ -227,6 +227,7 @@ function clearValue(): void { .mode { gap: .5rem; display: flex; + align-items: center; } .value { diff --git a/src/components/BottomPopup.vue b/src/components/BottomPopup.vue index 2cd3cd5..4e07b6f 100644 --- a/src/components/BottomPopup.vue +++ b/src/components/BottomPopup.vue @@ -44,9 +44,9 @@ interface Props { const props = withDefaults(defineProps(), { modelValue: false, - leftButtonText: "", - rightButtonText: "", - height: "50vh", + leftButtonText: "取消", + rightButtonText: "确定", + height: "auto", title: "" }); @@ -89,18 +89,18 @@ defineSlots<{ diff --git a/src/types/EventItem.ts b/src/types/EventItem.ts index d1967af..378aeeb 100644 --- a/src/types/EventItem.ts +++ b/src/types/EventItem.ts @@ -2,6 +2,9 @@ import type { Model } from "timi-web"; export type EventStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED"; +export type EventValueType = "NONE" | "NUMBER"; +export type EventValueUnit = "CENT" | "COUNT"; + export type EventLimitType = "NONE" | "DAILY_HALF_DAY" | "NATURAL_DAY" | "INTERVAL_HOUR" | "NATURAL_MONTH" | "TOTAL_LIMIT" | "CUSTOMER_TOTAL_LIMIT"; export type EventItem = { @@ -9,6 +12,9 @@ export type EventItem = { name: string; description?: string; color?: string; + valueType?: EventValueType; + valueUnit?: EventValueUnit | null; + requireValue?: boolean; status?: EventStatus; requirePhoto?: boolean; limitType?: EventLimitType; diff --git a/src/types/EventRecord.ts b/src/types/EventRecord.ts index b9240fd..0d2d0ca 100644 --- a/src/types/EventRecord.ts +++ b/src/types/EventRecord.ts @@ -7,7 +7,9 @@ export type EventRecord = { customerId?: string; customerIdList?: string[]; eventId?: string; - pointValue?: number; + pointValue?: number | null; + numberValue?: number | null; + earnedPointValue?: number | null; operatorUserId?: string; remark?: string; registeredAt?: number; diff --git a/src/types/Point.ts b/src/types/Point.ts index 4466121..c1c6a25 100644 --- a/src/types/Point.ts +++ b/src/types/Point.ts @@ -2,8 +2,8 @@ import type { BasePage, Model } from "timi-web"; import type { Customer } from "@/types/Customer"; export type PointRuleStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED"; -export type PointRuleTriggerType = "EVERY_TIME" | "FIRST_TIME" | "AFTER_COUNT_EVERY_TIME" | "SOURCE_VALUE"; -export type PointRuleMode = "FIXED" | "SOURCE_VALUE"; +export type PointRuleTriggerType = "EVERY_TIME" | "FIRST_TIME" | "AFTER_COUNT_EVERY_TIME" | "SOURCE_VALUE" | "CUSTOMER_EVENT_VALUE_TIER"; +export type PointRuleMode = "FIXED" | "SOURCE_VALUE" | "TIER"; export type PointLedgerChangeType = "EARN" | "REVOKE" | "ADJUST"; export type PointLedgerDirection = "INCREASE" | "DECREASE"; @@ -13,6 +13,26 @@ export type PointRuleCondition = { threshold?: number; includeCurrent?: boolean; scope?: "CUSTOMER_EVENT"; + sourceEventId?: string; + sourceValueField?: "numberValue"; + window?: { + type: "ROLLING"; + amount: number; + unit: "YEAR"; + }; + tiers?: PointRuleTier[]; +}; + +export type PointRuleTier = { + min: number; + max?: number | null; + pointValue: number; +}; + +export type PointRuleTierForm = { + key: number; + maxText: string; + pointValueText: string; }; export type PointAccount = { diff --git a/src/utils/eventValue.ts b/src/utils/eventValue.ts new file mode 100644 index 0000000..8bb44a1 --- /dev/null +++ b/src/utils/eventValue.ts @@ -0,0 +1,76 @@ +import type { EventItem } from "@/types/EventItem"; + +/** + * 返回事件数值单位的前端文案。 + * 后端仍使用 CENT 表示金额分,前端统一按元展示和录入。 + */ +export function getEventValueUnitText(valueUnit?: EventItem["valueUnit"]): string { + if (valueUnit === "CENT") { + return "元"; + } + if (valueUnit === "COUNT") { + return "次"; + } + return "未配置单位"; +} + +/** + * 获取事件数值输入提示。 + */ +export function getEventNumberValuePlaceholder(event: EventItem): string { + if (event.valueUnit === "CENT") { + return event.requireValue ? "请输入金额(元),最多两位小数" : "请输入金额(元),可留空,最多两位小数"; + } + return event.requireValue ? "请输入次数,必须是大于等于 0 的整数" : "请输入次数,可留空,必须是大于等于 0 的整数"; +} + +/** + * 将前端输入的事件数值转换为后端要求的整数。 + * 金额使用字符串拆分后转换为分,避免直接使用浮点数放大导致精度误差。 + */ +export function resolveEventNumberValue(event: EventItem, valueText: string): number | undefined { + const normalizedValue = valueText.trim(); + if (!normalizedValue) { + return undefined; + } + if (event.valueUnit === "CENT") { + return resolveCentValue(normalizedValue); + } + if (!/^\d+$/.test(normalizedValue)) { + return undefined; + } + const value = Number(normalizedValue); + return Number.isSafeInteger(value) ? value : undefined; +} + +/** + * 将后端返回的整数分转换为前端金额文案。 + */ +export function formatEventNumberValue(event: EventItem | undefined, value?: number | null): string { + if (value === undefined || value === null) { + return "-"; + } + if (event?.valueUnit !== "CENT") { + return `${value}${event?.valueUnit === "COUNT" ? " 次" : ""}`; + } + + const centText = String(Math.trunc(value)); + const yuanText = centText.length < 3 ? "0" : centText.slice(0, -2); + const decimalText = centText.slice(-2).padStart(2, "0").replace(/0+$/, ""); + return `${yuanText}${decimalText ? `.${decimalText}` : ""} 元`; +} + +function resolveCentValue(valueText: string): number | undefined { + const match = /^(\d+)(?:\.(\d{0,2}))?$/.exec(valueText); + if (!match) { + return undefined; + } + + const integerText = match[1]; + const decimalText = (match[2] || "").padEnd(2, "0"); + const centValue = BigInt(integerText) * 100n + BigInt(decimalText || "0"); + if (centValue > BigInt(Number.MAX_SAFE_INTEGER)) { + return undefined; + } + return Number(centValue); +} diff --git a/src/utils/pointRule.ts b/src/utils/pointRule.ts new file mode 100644 index 0000000..fbbaeba --- /dev/null +++ b/src/utils/pointRule.ts @@ -0,0 +1,29 @@ +import type { PointRuleTierForm } from "@/types/Point"; + +export function resolveNonNegativeInteger(value: string): number | undefined { + if (!value.trim()) { + return undefined; + } + const numberValue = Number(value); + return Number.isSafeInteger(numberValue) && !(numberValue < 0) ? numberValue : undefined; +} + +export function getTierMinValue(tierList: PointRuleTierForm[], index: number): number | undefined { + if (index === 0) { + return 0; + } + const previousMax = resolveNonNegativeInteger(tierList[index - 1]?.maxText || ""); + if (previousMax === undefined || !Number.isSafeInteger(previousMax + 1)) { + return undefined; + } + return previousMax + 1; +} + +export function getTierMinText(tierList: PointRuleTierForm[], index: number): string { + const min = getTierMinValue(tierList, index); + return min === undefined ? "待填写" : String(min); +} + +export function isLastTier(tierList: PointRuleTierForm[], index: number): boolean { + return index === tierList.length - 1; +}