fix point rule unit
This commit is contained in:
@@ -296,7 +296,15 @@ function getRuleConditionText(rule: PointRule): string {
|
|||||||
textList.push(`来源事件:${sourceEvent.value?.name || sourceEvent.value?.id || condition.sourceEventId}`);
|
textList.push(`来源事件:${sourceEvent.value?.name || sourceEvent.value?.id || condition.sourceEventId}`);
|
||||||
}
|
}
|
||||||
if (condition.tiers?.length) {
|
if (condition.tiers?.length) {
|
||||||
textList.push(condition.tiers.map(tier => `${tier.min}~${tier.max ?? "以上"}:${0 < tier.pointValue ? "发放" : "消耗"} ${Math.abs(tier.pointValue)} 分`).join(","));
|
const tierSourceEvent = sourceEvent.value || (condition.sourceEventId === triggerEvent.value?.id ? triggerEvent.value : undefined);
|
||||||
|
textList.push(condition.tiers.map(tier => {
|
||||||
|
const minText = formatEventNumberValue(tierSourceEvent, tier.min);
|
||||||
|
const maxText = tier.max === undefined || tier.max === null ? "以上" : formatEventNumberValue(tierSourceEvent, tier.max);
|
||||||
|
const pointText = tier.pointValue === 0
|
||||||
|
? "不产生积分流水"
|
||||||
|
: `${0 < tier.pointValue ? "发放" : "消耗"} ${Math.abs(tier.pointValue)} 分`;
|
||||||
|
return `${minText}~${maxText}:${pointText}`;
|
||||||
|
}).join(","));
|
||||||
}
|
}
|
||||||
return textList.join(";") || "无额外条件";
|
return textList.join(";") || "无额外条件";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<t-form-item label="累计窗口(年)" label-width="8rem">
|
<t-form-item label="累计窗口(年)" label-width="8rem">
|
||||||
<t-input v-model.trim="formModel.windowAmountText" type="number" align="right" borderless />
|
<t-input v-model.trim="formModel.windowAmountText" type="number" align="right" borderless />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<point-rule-tier-editor v-model="formModel.tiers" />
|
<point-rule-tier-editor v-model="formModel.tiers" :value-unit="selectedSourceValueUnit" />
|
||||||
</template>
|
</template>
|
||||||
</card-container>
|
</card-container>
|
||||||
<card-container>
|
<card-container>
|
||||||
@@ -91,9 +91,8 @@
|
|||||||
</t-form-item>
|
</t-form-item>
|
||||||
</card-container>
|
</card-container>
|
||||||
</t-form>
|
</t-form>
|
||||||
<div class="bottom-actions">
|
<div v-if="!isCreateMode" class="bottom-actions">
|
||||||
<t-button
|
<t-button
|
||||||
v-if="!isCreateMode"
|
|
||||||
v-can="permissionCodes.pointRule.delete"
|
v-can="permissionCodes.pointRule.delete"
|
||||||
class="delete bg-white"
|
class="delete bg-white"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
@@ -103,15 +102,6 @@
|
|||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button
|
|
||||||
v-can="savePermissionCode"
|
|
||||||
class="save"
|
|
||||||
theme="primary"
|
|
||||||
:loading="isSaving"
|
|
||||||
@click="saveRule"
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<t-popup v-model="storePickerVisible" placement="bottom">
|
<t-popup v-model="storePickerVisible" placement="bottom">
|
||||||
@@ -208,11 +198,13 @@ import EventAPI from "@/api/EventAPI";
|
|||||||
import PointAPI from "@/api/PointAPI";
|
import PointAPI from "@/api/PointAPI";
|
||||||
import PointRuleTierEditor from "@/pages/point/component/PointRuleTierEditor.vue";
|
import PointRuleTierEditor from "@/pages/point/component/PointRuleTierEditor.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
import type { PointAction, PointRule, PointRuleMode, PointRuleStatus, PointRuleTier, PointRuleTierForm, PointRuleTriggerType } from "@/types/Point";
|
import type { PointAction, PointRule, PointRuleMode, PointRuleStatus, PointRuleTier, PointRuleTierForm, PointRuleTriggerType } from "@/types/Point";
|
||||||
|
import { getEventValueUnitText } from "@/utils/eventValue";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
import { getTierMinValue, isLastTier, resolveNonNegativeInteger } from "@/utils/pointRule";
|
import { formatTierBoundaryValue, getTierMinValue, isLastTier, resolveNonNegativeInteger, resolveTierBoundaryValue } from "@/utils/pointRule";
|
||||||
|
|
||||||
type PickerValue = string | number;
|
type PickerValue = string | number;
|
||||||
|
|
||||||
@@ -222,6 +214,7 @@ defineOptions({
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
const routeId = computed(() => String(route.params.id || ""));
|
const routeId = computed(() => String(route.params.id || ""));
|
||||||
@@ -282,20 +275,24 @@ const eventPickerColumns = computed(() => [
|
|||||||
const sourceEventPickerVisible = ref(false);
|
const sourceEventPickerVisible = ref(false);
|
||||||
const sourceEventPickerDraft = ref<PickerValue[]>([]);
|
const sourceEventPickerDraft = ref<PickerValue[]>([]);
|
||||||
const sourceEventList = computed(() => eventList.value.filter(item => item.valueType === "NUMBER" && item.id));
|
const sourceEventList = computed(() => eventList.value.filter(item => item.valueType === "NUMBER" && item.id));
|
||||||
|
const selectedSourceEvent = computed(() => sourceEventList.value.find(item => item.id === formModel.sourceEventId));
|
||||||
|
const selectedSourceValueUnit = computed(() => selectedSourceEvent.value?.valueUnit || null);
|
||||||
const sourceEventPickerColumns = computed(() => [
|
const sourceEventPickerColumns = computed(() => [
|
||||||
sourceEventList.value
|
sourceEventList.value
|
||||||
.map(item => ({ label: item.name || "未命名事件", value: item.id || "" }))
|
.map(item => ({ label: `${item.name || "未命名事件"}(${getEventValueUnitText(item.valueUnit)})`, value: item.id || "" }))
|
||||||
.filter(item => item.value)
|
.filter(item => item.value)
|
||||||
]);
|
]);
|
||||||
const selectedSourceEventName = computed(() => sourceEventList.value.find(item => item.id === formModel.sourceEventId)?.name || (formModel.sourceEventId ? `事件 ${formModel.sourceEventId}` : "请选择来源事件"));
|
const selectedSourceEventName = computed(() => selectedSourceEvent.value
|
||||||
|
? `${selectedSourceEvent.value.name || "未命名事件"}(${getEventValueUnitText(selectedSourceEvent.value.valueUnit)})`
|
||||||
|
: formModel.sourceEventId ? `事件 ${formModel.sourceEventId}` : "请选择来源事件");
|
||||||
|
|
||||||
let tierKey = 0;
|
let tierKey = 0;
|
||||||
|
|
||||||
function createTierForm(tier?: PointRuleTier, isLast = false): PointRuleTierForm {
|
function createTierForm(tier?: PointRuleTier, isLast = false, valueUnit?: EventItem["valueUnit"]): PointRuleTierForm {
|
||||||
const pointValue = tier?.pointValue;
|
const pointValue = tier?.pointValue;
|
||||||
return {
|
return {
|
||||||
key: tierKey++,
|
key: tierKey++,
|
||||||
maxText: !isLast && tier?.max !== undefined && tier.max !== null ? String(tier.max) : "",
|
maxText: !isLast && tier?.max !== undefined && tier.max !== null ? formatTierBoundaryValue(tier.max, valueUnit) : "",
|
||||||
pointAction: pointValue !== undefined && pointValue < 0 ? "CONSUME" : "EARN",
|
pointAction: pointValue !== undefined && pointValue < 0 ? "CONSUME" : "EARN",
|
||||||
pointValueText: pointValue === undefined ? "0" : String(Math.abs(pointValue))
|
pointValueText: pointValue === undefined ? "0" : String(Math.abs(pointValue))
|
||||||
};
|
};
|
||||||
@@ -360,6 +357,12 @@ function openSourceEventPicker(): void {
|
|||||||
function confirmSourceEventPicker(value: Array<string | number>): void {
|
function confirmSourceEventPicker(value: Array<string | number>): void {
|
||||||
const sourceEventId = String(value[0] || "");
|
const sourceEventId = String(value[0] || "");
|
||||||
if (sourceEventId) {
|
if (sourceEventId) {
|
||||||
|
const previousSourceEvent = sourceEventList.value.find(item => item.id === formModel.sourceEventId);
|
||||||
|
const nextSourceEvent = sourceEventList.value.find(item => item.id === sourceEventId);
|
||||||
|
if (formModel.sourceEventId && previousSourceEvent?.valueUnit !== nextSourceEvent?.valueUnit) {
|
||||||
|
formModel.tiers = [createTierForm(undefined, true, nextSourceEvent?.valueUnit)];
|
||||||
|
Toast.warning("来源事件单位已变化,原分档已清空,请重新配置");
|
||||||
|
}
|
||||||
formModel.sourceEventId = sourceEventId;
|
formModel.sourceEventId = sourceEventId;
|
||||||
}
|
}
|
||||||
sourceEventPickerVisible.value = false;
|
sourceEventPickerVisible.value = false;
|
||||||
@@ -382,8 +385,11 @@ async function loadEventList(): Promise<void> {
|
|||||||
const options = isGlobalManager.value ? { storeId: formModel.storeId } : undefined;
|
const options = isGlobalManager.value ? { storeId: formModel.storeId } : undefined;
|
||||||
const result = await EventAPI.list({ index: 0, size: 1000 }, options);
|
const result = await EventAPI.list({ index: 0, size: 1000 }, options);
|
||||||
eventList.value = result.list || [];
|
eventList.value = result.list || [];
|
||||||
if (formModel.eventId && !eventList.value.some(item => item.id === formModel.eventId)) {
|
const requiredEventIdList = [formModel.eventId, formModel.sourceEventId].filter((id, index, list) => id && list.indexOf(id) === index);
|
||||||
eventList.value.push(await EventAPI.detail(formModel.eventId));
|
for (const eventId of requiredEventIdList) {
|
||||||
|
if (!eventList.value.some(item => item.id === eventId)) {
|
||||||
|
eventList.value.push(await EventAPI.detail(eventId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,6 +577,7 @@ function resolveTimestamp(value: string): number | undefined {
|
|||||||
|
|
||||||
function buildCondition(): PointRule["conditionJson"] {
|
function buildCondition(): PointRule["conditionJson"] {
|
||||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
||||||
|
const valueUnit = selectedSourceValueUnit.value;
|
||||||
return {
|
return {
|
||||||
sourceEventId: formModel.sourceEventId,
|
sourceEventId: formModel.sourceEventId,
|
||||||
sourceValueField: "numberValue",
|
sourceValueField: "numberValue",
|
||||||
@@ -580,12 +587,13 @@ function buildCondition(): PointRule["conditionJson"] {
|
|||||||
unit: "YEAR"
|
unit: "YEAR"
|
||||||
},
|
},
|
||||||
tiers: formModel.tiers.map((tier, index) => {
|
tiers: formModel.tiers.map((tier, index) => {
|
||||||
|
const min = getTierMinValue(formModel.tiers, index, valueUnit);
|
||||||
const result: PointRuleTier = {
|
const result: PointRuleTier = {
|
||||||
min: getTierMinValue(formModel.tiers, index) || 0,
|
min: min ?? 0,
|
||||||
pointValue: (tier.pointAction === "CONSUME" ? -1 : 1) * (resolveNonNegativeInteger(tier.pointValueText) || 0)
|
pointValue: (tier.pointAction === "CONSUME" ? -1 : 1) * (resolveNonNegativeInteger(tier.pointValueText) || 0)
|
||||||
};
|
};
|
||||||
if (!isLastTier(formModel.tiers, index)) {
|
if (!isLastTier(formModel.tiers, index)) {
|
||||||
result.max = resolveNonNegativeInteger(tier.maxText);
|
result.max = resolveTierBoundaryValue(tier.maxText, valueUnit);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
@@ -628,10 +636,14 @@ function validateForm(): boolean {
|
|||||||
Toast.warning("分档规则必须使用分档积分模式");
|
Toast.warning("分档规则必须使用分档积分模式");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!sourceEventList.value.some(item => item.id === formModel.sourceEventId)) {
|
if (!selectedSourceEvent.value) {
|
||||||
Toast.warning("请选择 NUMBER 类型的来源事件");
|
Toast.warning("请选择 NUMBER 类型的来源事件");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (selectedSourceValueUnit.value !== "CENT" && selectedSourceValueUnit.value !== "COUNT") {
|
||||||
|
Toast.warning("来源事件必须配置数值单位");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!resolvePositiveInteger(formModel.windowAmountText)) {
|
if (!resolvePositiveInteger(formModel.windowAmountText)) {
|
||||||
Toast.warning("累计窗口必须是正整数");
|
Toast.warning("累计窗口必须是正整数");
|
||||||
return false;
|
return false;
|
||||||
@@ -642,7 +654,7 @@ function validateForm(): boolean {
|
|||||||
}
|
}
|
||||||
for (let i = 0; i < formModel.tiers.length; i++) {
|
for (let i = 0; i < formModel.tiers.length; i++) {
|
||||||
const tier = formModel.tiers[i];
|
const tier = formModel.tiers[i];
|
||||||
const min = getTierMinValue(formModel.tiers, i);
|
const min = getTierMinValue(formModel.tiers, i, selectedSourceValueUnit.value);
|
||||||
const pointValue = resolveNonNegativeInteger(tier.pointValueText);
|
const pointValue = resolveNonNegativeInteger(tier.pointValueText);
|
||||||
if (min === undefined) {
|
if (min === undefined) {
|
||||||
Toast.warning(`第 ${i + 1} 档的上一档最大值无效`);
|
Toast.warning(`第 ${i + 1} 档的上一档最大值无效`);
|
||||||
@@ -653,9 +665,11 @@ function validateForm(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isLastTier(formModel.tiers, i)) {
|
if (!isLastTier(formModel.tiers, i)) {
|
||||||
const max = resolveNonNegativeInteger(tier.maxText);
|
const max = resolveTierBoundaryValue(tier.maxText, selectedSourceValueUnit.value);
|
||||||
if (max === undefined || max < min) {
|
if (max === undefined || max < min) {
|
||||||
Toast.warning(`第 ${i + 1} 档最大值必须是不小于最小值的整数`);
|
Toast.warning(selectedSourceValueUnit.value === "CENT"
|
||||||
|
? `第 ${i + 1} 档最大值必须是不小于最小值且最多两位小数`
|
||||||
|
: `第 ${i + 1} 档最大值必须是不小于最小值的整数`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,31 +800,43 @@ async function fillForm(): Promise<void> {
|
|||||||
maxText: current.conditionJson?.max === undefined ? "" : String(Math.abs(current.conditionJson.max)),
|
maxText: current.conditionJson?.max === undefined ? "" : String(Math.abs(current.conditionJson.max)),
|
||||||
sourceEventId: current.conditionJson?.sourceEventId || "",
|
sourceEventId: current.conditionJson?.sourceEventId || "",
|
||||||
windowAmountText: current.conditionJson?.window?.amount === undefined ? "1" : String(current.conditionJson.window.amount),
|
windowAmountText: current.conditionJson?.window?.amount === undefined ? "1" : String(current.conditionJson.window.amount),
|
||||||
tiers: current.conditionJson?.tiers?.length
|
tiers: [],
|
||||||
? current.conditionJson.tiers.map((tier, index, list) => createTierForm(tier, index === list.length - 1))
|
|
||||||
: [createTierForm(undefined, true)],
|
|
||||||
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
|
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
|
||||||
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
||||||
priorityText: String(current.priority ?? 0),
|
priorityText: String(current.priority ?? 0),
|
||||||
exclusive: current.exclusive ?? false,
|
exclusive: current.exclusive ?? false,
|
||||||
remark: current.remark || ""
|
remark: current.remark || ""
|
||||||
});
|
});
|
||||||
|
await loadEventList();
|
||||||
|
const valueUnit = selectedSourceValueUnit.value;
|
||||||
|
formModel.tiers = current.conditionJson?.tiers?.length
|
||||||
|
? current.conditionJson.tiers.map((tier, index, list) => createTierForm(tier, index === list.length - 1, valueUnit))
|
||||||
|
: [createTierForm(undefined, true, valueUnit)];
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
await loadEventList();
|
await loadEventList();
|
||||||
|
if (!formModel.tiers.length) {
|
||||||
|
formModel.tiers.push(createTierForm(undefined, true, selectedSourceValueUnit.value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
if (userStore.hasPermissions(savePermissionCode.value)) {
|
||||||
|
navBarStore.setRightText("保存", "var(--td-brand-color)", saveRule);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fillForm();
|
await fillForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast.error(getErrorMessage(error, "积分规则加载失败"));
|
Toast.error(getErrorMessage(error, "积分规则加载失败"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => navBarStore.clearRight());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.point-rule-edit {
|
.point-rule-edit {
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 0 5rem;
|
padding: calc(var(--app-nav-height) + 1rem) 0 calc(1rem + env(safe-area-inset-bottom, 0));
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -892,19 +918,12 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bottom-actions {
|
.bottom-actions {
|
||||||
gap: .75rem;
|
margin: 0 1rem;
|
||||||
left: 1rem;
|
padding-top: 1rem;
|
||||||
right: 1rem;
|
|
||||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
.delete {
|
.delete {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.save {
|
|
||||||
flex: 2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tier-editor">
|
<div class="tier-editor">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div>累计数值分档</div>
|
<div v-text="`累计数值分档(${valueUnitText})`" />
|
||||||
<div class="tips gray">首档从 0 开始,末档不设上限;积分为 0 不产生流水</div>
|
<div class="tips gray" v-text="`首档从 0 开始,末档不设上限;分档单位为${valueUnitText},积分为 0 不产生流水`" />
|
||||||
</div>
|
</div>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<div
|
<div
|
||||||
@@ -15,7 +15,11 @@
|
|||||||
<span class="index" v-text="`第 ${index + 1} 档`" />
|
<span class="index" v-text="`第 ${index + 1} 档`" />
|
||||||
<strong v-text="getTierRangeText(index)" />
|
<strong v-text="getTierRangeText(index)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="points text-bold" v-text="getTierPointText(tier)" />
|
<div
|
||||||
|
class="point text-bold"
|
||||||
|
:class="{ 'consume': tier.pointAction === 'CONSUME' }"
|
||||||
|
v-text="getTierPointText(tier)"
|
||||||
|
/>
|
||||||
<t-button
|
<t-button
|
||||||
v-if="1 < tiers.length"
|
v-if="1 < tiers.length"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
@@ -38,20 +42,28 @@
|
|||||||
>
|
>
|
||||||
<div class="tier-editor-popup">
|
<div class="tier-editor-popup">
|
||||||
<t-form v-if="tierEditorIndex >= 0" class="form" label-align="left" label-width="10rem">
|
<t-form v-if="tierEditorIndex >= 0" class="form" label-align="left" label-width="10rem">
|
||||||
<t-form-item label="最小值" content-align="right">
|
<t-form-item :label="`最小值(${valueUnitText})`" content-align="right">
|
||||||
<span class="gray" v-text="getTierMinText(tiers, tierEditorIndex)" />
|
<span class="gray" v-text="getTierMinText(tiers, tierEditorIndex, valueUnit)" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item v-if="!isLastTier(tiers, tierEditorIndex) || tierEditorAdding" :label="tierEditorAdding ? '当前档最大值' : '最大值'" required-mark>
|
<t-form-item
|
||||||
|
v-if="!isLastTier(tiers, tierEditorIndex) || tierEditorAdding"
|
||||||
|
:label="`${tierEditorAdding ? '当前档最大值' : '最大值'}(${valueUnitText})`"
|
||||||
|
required-mark
|
||||||
|
>
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="tierEditorDraft.maxText"
|
v-model.trim="tierEditorDraft.maxText"
|
||||||
type="number"
|
type="number"
|
||||||
|
:step="valueUnit === 'CENT' ? '0.01' : '1'"
|
||||||
align="right"
|
align="right"
|
||||||
borderless
|
borderless
|
||||||
placeholder="请输入最大值"
|
:placeholder="valueUnit === 'CENT' ? '最多两位小数' : '请输入整数'"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="积分动作" arrow @click="openActionPicker">
|
<t-form-item label="积分动作" content-align="right">
|
||||||
<span class="picker-text" v-text="getPointActionText(tierEditorDraft.pointAction)" />
|
<t-radio-group v-model="tierEditorDraft.pointAction" variant="default-filled">
|
||||||
|
<t-radio :block="false" borderless value="EARN">发放</t-radio>
|
||||||
|
<t-radio :block="false" borderless value="CONSUME">消耗</t-radio>
|
||||||
|
</t-radio-group>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="积分数量" required-mark>
|
<t-form-item label="积分数量" required-mark>
|
||||||
<t-input
|
<t-input
|
||||||
@@ -66,26 +78,21 @@
|
|||||||
</t-form>
|
</t-form>
|
||||||
</div>
|
</div>
|
||||||
</bottom-popup>
|
</bottom-popup>
|
||||||
<t-popup v-model="actionPickerVisible" placement="bottom">
|
|
||||||
<t-picker
|
|
||||||
v-model="actionPickerDraft"
|
|
||||||
:columns="actionPickerColumns"
|
|
||||||
title="积分动作"
|
|
||||||
@cancel="actionPickerVisible = false"
|
|
||||||
@confirm="confirmActionPicker"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { EventValueUnit } from "@/types/EventItem";
|
||||||
import type { PointAction, PointRuleTierForm } from "@/types/Point";
|
import type { PointAction, PointRuleTierForm } from "@/types/Point";
|
||||||
import { getTierMinText, getTierMinValue, isLastTier, resolveNonNegativeInteger } from "@/utils/pointRule";
|
import { formatTierBoundaryValue, getTierMinText, getTierMinValue, isLastTier, resolveNonNegativeInteger, resolveTierBoundaryValue } from "@/utils/pointRule";
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "PointRuleTierEditor"
|
name: "PointRuleTierEditor"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
valueUnit?: EventValueUnit | null;
|
||||||
|
}>();
|
||||||
const tiers = defineModel<PointRuleTierForm[]>({ required: true });
|
const tiers = defineModel<PointRuleTierForm[]>({ required: true });
|
||||||
const tierEditorVisible = ref(false);
|
const tierEditorVisible = ref(false);
|
||||||
const tierEditorIndex = ref(-1);
|
const tierEditorIndex = ref(-1);
|
||||||
@@ -95,31 +102,26 @@ const tierEditorDraft = reactive({
|
|||||||
pointAction: "EARN" as PointAction,
|
pointAction: "EARN" as PointAction,
|
||||||
pointValueText: ""
|
pointValueText: ""
|
||||||
});
|
});
|
||||||
const actionOptions: Array<{ label: string; value: PointAction }> = [
|
|
||||||
{ label: "发放积分", value: "EARN" },
|
|
||||||
{ label: "消耗积分", value: "CONSUME" }
|
|
||||||
];
|
|
||||||
const actionPickerVisible = ref(false);
|
|
||||||
const actionPickerDraft = ref<Array<string | number>>([]);
|
|
||||||
const actionPickerColumns = [actionOptions];
|
|
||||||
const tierEditorTitle = computed(() => {
|
const tierEditorTitle = computed(() => {
|
||||||
if (tierEditorIndex.value < 0) {
|
if (tierEditorIndex.value < 0) {
|
||||||
return "编辑分档";
|
return "编辑分档";
|
||||||
}
|
}
|
||||||
return tierEditorAdding.value ? `编辑第 ${tierEditorIndex.value + 1} 档并新增下一档` : `编辑第 ${tierEditorIndex.value + 1} 档`;
|
return tierEditorAdding.value ? `编辑第 ${tierEditorIndex.value + 1} 档并新增下一档` : `编辑第 ${tierEditorIndex.value + 1} 档`;
|
||||||
});
|
});
|
||||||
|
const valueUnit = computed(() => props.valueUnit);
|
||||||
|
const valueUnitText = computed(() => valueUnit.value === "CENT" ? "元" : valueUnit.value === "COUNT" ? "次" : "数值");
|
||||||
|
|
||||||
function getTierRangeText(index: number): string {
|
function getTierRangeText(index: number): string {
|
||||||
const tier = tiers.value[index];
|
const tier = tiers.value[index];
|
||||||
if (!tier) {
|
if (!tier) {
|
||||||
return "待填写";
|
return "待填写";
|
||||||
}
|
}
|
||||||
const minText = getTierMinText(tiers.value, index);
|
const minText = getTierMinText(tiers.value, index, valueUnit.value);
|
||||||
if (isLastTier(tiers.value, index)) {
|
if (isLastTier(tiers.value, index)) {
|
||||||
return `${minText} 及以上`;
|
return `${minText} 及以上`;
|
||||||
}
|
}
|
||||||
const max = resolveNonNegativeInteger(tier.maxText);
|
const max = resolveTierBoundaryValue(tier.maxText, valueUnit.value);
|
||||||
return max === undefined ? `${minText} - 待填写` : `${minText}-${max}`;
|
return max === undefined ? `${minText} - 待填写` : `${minText}-${formatTierBoundaryValue(max, valueUnit.value)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTierPointText(tier: PointRuleTierForm): string {
|
function getTierPointText(tier: PointRuleTierForm): string {
|
||||||
@@ -127,7 +129,7 @@ function getTierPointText(tier: PointRuleTierForm): string {
|
|||||||
if (pointValue === undefined) {
|
if (pointValue === undefined) {
|
||||||
return "待填写";
|
return "待填写";
|
||||||
}
|
}
|
||||||
return `${tier.pointAction === "CONSUME" ? "消耗" : "发放"} ${pointValue} 分`;
|
return `${tier.pointAction === "CONSUME" ? "-" : "+"}${pointValue} 分`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTierEditor(index: number, adding = false): void {
|
function openTierEditor(index: number, adding = false): void {
|
||||||
@@ -138,7 +140,7 @@ function openTierEditor(index: number, adding = false): void {
|
|||||||
tierEditorIndex.value = index;
|
tierEditorIndex.value = index;
|
||||||
tierEditorAdding.value = adding;
|
tierEditorAdding.value = adding;
|
||||||
tierEditorDraft.maxText = adding ? "" : tier.maxText;
|
tierEditorDraft.maxText = adding ? "" : tier.maxText;
|
||||||
tierEditorDraft.pointAction = tier.pointAction;
|
tierEditorDraft.pointAction = tier.pointAction === "CONSUME" ? "CONSUME" : "EARN";
|
||||||
tierEditorDraft.pointValueText = tier.pointValueText;
|
tierEditorDraft.pointValueText = tier.pointValueText;
|
||||||
tierEditorVisible.value = true;
|
tierEditorVisible.value = true;
|
||||||
}
|
}
|
||||||
@@ -154,7 +156,7 @@ function confirmTierEditor(): void {
|
|||||||
if (!tier) {
|
if (!tier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const min = getTierMinValue(tiers.value, index);
|
const min = getTierMinValue(tiers.value, index, valueUnit.value);
|
||||||
const pointValue = resolveNonNegativeInteger(tierEditorDraft.pointValueText);
|
const pointValue = resolveNonNegativeInteger(tierEditorDraft.pointValueText);
|
||||||
if (min === undefined) {
|
if (min === undefined) {
|
||||||
Toast.warning("请先修正上一档的最大值");
|
Toast.warning("请先修正上一档的最大值");
|
||||||
@@ -166,12 +168,12 @@ function confirmTierEditor(): void {
|
|||||||
}
|
}
|
||||||
const shouldAddTier = tierEditorAdding.value && isLastTier(tiers.value, index);
|
const shouldAddTier = tierEditorAdding.value && isLastTier(tiers.value, index);
|
||||||
if (!isLastTier(tiers.value, index) || shouldAddTier) {
|
if (!isLastTier(tiers.value, index) || shouldAddTier) {
|
||||||
const max = resolveNonNegativeInteger(tierEditorDraft.maxText);
|
const max = resolveTierBoundaryValue(tierEditorDraft.maxText, valueUnit.value);
|
||||||
if (max === undefined || max < min) {
|
if (max === undefined || max < min) {
|
||||||
Toast.warning("当前档最大值必须是不小于最小值的整数");
|
Toast.warning(valueUnit.value === "CENT" ? "当前档最大值必须是不小于最小值且最多两位小数" : "当前档最大值必须是不小于最小值的整数");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tier.maxText = String(max);
|
tier.maxText = formatTierBoundaryValue(max, valueUnit.value);
|
||||||
} else {
|
} else {
|
||||||
tier.maxText = "";
|
tier.maxText = "";
|
||||||
}
|
}
|
||||||
@@ -203,23 +205,6 @@ function removeTier(index: number): void {
|
|||||||
tiers.value.splice(index, 1);
|
tiers.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openActionPicker(): void {
|
|
||||||
actionPickerDraft.value = [tierEditorDraft.pointAction];
|
|
||||||
actionPickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmActionPicker(value: Array<string | number>): void {
|
|
||||||
const action = value[0];
|
|
||||||
if (action === "EARN" || action === "CONSUME") {
|
|
||||||
tierEditorDraft.pointAction = action;
|
|
||||||
}
|
|
||||||
actionPickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPointActionText(action: PointAction): string {
|
|
||||||
return action === "CONSUME" ? "消耗积分" : "发放积分";
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTierForm(): PointRuleTierForm {
|
function createTierForm(): PointRuleTierForm {
|
||||||
const key = tiers.value.reduce((max, tier) => Math.max(max, tier.key), -1) + 1;
|
const key = tiers.value.reduce((max, tier) => Math.max(max, tier.key), -1) + 1;
|
||||||
return {
|
return {
|
||||||
@@ -271,13 +256,17 @@ function createTierForm(): PointRuleTierForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.points {
|
.point {
|
||||||
gap: .25rem;
|
gap: .25rem;
|
||||||
color: var(--td-brand-color);
|
color: var(--td-brand-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.consume {
|
||||||
|
color: var(--td-error-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +286,7 @@ function createTierForm(): PointRuleTierForm {
|
|||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
margin-top: .5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-5
@@ -54,13 +54,13 @@ export function formatEventNumberValue(event: EventItem | undefined, value?: num
|
|||||||
return `${value}${event?.valueUnit === "COUNT" ? " 次" : ""}`;
|
return `${value}${event?.valueUnit === "COUNT" ? " 次" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const centText = String(Math.trunc(value));
|
return `${formatCentValue(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 {
|
/**
|
||||||
|
* 将金额字符串转换为分,最多保留两位小数。
|
||||||
|
*/
|
||||||
|
export function resolveCentValue(valueText: string): number | undefined {
|
||||||
const match = /^(\d+)(?:\.(\d{0,2}))?$/.exec(valueText);
|
const match = /^(\d+)(?:\.(\d{0,2}))?$/.exec(valueText);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -74,3 +74,16 @@ function resolveCentValue(valueText: string): number | undefined {
|
|||||||
}
|
}
|
||||||
return Number(centValue);
|
return Number(centValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将整数分格式化为不带单位的元字符串。
|
||||||
|
*/
|
||||||
|
export function formatCentValue(value: number): string {
|
||||||
|
if (!Number.isSafeInteger(value) || value < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const centText = String(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}` : ""}`;
|
||||||
|
}
|
||||||
|
|||||||
+22
-5
@@ -1,4 +1,6 @@
|
|||||||
|
import type { EventValueUnit } from "@/types/EventItem";
|
||||||
import type { PointRuleTierForm } from "@/types/Point";
|
import type { PointRuleTierForm } from "@/types/Point";
|
||||||
|
import { formatCentValue, resolveCentValue } from "@/utils/eventValue";
|
||||||
|
|
||||||
export function resolveNonNegativeInteger(value: string): number | undefined {
|
export function resolveNonNegativeInteger(value: string): number | undefined {
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
@@ -8,20 +10,35 @@ export function resolveNonNegativeInteger(value: string): number | undefined {
|
|||||||
return Number.isSafeInteger(numberValue) && !(numberValue < 0) ? numberValue : undefined;
|
return Number.isSafeInteger(numberValue) && !(numberValue < 0) ? numberValue : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTierMinValue(tierList: PointRuleTierForm[], index: number): number | undefined {
|
/**
|
||||||
|
* 按来源事件的存储单位解析分档边界。
|
||||||
|
* CENT 的表单值使用元,解析结果使用分;COUNT 直接使用整数次数。
|
||||||
|
*/
|
||||||
|
export function resolveTierBoundaryValue(value: string, valueUnit?: EventValueUnit | null): number | undefined {
|
||||||
|
return valueUnit === "CENT" ? resolveCentValue(value.trim()) : resolveNonNegativeInteger(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按来源事件的展示单位格式化分档边界。
|
||||||
|
*/
|
||||||
|
export function formatTierBoundaryValue(value: number, valueUnit?: EventValueUnit | null): string {
|
||||||
|
return valueUnit === "CENT" ? formatCentValue(value) : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTierMinValue(tierList: PointRuleTierForm[], index: number, valueUnit?: EventValueUnit | null): number | undefined {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const previousMax = resolveNonNegativeInteger(tierList[index - 1]?.maxText || "");
|
const previousMax = resolveTierBoundaryValue(tierList[index - 1]?.maxText || "", valueUnit);
|
||||||
if (previousMax === undefined || !Number.isSafeInteger(previousMax + 1)) {
|
if (previousMax === undefined || !Number.isSafeInteger(previousMax + 1)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return previousMax + 1;
|
return previousMax + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTierMinText(tierList: PointRuleTierForm[], index: number): string {
|
export function getTierMinText(tierList: PointRuleTierForm[], index: number, valueUnit?: EventValueUnit | null): string {
|
||||||
const min = getTierMinValue(tierList, index);
|
const min = getTierMinValue(tierList, index, valueUnit);
|
||||||
return min === undefined ? "待填写" : String(min);
|
return min === undefined ? "待填写" : formatTierBoundaryValue(min, valueUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLastTier(tierList: PointRuleTierForm[], index: number): boolean {
|
export function isLastTier(tierList: PointRuleTierForm[], index: number): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user