update point tier rule

This commit is contained in:
Timi
2026-08-14 18:26:11 +08:00
parent 3b5ff75772
commit a43694daa3
16 changed files with 940 additions and 32 deletions
+106 -4
View File
@@ -1,6 +1,6 @@
<template>
<div v-can="pageAccess" class="registration-event-edit">
<t-form class="form" label-align="left">
<t-form class="form" label-align="left" label-width="7rem">
<t-form-item label="颜色">
<t-button
size="extra-small"
@@ -33,6 +33,15 @@
borderless
/>
</t-form-item>
<t-form-item label="数值类型" arrow @click="openValueTypePicker">
<div class="picker-text" v-text="getValueTypeText(formModel.valueType)" />
</t-form-item>
<t-form-item v-if="isNumberValueType(formModel.valueType)" label="数值单位" required-mark arrow @click="openValueUnitPicker">
<div class="picker-text" v-text="getValueUnitText(formModel.valueUnit)" />
</t-form-item>
<t-form-item v-if="isNumberValueType(formModel.valueType)" label="登记必填数值" content-align="right">
<t-switch v-model="formModel.requireValue" />
</t-form-item>
<t-form-item label="起始时间" arrow @click="openBeginAtPicker">
<div class="begin-at">
<div class="picker-text" v-text="formModel.beginAtText || '不限'" />
@@ -161,6 +170,24 @@
@confirm="confirmStatusPicker"
/>
</t-popup>
<t-popup v-model="valueTypePickerVisible" placement="bottom">
<t-picker
v-model="valueTypePickerDraft"
:columns="valueTypePickerColumns"
title="数值类型"
@cancel="valueTypePickerVisible = false"
@confirm="confirmValueType"
/>
</t-popup>
<t-popup v-model="valueUnitPickerVisible" placement="bottom">
<t-picker
v-model="valueUnitPickerDraft"
:columns="valueUnitPickerColumns"
title="数值单位"
@cancel="valueUnitPickerVisible = false"
@confirm="confirmValueUnit"
/>
</t-popup>
<t-popup v-model="beginAtPickerVisible" placement="bottom">
<div class="picker-popup">
<t-date-time-picker
@@ -198,7 +225,8 @@ import type { ColorObject } from "tdesign-mobile-vue";
import { useUserStore, type Role } from "timi-web";
import EventAPI from "@/api/EventAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { EventItem, EventStatus, EventLimitType } from "@/types/EventItem";
import type { EventItem, EventStatus, EventLimitType, EventValueType, EventValueUnit } from "@/types/EventItem";
import { getErrorMessage } from "@/utils/error";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
type PickerValue = string | number;
@@ -239,6 +267,9 @@ const formModel = reactive({
name: "",
description: "",
color: "#0f766e",
valueType: "NONE" as EventValueType,
valueUnit: null as EventValueUnit | null,
requireValue: false,
status: "ACTIVE" as EventStatus,
requirePhoto: false,
limitType: "NONE" as EventLimitType,
@@ -400,6 +431,71 @@ function getStatusText(value: EventStatus): string {
return statusOptions.find(option => option.value === value)?.label || "可登记";
}
// ---------- 数值定义 ----------
const valueTypeOptions: Array<{ label: string; value: EventValueType }> = [
{ label: "不携带数值", value: "NONE" },
{ label: "携带数值", value: "NUMBER" }
];
const valueUnitOptions: Array<{ label: string; value: EventValueUnit }> = [
{ label: "金额(元)", value: "CENT" },
{ label: "次数", value: "COUNT" }
];
const valueTypePickerVisible = ref(false);
const valueTypePickerDraft = ref<PickerValue[]>([]);
const valueTypePickerColumns = [valueTypeOptions];
const valueUnitPickerVisible = ref(false);
const valueUnitPickerDraft = ref<PickerValue[]>([]);
const valueUnitPickerColumns = [valueUnitOptions];
function isNumberValueType(value: EventValueType): boolean {
return value === "NUMBER";
}
function getValueTypeText(value: EventValueType): string {
return valueTypeOptions.find(option => option.value === value)?.label || "不携带数值";
}
function getValueUnitText(value: EventValueUnit | null): string {
return valueUnitOptions.find(option => option.value === value)?.label || "请选择单位";
}
function openValueTypePicker(): void {
valueTypePickerDraft.value = [formModel.valueType];
valueTypePickerVisible.value = true;
}
function confirmValueType(value: Array<string | number>): void {
const valueType = value[0];
if (valueType !== "NONE" && valueType !== "NUMBER") {
return;
}
formModel.valueType = valueType;
if (valueType === "NONE") {
formModel.valueUnit = null;
formModel.requireValue = false;
} else if (!formModel.valueUnit) {
formModel.valueUnit = "CENT";
}
valueTypePickerVisible.value = false;
}
function openValueUnitPicker(): void {
if (!isNumberValueType(formModel.valueType)) {
return;
}
valueUnitPickerDraft.value = [formModel.valueUnit || "CENT"];
valueUnitPickerVisible.value = true;
}
function confirmValueUnit(value: Array<string | number>): void {
const valueUnit = value[0];
if (valueUnit === "CENT" || valueUnit === "COUNT") {
formModel.valueUnit = valueUnit;
}
valueUnitPickerVisible.value = false;
}
// ---------- 次数限制 ----------
function openLimitPicker(): void {
@@ -539,6 +635,9 @@ async function saveEventItem(): Promise<void> {
name: formModel.name.trim(),
description: formModel.description,
color: formModel.color,
valueType: formModel.valueType,
valueUnit: isNumberValueType(formModel.valueType) ? formModel.valueUnit : null,
requireValue: isNumberValueType(formModel.valueType) && formModel.requireValue,
status: formModel.status,
requirePhoto: formModel.requirePhoto,
limitType: formModel.limitType,
@@ -554,8 +653,8 @@ async function saveEventItem(): Promise<void> {
}
Toast.success("登记事件已保存");
await router.replace("/management/registration-events");
} catch {
Toast.error("登记事件保存失败");
} catch (error) {
Toast.error(getErrorMessage(error, "登记事件保存失败"));
} finally {
isSaving.value = false;
}
@@ -629,6 +728,9 @@ async function fillForm(): Promise<void> {
name: current.name || "",
description: current.description || "",
color: current.color || "#2f88ff",
valueType: current.valueType || "NONE",
valueUnit: current.valueUnit || null,
requireValue: current.requireValue ?? false,
status: current.status === "DRAFT" ? "DRAFT" : current.status === "INACTIVE" ? "INACTIVE" : "ACTIVE",
requirePhoto: current.requirePhoto ?? false,
limitType: current.limitType || "NONE",
+10
View File
@@ -57,6 +57,7 @@
<span v-if="!(item.roleCodeList || []).length" class="muted" v-text="'未配置'" />
</div>
<div class="line" v-text="getLimitText(item)" />
<div class="line" v-text="getValueDefinitionText(item)" />
</div>
</template>
</t-cell>
@@ -81,6 +82,7 @@ import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem, EventLimitType, EventStatus } from "@/types/EventItem";
import { getEventValueUnitText } from "@/utils/eventValue";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({
@@ -199,6 +201,14 @@ function getLimitText(item: EventItem): string {
return limitTextMap[item.limitType || "NONE"];
}
function getValueDefinitionText(item: EventItem): string {
if (item.valueType !== "NUMBER") {
return "不携带数值";
}
const unitText = getEventValueUnitText(item.valueUnit);
return `整数数值|${unitText}${item.requireValue ? "|登记必填" : "|登记可选"}`;
}
function resolveLimitValue(value?: number): number {
if (value !== undefined && Number.isFinite(value) && 0 < value) {
return Math.floor(value);
+8
View File
@@ -8,6 +8,9 @@
/>
<t-cell-group v-if="record" title="登记信息" theme="card">
<t-cell title="登记事件" :note="record.event?.name || '未知事件'" />
<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="`${record.pointValue} 分`" />
<t-cell v-if="record.earnedPointValue !== undefined && record.earnedPointValue !== null" title="本次获得积分" :note="`${record.earnedPointValue} 分`" />
<t-cell title="登记时间" :note="record.registeredAt ? Time.toPassedDateTime(record.registeredAt) : '-'" />
<t-cell title="登记人" :note="Text.display(record.operator?.nick || record.operator?.name)" />
<t-cell title="客户编码" :note="Text.display(record.customer?.code)" />
@@ -33,6 +36,7 @@ import PhotoScroller from "@/components/PhotoScroller.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventRecord } from "@/types/EventRecord";
import { getEventRecordPhotoURL } from "@/utils/eventRecord";
import { formatEventNumberValue } from "@/utils/eventValue";
import { permissionCodes } from "@/utils/permission";
defineOptions({
@@ -55,6 +59,10 @@ const recordPhotoList = computed(() => {
return url ? [url] : [];
});
function getNumberValueText(value: EventRecord): string {
return formatEventNumberValue(value.event, value.numberValue);
}
// ---------- 页面数据 ----------
async function loadRecord(): Promise<void> {
+78
View File
@@ -50,6 +50,23 @@
:disabled="isEditMode"
required-mark
/>
<div v-if="!isEditMode && selectedNumberEventList.length" class="event-value-list">
<t-form-item
v-for="item in selectedNumberEventList"
:key="item.id"
:label="`${item.name || '未命名事件'}数值`"
:required-mark="item.requireValue === true"
label-align="top"
>
<t-input
v-model.trim="numberValueTextMap[item.id || '']"
type="number"
align="right"
borderless
:placeholder="getNumberValuePlaceholder(item)"
/>
</t-form-item>
</div>
<div v-if="isEditMode" class="edit-point-tip">已产生积分的登记记录不会因修改客户、事件或时间而重算;如需修改这些事实,请删除后重新创建。</div>
<t-form-item v-if="showSourceValueInput" label="自定义积分" required-mark>
<t-input
@@ -182,6 +199,7 @@ import type { Customer } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem";
import type { EventRecord } from "@/types/EventRecord";
import { getEventRecordPhotoURL } from "@/utils/eventRecord";
import { getEventNumberValuePlaceholder, resolveEventNumberValue } from "@/utils/eventValue";
import { permissionCodes } from "@/utils/permission";
import { loadSourceValueRange } from "@/utils/point";
import { compressUploadImageList } from "@/utils/imageCompression";
@@ -372,6 +390,10 @@ function handleScanStatusChange(value: string): void {
const eventList = ref<EventItem[]>([]);
const selectedEventIdList = ref<string[]>([]);
const numberValueTextMap = reactive<Record<string, string>>({});
const selectedNumberEventList = computed(() => selectedEventIdList.value
.map(eventId => eventList.value.find(item => item.id === eventId))
.filter((item): item is EventItem => item?.valueType === "NUMBER"));
const eventNameMap = computed(() => {
const map = new Map<string, string>();
@@ -391,6 +413,46 @@ function appendRecordEvent(record: EventRecord): void {
eventList.value = [record.event, ...eventList.value];
}
function getNumberValuePlaceholder(event: EventItem): string {
return getEventNumberValuePlaceholder(event);
}
function resolveNumberValue(eventId: string): number | undefined {
const valueText = numberValueTextMap[eventId] || "";
if (!valueText.trim()) {
return undefined;
}
const event = eventList.value.find(item => item.id === eventId);
return event ? resolveEventNumberValue(event, valueText) : undefined;
}
function validateNumberValueList(): boolean {
for (const eventId of selectedEventIdList.value) {
const event = eventList.value.find(item => item.id === eventId);
if (!event) {
continue;
}
const valueText = numberValueTextMap[eventId] || "";
if (event.valueType !== "NUMBER") {
if (valueText.trim()) {
Toast.warning(`${event.name || "当前事件"}不能提交数值`);
return false;
}
continue;
}
const numberValue = resolveNumberValue(eventId);
if (valueText.trim() && numberValue === undefined) {
Toast.warning(`${event.name || "当前事件"}数值格式不正确,请检查单位和小数位`);
return false;
}
if (event.requireValue && numberValue === undefined) {
Toast.warning(`请填写${event.name || "当前事件"}数值`);
return false;
}
}
return true;
}
// ---------- 积分规则联动 ----------
const pointValueText = ref("");
@@ -533,6 +595,7 @@ async function submitRecord(): Promise<void> {
}
const customerIdList = selectedCustomerList.value.map(customer => customer.id).filter((item): item is string => !!item);
// 多个事件按选择顺序逐条提交,避免把有累计积分顺序依赖的事件混入同一批次。
for (const eventId of selectedEventIdList.value) {
await EventRecordAPI.create({
eventId,
@@ -540,6 +603,7 @@ async function submitRecord(): Promise<void> {
remark: note.value || "客户现场登记",
registeredAt: buildRegisteredAt(),
pointValue: showSourceValueInput.value ? resolveSourceValue() : undefined,
numberValue: resolveNumberValue(eventId),
attachmentList: buildTempFileIdList().map(tempFileId => ({
tempFileId,
attachType: "PHOTO"
@@ -551,6 +615,9 @@ async function submitRecord(): Promise<void> {
note.value = "";
pointValueText.value = "";
photoItemList.value = [];
for (const eventId of Object.keys(numberValueTextMap)) {
delete numberValueTextMap[eventId];
}
await loadRecentRecordList();
await settingStore.playRegisterSuccessSound();
Toast.success("登记已提交");
@@ -569,6 +636,9 @@ function validateForm(): boolean {
Toast.warning("请至少选择一个登记事件");
return false;
}
if (!isEditMode.value && !validateNumberValueList()) {
return false;
}
if (showSourceValueInput.value) {
const pointValue = resolveSourceValue();
const range = sourceValueRange.value;
@@ -710,6 +780,14 @@ watch(customerCode, async (value) => {
margin-top: 1rem;
}
.event-value-list {
padding: 0 1rem;
.t-form-item {
padding: 0;
}
}
.time-actions {
gap: .5rem;
display: flex;