update QuickSignInPage.vue style

This commit is contained in:
Timi
2026-08-30 00:54:52 +08:00
parent 4487de9ec2
commit 320dab542a
7 changed files with 350 additions and 245 deletions
@@ -45,6 +45,10 @@ html.theme-dark body {
margin: 0 1rem; margin: 0 1rem;
} }
.content-margin {
margin: 0 1rem;
}
.t-input--password { .t-input--password {
letter-spacing: 0; letter-spacing: 0;
} }
+20 -11
View File
@@ -7,12 +7,12 @@
:key="item.id || item.name" :key="item.id || item.name"
type="button" type="button"
class="item bg-white" class="item bg-white"
:class="{selected: isSelected(item.id)}" :class="{selected: isSelected(item)}"
:title="item.name" :title="item.name"
:disabled="props.disabled || !item.id" :disabled="props.disabled || !normalizeEventId(item)"
:aria-pressed="isSelected(item.id)" :aria-pressed="isSelected(item)"
v-entrance-transition="index * 30" v-entrance-transition="index * 30"
@click="toggleEvent(item.id)" @click="toggleEvent(item)"
> >
<span <span
class="swatch" class="swatch"
@@ -20,7 +20,7 @@
aria-hidden="true" aria-hidden="true"
/> />
<span class="clip-text" v-text="item.name" /> <span class="clip-text" v-text="item.name" />
<span v-if="isSelected(item.id)" class="mark" aria-hidden="true" /> <span v-if="isSelected(item)" class="mark" aria-hidden="true" />
</button> </button>
</div> </div>
<div v-else class="empty-block">暂无可用登记事件</div> <div v-else class="empty-block">暂无可用登记事件</div>
@@ -46,20 +46,29 @@ const props = withDefaults(defineProps<Props>(), {
const modelValue = defineModel<string[]>({ required: true }); const modelValue = defineModel<string[]>({ required: true });
const defaultColor = "#2f88ff"; const defaultColor = "#2f88ff";
const isSelected = (eventId?: string) => Boolean(eventId && modelValue.value.includes(eventId)); function normalizeEventId(item: EventItem): string {
if (item.id === undefined || item.id === null) {
return "";
}
return String(item.id);
}
const toggleEvent = (eventId?: string) => { function isSelected(item: EventItem): boolean {
const eventId = normalizeEventId(item);
return Boolean(eventId && modelValue.value.includes(eventId));
}
function toggleEvent(item: EventItem): void {
const eventId = normalizeEventId(item);
if (props.disabled || !eventId) { if (props.disabled || !eventId) {
return; return;
} }
if (modelValue.value.includes(eventId)) {
if (isSelected(eventId)) {
modelValue.value = modelValue.value.filter(id => id !== eventId); modelValue.value = modelValue.value.filter(id => id !== eventId);
return; return;
} }
modelValue.value = [...modelValue.value, eventId]; modelValue.value = [...modelValue.value, eventId];
}; }
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
+2 -2
View File
@@ -8,8 +8,8 @@ import "tdesign-mobile-vue/es/style/index.css";
import "timi-web/style.css"; import "timi-web/style.css";
import "timi-tdesign-mobile/style.css"; import "timi-tdesign-mobile/style.css";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import "@/styles/tdesign-custom.css"; import "@/assets/styles/tdesign-custom.css";
import "@/styles/global.less"; import "@/assets/styles/global.less";
import TimiWebUI from "timi-web"; import TimiWebUI from "timi-web";
import { configureUserStore, Network, useUserStore, VCan, VRole } from "timi-web-api"; import { configureUserStore, Network, useUserStore, VCan, VRole } from "timi-web-api";
import type { ApiResponse } from "timi-web-api"; import type { ApiResponse } from "timi-web-api";
+239 -209
View File
@@ -1,42 +1,82 @@
<template> <template>
<div v-can="permissionCodes.eventRecord.create" class="quick-sign-in-page"> <div v-can="permissionCodes.eventRecord.create" class="quick-sign-in-page">
<button class="registration-event-select" type="button" @click="eventSelectorVisible = true"> <space class="content-margin" direction="vertical" v-entrance-transition>
<span class="required-label">登记事件</span> <div class="event-panel">
<strong v-text="selectedEventName" /> <event-selector
<t-icon name="chevron-right" /> v-if="!isEventLoading && !eventLoadFailed"
</button> :model-value="selectedEventIdList"
<code-scanner :event-list="eventList"
visible required-mark
:session-key="scannerSessionKey" @update:model-value="handleEventSelectionChange"
:status-text="scanStatus" />
active-text="请将客户二维码移入取景框" <div v-else-if="isEventLoading" class="empty-block">正在加载登记事件</div>
@decode="handleDecodedText" <div v-else class="event-load-error">
@status-change="handleScanStatusChange" <span>登记事件加载失败</span>
/> <t-button size="small" theme="primary" variant="text" @click="loadActions">重试</t-button>
</div>
</div>
<code-scanner
visible
:session-key="scannerSessionKey"
:status-text="scanStatus"
active-text="请将客户二维码移入取景框"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
</space>
<t-button
class="cell-button"
theme="primary"
:disabled="!canSubmitRegister"
:loading="submitting"
v-entrance-transition="70"
@click="doSubmit"
>登记</t-button>
<customer-profile-card <customer-profile-card
v-if="matchedCustomer" v-if="matchedCustomer"
class="content-margin"
:customer="matchedCustomer" :customer="matchedCustomer"
/> />
<div v-if="showNumberValueInput" class="number-value-panel"> <t-form
<div class="number-value-title" v-text="`${selectedEvent?.name || '事件'}数值`" /> v-if="selectedNumberEventList.length"
<t-input class="input-form"
v-model.trim="numberValueText" label-align="left"
type="number" label-width="8rem"
align="right" >
borderless <t-form-item
:placeholder="numberValuePlaceholder" v-for="item in selectedNumberEventList"
/> :key="item.id"
</div> :label="`${item.name || '未命名事件'}数值`"
<div v-if="showSourceValueInput" class="source-value-panel"> :required-mark="item.requireValue === true"
<div class="source-value-title" v-text="sourceValueAction === 'CONSUME' ? '消耗积分' : '发放积分'" /> >
<t-input <t-input
v-model.trim="pointValueText" v-model.trim="numberValueTextMap[item.id || '']"
type="number" type="number"
align="right" align="right"
borderless borderless
:placeholder="sourceValuePlaceholder" :placeholder="getNumberValuePlaceholder(item)"
/> />
</div> </t-form-item>
</t-form>
<t-form
v-if="showSourceValueInput"
class="input-form"
label-align="left"
label-width="6rem"
>
<t-form-item label="积分动作" content-align="right">
<span class="form-text" v-text="sourceValueAction === 'CONSUME' ? '消耗积分' : '发放积分'" />
</t-form-item>
<t-form-item label="积分数量" required-mark>
<t-input
v-model.trim="pointValueText"
type="number"
align="right"
borderless
:placeholder="sourceValuePlaceholder"
/>
</t-form-item>
</t-form>
<t-cell-group <t-cell-group
v-if="recentRecordList.length" v-if="recentRecordList.length"
title="最近登记记录" title="最近登记记录"
@@ -63,29 +103,7 @@
</template> </template>
</t-cell> </t-cell>
</t-cell-group> </t-cell-group>
<t-button
class="submit"
theme="primary"
:disabled="!canSubmitRegister"
:loading="submitting"
@click="doSubmit"
>登记</t-button>
</div> </div>
<t-popup v-model="eventSelectorVisible" placement="bottom">
<div class="registration-event-panel">
<div class="title">登记事件</div>
<t-cell-group v-if="eventList.length" theme="card">
<t-cell
v-for="item in eventList"
:key="item.id"
:title="item.name"
:note="item.id === selectedEventId ? '当前使用' : ''"
@click="selectEvent(item.id)"
/>
</t-cell-group>
<div v-else class="empty-block">暂无可用登记事件</div>
</div>
</t-popup>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -106,6 +124,8 @@ import { getEventRecordPhotoURL } from "@/utils/eventRecord";
import { Storage, Time } from "timi-web"; import { Storage, Time } from "timi-web";
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import { loadSourceValueConfig, type SourceValueConfig } from "@/utils/point"; import { loadSourceValueConfig, type SourceValueConfig } from "@/utils/point";
import EventSelector from "@/components/EventSelector.vue";
import { Space } from "timi-tdesign-mobile";
defineOptions({ defineOptions({
name: "QuickSignInPage" name: "QuickSignInPage"
@@ -118,33 +138,74 @@ const settingStore = useSettingStore();
// ---------- 登记事件 ---------- // ---------- 登记事件 ----------
const EventItemStorageKey = "quick-scan-event-id"; const EventItemStorageKey = "quick-scan-event-id";
const eventSelectorVisible = ref(false); const selectedEventIdList = ref<string[]>([]);
const selectedEventId = ref("");
const eventList = ref<EventItem[]>([]); const eventList = ref<EventItem[]>([]);
const selectedEvent = computed(() => eventList.value.find(item => item.id === selectedEventId.value)); const isEventLoading = ref(false);
const selectedEventName = computed(() => eventList.value.find(item => item.id === selectedEventId.value)?.name || "请选择登记事件"); const eventLoadFailed = ref(false);
const selectedEventList = computed(() => selectedEventIdList.value
.map(eventId => eventList.value.find(item => normalizeEventId(item) === eventId))
.filter((item): item is EventItem => !!item));
const selectedNumberEventList = computed(() => selectedEventList.value.filter(item => item.valueType === "NUMBER"));
let sourceValueRequestNo = 0;
function normalizeEventId(item?: EventItem): string {
if (!item || item.id === undefined || item.id === null) {
return "";
}
return String(item.id);
}
async function loadActions(): Promise<void> { async function loadActions(): Promise<void> {
eventList.value = await EventAPI.listValid(); if (isEventLoading.value) {
const storedEventId = Storage.getString(EventItemStorageKey); return;
const storedAction = eventList.value.find(item => item.id === storedEventId); }
selectedEventId.value = storedAction?.id || eventList.value[0]?.id || ""; isEventLoading.value = true;
if (selectedEventId.value) { eventLoadFailed.value = false;
Storage.setString(EventItemStorageKey, selectedEventId); try {
eventList.value = await EventAPI.listValid();
selectedEventIdList.value = buildInitialSelectedEventIdList();
syncStoredEventId();
} catch (error) {
console.warn("登记事件加载失败", error);
eventLoadFailed.value = true;
eventList.value = [];
selectedEventIdList.value = [];
sourceValueConfig.value = undefined;
Message.error("登记事件加载失败");
} finally {
isEventLoading.value = false;
} }
await loadSourceValueRule(); await loadSourceValueRule();
} }
async function selectEvent(eventId?: string): Promise<void> { function buildInitialSelectedEventIdList(): string[] {
if (!eventId) { const selectedIdList = normalizeSelectedEventIdList(selectedEventIdList.value);
return; if (selectedIdList.length) {
return selectedIdList;
} }
selectedEventId.value = eventId;
numberValueText.value = ""; const storedEventId = Storage.getString(EventItemStorageKey);
Storage.setString(EventItemStorageKey, selectedEventId); const storedAction = eventList.value.find(item => normalizeEventId(item) === storedEventId);
eventSelectorVisible.value = false; const defaultEventId = normalizeEventId(storedAction || eventList.value[0]);
return defaultEventId ? [defaultEventId] : [];
}
function normalizeSelectedEventIdList(eventIdList: string[]): string[] {
const validEventIdSet = new Set(eventList.value.map(normalizeEventId).filter(Boolean));
return [...new Set(eventIdList.map(String))].filter(eventId => validEventIdSet.has(eventId));
}
async function handleEventSelectionChange(eventIdList: string[]): Promise<void> {
selectedEventIdList.value = normalizeSelectedEventIdList(eventIdList);
syncStoredEventId();
await loadSourceValueRule(); await loadSourceValueRule();
Message.success("登记事件已切换"); }
function syncStoredEventId(): void {
const firstEventId = selectedEventIdList.value[0];
if (firstEventId) {
Storage.setString(EventItemStorageKey, firstEventId);
}
} }
onMounted(loadActions); onMounted(loadActions);
@@ -207,12 +268,10 @@ async function toRecordDetail(id: string): Promise<void> {
const submitting = ref(false); const submitting = ref(false);
const pointValueText = ref(""); const pointValueText = ref("");
const numberValueText = ref(""); const numberValueTextMap = reactive<Record<string, string>>({});
const sourceValueConfig = ref<SourceValueConfig>(); const sourceValueConfig = ref<SourceValueConfig>();
const sourceValueAction = ref<PointAction>("EARN"); const sourceValueAction = ref<PointAction>("EARN");
const canSubmitRegister = computed(() => !!matchedCustomer.value && !!selectedEventId.value); const canSubmitRegister = computed(() => !!matchedCustomer.value && !!selectedEventIdList.value.length);
const showNumberValueInput = computed(() => selectedEvent.value?.valueType === "NUMBER");
const numberValuePlaceholder = computed(() => selectedEvent.value ? getEventNumberValuePlaceholder(selectedEvent.value) : "请输入事件数值");
const showSourceValueInput = computed(() => ( const showSourceValueInput = computed(() => (
userStore.hasPermissions(permissionCodes.point.sourceValue) userStore.hasPermissions(permissionCodes.point.sourceValue)
&& !!sourceValueConfig.value && !!sourceValueConfig.value
@@ -223,14 +282,22 @@ const sourceValuePlaceholder = computed(() => {
}); });
async function loadSourceValueRule(): Promise<void> { async function loadSourceValueRule(): Promise<void> {
if (!selectedEventId.value || !userStore.hasPermissions(permissionCodes.point.sourceValue)) { const requestNo = ++sourceValueRequestNo;
if (!selectedEventIdList.value.length || !userStore.hasPermissions(permissionCodes.point.sourceValue)) {
sourceValueConfig.value = undefined; sourceValueConfig.value = undefined;
return; return;
} }
try { try {
sourceValueConfig.value = await loadSourceValueConfig([selectedEventId.value]); const config = await loadSourceValueConfig(selectedEventIdList.value);
if (requestNo !== sourceValueRequestNo) {
return;
}
sourceValueConfig.value = config;
sourceValueAction.value = sourceValueConfig.value?.action || "EARN"; sourceValueAction.value = sourceValueConfig.value?.action || "EARN";
} catch { } catch {
if (requestNo !== sourceValueRequestNo) {
return;
}
sourceValueConfig.value = undefined; sourceValueConfig.value = undefined;
} }
} }
@@ -240,8 +307,44 @@ function resolveSourceValue(): number | undefined {
return Number.isSafeInteger(value) && 0 < value ? sourceValueAction.value === "CONSUME" ? -value : value : undefined; return Number.isSafeInteger(value) && 0 < value ? sourceValueAction.value === "CONSUME" ? -value : value : undefined;
} }
function resolveNumberValue(): number | undefined { function getNumberValuePlaceholder(event: EventItem): string {
return selectedEvent.value ? resolveEventNumberValue(selectedEvent.value, numberValueText.value) : undefined; return getEventNumberValuePlaceholder(event);
}
function resolveNumberValue(eventId: string): number | undefined {
const valueText = numberValueTextMap[eventId] || "";
if (!valueText.trim()) {
return undefined;
}
const event = eventList.value.find(item => normalizeEventId(item) === 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()) {
Message.warning(`${event.name || "当前事件"}不能提交数值`);
return false;
}
continue;
}
const numberValue = resolveNumberValue(eventId);
if (valueText.trim() && numberValue === undefined) {
Message.warning(`${event.name || "当前事件"}数值格式不正确,请检查单位和小数位`);
return false;
}
if (event.requireValue && numberValue === undefined) {
Message.warning(`请填写${event.name || "当前事件"}数值`);
return false;
}
}
return true;
} }
async function doSubmit(): Promise<void> { async function doSubmit(): Promise<void> {
@@ -249,16 +352,14 @@ async function doSubmit(): Promise<void> {
if (!customer) { if (!customer) {
return; return;
} }
if (!selectedEventIdList.value.length) {
Message.warning("请至少选择一个登记事件");
return;
}
if (!validateNumberValueList()) {
return;
}
const pointValue = resolveSourceValue(); const pointValue = resolveSourceValue();
const numberValue = resolveNumberValue();
if (showNumberValueInput.value && numberValueText.value.trim() && numberValue === undefined) {
Message.warning("事件数值格式不正确,请检查单位和小数位");
return;
}
if (selectedEvent.value?.requireValue && numberValue === undefined) {
Message.warning("请填写事件数值");
return;
}
const range = sourceValueConfig.value?.range; const range = sourceValueConfig.value?.range;
if (showSourceValueInput.value && (!pointValue || !range || Math.abs(pointValue) < range[0] || range[1] < Math.abs(pointValue))) { if (showSourceValueInput.value && (!pointValue || !range || Math.abs(pointValue) < range[0] || range[1] < Math.abs(pointValue))) {
Message.warning(`积分数量必须是 ${range?.[0] || 1}-${range?.[1] || 0} 的正整数`); Message.warning(`积分数量必须是 ${range?.[0] || 1}-${range?.[1] || 0} 的正整数`);
@@ -266,18 +367,22 @@ async function doSubmit(): Promise<void> {
} }
submitting.value = true; submitting.value = true;
try { try {
await EventRecordAPI.create({ for (const eventId of selectedEventIdList.value) {
customerId: customer.id, await EventRecordAPI.create({
eventId: selectedEventId.value, eventId,
pointValue: showSourceValueInput.value ? pointValue : undefined, customerId: customer.id,
numberValue, pointValue: showSourceValueInput.value ? pointValue : undefined,
remark: "客户快速扫码登记" numberValue: resolveNumberValue(eventId),
}); remark: "客户快速扫码登记"
});
}
scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`; scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`;
scanCode.value = ""; scanCode.value = "";
matchedCustomer.value = undefined; matchedCustomer.value = undefined;
pointValueText.value = ""; pointValueText.value = "";
numberValueText.value = ""; for (const eventId of Object.keys(numberValueTextMap)) {
delete numberValueTextMap[eventId];
}
recentRecordList.value = []; recentRecordList.value = [];
latestDecodedText = ""; latestDecodedText = "";
scannerSessionKey.value += 1; scannerSessionKey.value += 1;
@@ -286,74 +391,54 @@ async function doSubmit(): Promise<void> {
submitting.value = false; submitting.value = false;
} }
} }
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.quick-sign-in-page { .quick-sign-in-page {
gap: 1rem; gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem; padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.panel, .event-panel {
.bottom-bar {
left: 1rem;
right: 1rem;
}
.panel {
padding: 1rem;
background: rgba(255, 255, 255, .94);
border-radius: 1.4rem;
box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
}
.registration-event-select {
gap: .5rem;
border: 0;
padding: .875rem 1rem;
display: grid;
min-width: 0;
text-align: left;
background: rgba(255, 255, 255, .94);
box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
align-items: center;
border-radius: .875rem;
grid-template-columns: auto minmax(0, 1fr) auto;
span {
color: #52607a;
font-size: .8125rem;
}
strong {
color: #0f172a;
overflow: hidden;
font-size: 1rem;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.number-value-panel,
.source-value-panel {
padding: .75rem 1rem; padding: .75rem 1rem;
background: rgba(255, 255, 255, .94); background: rgba(255, 255, 255, .94);
box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08); box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
border-radius: .875rem; border-radius: .875rem;
}
.number-value-title, .input-form {
.source-value-title { padding: .5rem 0;
font-weight: 700; background: rgba(255, 255, 255, .94);
} box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
border-radius: .875rem;
.t-input { .form-text {
margin-top: .5rem; color: #0f172a;
border: 1px solid var(--td-component-border); display: block;
border-radius: .75rem; text-align: right;
} }
} }
.empty-block,
.event-load-error {
color: #52607a;
min-height: 4rem;
align-items: center;
justify-content: center;
}
.empty-block {
display: flex;
}
.event-load-error {
gap: .5rem;
display: grid;
text-align: center;
}
.code-box { .code-box {
padding: .875rem; padding: .875rem;
background: #f7faff; background: #f7faff;
@@ -384,13 +469,6 @@ async function doSubmit(): Promise<void> {
} }
} }
.submit {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
.record-photo { .record-photo {
width: 3.5rem; width: 3.5rem;
height: 3.5rem; height: 3.5rem;
@@ -398,54 +476,18 @@ async function doSubmit(): Promise<void> {
} }
} }
.registration-event-panel {
gap: .875rem;
padding: 1rem 0 calc(1rem + env(safe-area-inset-bottom, 0)) 0;
display: flex;
background: #f5f5f8;
border-radius: 1rem 1rem 0 0;
flex-direction: column;
.title {
margin: 0;
color: #0f172a;
font-size: 1rem;
text-align: center;
font-weight: 700;
}
.empty-block {
color: #52607a;
padding: 1rem;
text-align: center;
}
}
:global(html.theme-dark) { :global(html.theme-dark) {
.quick-sign-in-page { .quick-sign-in-page {
.panel, .panel,
.bottom-bar { .bottom-bar,
.event-panel,
.input-form {
background: rgba(15, 23, 42, .88); background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32); box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
} }
.registration-event-select { .input-form .form-text {
background: rgba(15, 23, 42, .88); color: #e2e8f0;
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
.number-value-panel,
.source-value-panel {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
} }
.code-box { .code-box {
@@ -457,17 +499,5 @@ async function doSubmit(): Promise<void> {
color: #94a3b8; color: #94a3b8;
} }
} }
.registration-event-panel {
background: #10161f;
.title {
color: #e2e8f0;
}
.empty-block {
color: #94a3b8;
}
}
} }
</style> </style>
+2 -2
View File
@@ -19,9 +19,9 @@ export function getEventValueUnitText(valueUnit?: EventItem["valueUnit"]): strin
*/ */
export function getEventNumberValuePlaceholder(event: EventItem): string { export function getEventNumberValuePlaceholder(event: EventItem): string {
if (event.valueUnit === "CENT") { if (event.valueUnit === "CENT") {
return event.requireValue ? "请输入金额(元),最多两位小数" : "请输入金额(元),可留空,最多两位小数"; return event.requireValue ? "金额(元),最多两位小数" : "金额(元),可留空,最多两位小数";
} }
return event.requireValue ? "请输入次数,必须是大于等于 0 的整数" : "请输入次数,可留空,必须是大于等于 0 的整数"; return event.requireValue ? "次数,必须是大于等于 0 的整数" : "次数,可留空,必须是大于等于 0 的整数";
} }
/** /**
+83 -21
View File
@@ -35,37 +35,99 @@ export async function loadSourceValueConfig(eventIdList: string[]): Promise<Sour
status: "ACTIVE" status: "ACTIVE"
} }
}))); })));
const ruleList = resultList const ruleList = resultList.flatMap(result => result.list || []);
.flatMap(result => result.list || [])
.filter(isSourceValueRule);
if (!ruleList.length) { if (!ruleList.length) {
return undefined; return undefined;
} }
const rangeList = ruleList const rangeList = ruleList
.map(rule => ({ min: rule.conditionJson?.min, max: rule.conditionJson?.max })) .map(resolvePointValueRuleConfig)
.filter((range): range is { min: number; max: number } => ( .filter((range): range is SourceValueConfig => !!range);
range.min !== undefined const action = rangeList[0]?.action;
&& range.max !== undefined if (!action) {
&& Number.isSafeInteger(range.min)
&& Number.isSafeInteger(range.max)
&& range.min !== 0
&& range.max !== 0
&& (range.min < 0) === (range.max < 0)
));
if (rangeList.length !== ruleList.length) {
return undefined; return undefined;
} }
if (rangeList.some(range => range.action !== action)) {
const action = rangeList[0].min < 0 ? "CONSUME" : "EARN";
if (rangeList.some(range => (range.min < 0 ? "CONSUME" : "EARN") !== action)) {
return undefined; return undefined;
} }
const min = Math.max(...rangeList.map(range => action === "CONSUME" ? Math.abs(range.max) : range.min), 1); const min = Math.max(...rangeList.map(range => range.range[0]), 1);
const max = Math.min(...rangeList.map(range => action === "CONSUME" ? Math.abs(range.min) : range.max)); const max = Math.min(...rangeList.map(range => range.range[1]));
return min <= max ? { range: [min, max], action } : undefined; return min <= max ? { range: [min, max], action } : undefined;
} }
function isSourceValueRule(rule: PointRule): boolean { /**
return rule.status === "ACTIVE" && rule.triggerType === "SOURCE_VALUE" && rule.pointMode === "SOURCE_VALUE"; * 解析积分规则动作。
*
* 固定积分优先按 pointValue 正负判定;登记时自定义积分按 conditionJson.min 正负判定。
*
* @param rule 积分规则
* @returns 积分动作;缺少有效积分值时返回 undefined
*/
export function resolvePointRuleAction(rule: PointRule): PointAction | undefined {
if (rule.pointValue !== undefined && rule.pointValue !== null && rule.pointValue !== 0) {
return rule.pointValue < 0 ? "CONSUME" : "EARN";
}
const min = rule.conditionJson?.min;
if (min === undefined || min === 0) {
return undefined;
}
return min < 0 ? "CONSUME" : "EARN";
}
function resolvePointValueRuleConfig(rule: PointRule): SourceValueConfig | undefined {
if (rule.status !== "ACTIVE") {
return undefined;
}
if (rule.pointMode === "FIXED") {
return resolveFixedPointValueRuleConfig(rule);
}
if (rule.triggerType === "SOURCE_VALUE" && rule.pointMode === "SOURCE_VALUE") {
return resolveSourceValueRuleConfig(rule);
}
return undefined;
}
function resolveFixedPointValueRuleConfig(rule: PointRule): SourceValueConfig | undefined {
const action = resolvePointRuleAction(rule);
const pointValue = rule.pointValue;
if (
action === undefined
|| pointValue === undefined
|| pointValue === null
|| !Number.isSafeInteger(pointValue)
|| pointValue === 0
) {
return undefined;
}
const value = Math.abs(pointValue);
return {
action,
range: [value, value]
};
}
function resolveSourceValueRuleConfig(rule: PointRule): SourceValueConfig | undefined {
const min = rule.conditionJson?.min;
const max = rule.conditionJson?.max;
const action = resolvePointRuleAction(rule);
if (
min === undefined
|| max === undefined
|| action === undefined
|| !Number.isSafeInteger(min)
|| !Number.isSafeInteger(max)
|| min === 0
|| max === 0
|| (action === "CONSUME") !== (min < 0)
|| (action === "CONSUME") !== (max < 0)
) {
return undefined;
}
const boundaryList = [Math.abs(min), Math.abs(max)];
return {
action,
range: [Math.min(...boundaryList), Math.max(...boundaryList)]
};
} }