update permission and product
This commit is contained in:
@@ -62,10 +62,8 @@
|
||||
<t-cell-group title="命中规则" theme="card" v-entrance-transition="350">
|
||||
<template v-if="hitRule">
|
||||
<t-cell title="规则名称" :note="hitRule.name || '未命名规则'" />
|
||||
<t-cell title="触发类型" :note="getTriggerTypeText(hitRule.triggerType)" />
|
||||
<t-cell title="积分模式" :note="getPointModeText(hitRule.pointMode)" />
|
||||
<t-cell title="规则模式" :note="hitRule.ruleMode === 'GROOVY' ? '脚本规则' : '简单规则'" />
|
||||
<t-cell title="规则积分" :note="getRulePointText(hitRule)" />
|
||||
<t-cell title="匹配条件" :note="getRuleConditionText(hitRule)" />
|
||||
<t-cell title="规则状态" :note="getRuleStatusText(hitRule.status)" />
|
||||
<t-cell title="规则优先级" :note="getPointText(hitRule.priority)" />
|
||||
<t-cell title="命中后停止匹配" :note="hitRule.exclusive ? '是' : '否'" />
|
||||
@@ -90,7 +88,7 @@ import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||
import type { Customer } from "@/types/Customer";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import type { EventRecord } from "@/types/EventRecord";
|
||||
import type { PointLedger, PointRule, PointRuleMode, PointRuleStatus, PointRuleTriggerType } from "@/types/Point";
|
||||
import type { PointLedger, PointRule, PointRuleStatus } from "@/types/Point";
|
||||
import { formatEventNumberValue, getEventValueUnitText } from "@/utils/eventValue";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
@@ -115,7 +113,6 @@ const canReadRule = computed(() => userStore.hasPermissions(permissionCodes.poin
|
||||
const ledger = ref<PointLedger>();
|
||||
const customer = ref<Customer>();
|
||||
const triggerEvent = ref<EventItem>();
|
||||
const sourceEvent = ref<EventItem>();
|
||||
const eventRecord = ref<EventRecord>();
|
||||
const hitRule = ref<PointRule>();
|
||||
const isLoading = ref(false);
|
||||
@@ -126,10 +123,7 @@ const hasEarnedPointValue = computed(() => eventRecord.value?.earnedPointValue !
|
||||
const isDeletedEventRevoke = computed(() => isDeletedEventRevokeLedger(ledger.value));
|
||||
|
||||
function isDeletedEventRevokeLedger(value?: PointLedger): boolean {
|
||||
const idempotencyKey = value?.idempotencyKey || "";
|
||||
return value?.changeType === "REVOKE"
|
||||
&& idempotencyKey.startsWith("GAO_EVENT_RECORD:")
|
||||
&& idempotencyKey.endsWith(":REVOKE");
|
||||
return value?.changeType === "REVOKE" && value?.triggerSourceType === "EVENT_RECORD" && !value.triggerSourceId;
|
||||
}
|
||||
|
||||
function getQueryText(key: string): string {
|
||||
@@ -171,7 +165,6 @@ async function loadPageData(): Promise<void> {
|
||||
ledger.value = parseLedgerSnapshot();
|
||||
customer.value = undefined;
|
||||
triggerEvent.value = undefined;
|
||||
sourceEvent.value = undefined;
|
||||
eventRecord.value = undefined;
|
||||
hitRule.value = undefined;
|
||||
if (!ledger.value) {
|
||||
@@ -180,11 +173,12 @@ async function loadPageData(): Promise<void> {
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
if (ledger.value.eventRecordId && canReadEventRecord.value && !isDeletedEventRevoke.value) {
|
||||
eventRecord.value = await loadOptional(() => EventRecordAPI.detail(ledger.value!.eventRecordId!), "积分流水关联事件记录加载失败");
|
||||
if (ledger.value.triggerSourceType === "EVENT_RECORD" && ledger.value.triggerSourceId && canReadEventRecord.value && !isDeletedEventRevoke.value) {
|
||||
eventRecord.value = await loadOptional(() => EventRecordAPI.detail(ledger.value!.triggerSourceId!), "积分流水关联事件记录加载失败");
|
||||
}
|
||||
|
||||
const eventId = eventRecord.value?.eventId || ledger.value.eventId;
|
||||
const eventId = eventRecord.value?.eventId
|
||||
|| (ledger.value.triggerSourceType === "EVENT_RECORD" ? ledger.value.triggerSourceId : "");
|
||||
const eventResult = eventRecord.value?.event
|
||||
? Promise.resolve(eventRecord.value.event)
|
||||
: eventId && canReadEvent.value
|
||||
@@ -201,11 +195,6 @@ async function loadPageData(): Promise<void> {
|
||||
if (eventRecord.value?.customer) {
|
||||
customer.value = eventRecord.value.customer;
|
||||
}
|
||||
|
||||
const sourceEventId = hitRule.value?.conditionJson?.sourceEventId;
|
||||
if (sourceEventId && sourceEventId !== eventId && canReadEvent.value) {
|
||||
sourceEvent.value = await loadOptional(() => EventAPI.detail(sourceEventId), "积分规则来源事件加载失败");
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -255,27 +244,9 @@ function getEventNumberValueText(): string {
|
||||
return formatEventNumberValue(eventRecord.value?.event || triggerEvent.value, eventRecord.value?.numberValue);
|
||||
}
|
||||
|
||||
function getTriggerTypeText(triggerType?: PointRuleTriggerType): string {
|
||||
return ({
|
||||
EVERY_TIME: "每次登记",
|
||||
FIRST_TIME: "首次登记",
|
||||
AFTER_COUNT_EVERY_TIME: "达到阈值后每次登记",
|
||||
SOURCE_VALUE: "登记时自定义积分",
|
||||
CUSTOMER_EVENT_VALUE_TIER: "按来源事件累计分档"
|
||||
} as Record<string, string>)[triggerType || ""] || "未知触发类型";
|
||||
}
|
||||
|
||||
function getPointModeText(pointMode?: PointRuleMode): string {
|
||||
return ({
|
||||
FIXED: "固定积分",
|
||||
SOURCE_VALUE: "使用登记时填写的积分",
|
||||
TIER: "分档积分"
|
||||
} as Record<string, string>)[pointMode || ""] || "未知积分模式";
|
||||
}
|
||||
|
||||
function getRulePointText(rule: PointRule): string {
|
||||
if (rule.pointMode === "TIER") {
|
||||
return rule.conditionJson?.tiers?.length ? `${rule.conditionJson.tiers.length} 档` : "分档积分";
|
||||
if (rule.ruleMode === "GROOVY") {
|
||||
return "由脚本决定";
|
||||
}
|
||||
if (rule.pointValue === undefined || rule.pointValue === null) {
|
||||
return "-";
|
||||
@@ -283,42 +254,6 @@ function getRulePointText(rule: PointRule): string {
|
||||
return `${0 < rule.pointValue ? "发放" : "消耗"} ${Math.abs(rule.pointValue)} 分`;
|
||||
}
|
||||
|
||||
function getRuleConditionText(rule: PointRule): string {
|
||||
const condition = rule.conditionJson;
|
||||
if (!condition) {
|
||||
return "无额外条件";
|
||||
}
|
||||
|
||||
const textList: string[] = [];
|
||||
if (condition.min !== undefined || condition.max !== undefined) {
|
||||
textList.push(`数值范围 ${condition.min ?? "不限"}~${condition.max ?? "不限"}`);
|
||||
}
|
||||
if (condition.threshold !== undefined) {
|
||||
textList.push(`累计达到 ${condition.threshold} 次`);
|
||||
}
|
||||
if (condition.includeCurrent !== undefined) {
|
||||
textList.push(condition.includeCurrent ? "包含本次登记" : "不含本次登记");
|
||||
}
|
||||
if (condition.window) {
|
||||
textList.push(`累计窗口 ${condition.window.amount} ${condition.window.unit === "YEAR" ? "年" : condition.window.unit}`);
|
||||
}
|
||||
if (condition.sourceEventId) {
|
||||
textList.push(`来源事件:${sourceEvent.value?.name || sourceEvent.value?.id || condition.sourceEventId}`);
|
||||
}
|
||||
if (condition.tiers?.length) {
|
||||
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(";") || "无额外条件";
|
||||
}
|
||||
|
||||
function getRuleStatusText(status?: PointRuleStatus): string {
|
||||
return ({
|
||||
DRAFT: "草稿",
|
||||
|
||||
@@ -418,8 +418,11 @@ async function toDetail(item: PointLedger): Promise<void> {
|
||||
balanceBefore: item.balanceBefore,
|
||||
balanceAfter: item.balanceAfter,
|
||||
ruleId: item.ruleId,
|
||||
eventId: item.eventId,
|
||||
eventRecordId: item.eventRecordId,
|
||||
triggerSourceType: item.triggerSourceType,
|
||||
triggerSourceId: item.triggerSourceId,
|
||||
salesOrderId: item.salesOrderId,
|
||||
groovyScriptVersionId: item.groovyScriptVersionId,
|
||||
ruleSnapshot: item.ruleSnapshot,
|
||||
sourcePayload: item.sourcePayload,
|
||||
idempotencyKey: item.idempotencyKey,
|
||||
remark: item.remark,
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
<t-cell-group v-if="rule" title="基本信息" theme="card" v-entrance-transition="70">
|
||||
<t-cell title="所属门店" :note="storeName" />
|
||||
<t-cell
|
||||
title="登记事件"
|
||||
v-if="rule.triggerSourceType === 'EVENT_RECORD'"
|
||||
title="触发事件"
|
||||
:note="event?.name || '未绑定事件'"
|
||||
:arrow="canReadEvent && !!event?.id"
|
||||
:hover="canReadEvent && !!event?.id"
|
||||
@click="toEventDetail"
|
||||
/>
|
||||
<t-cell v-else-if="rule.triggerSourceType === 'SALES_ORDER_PAID'" title="触发来源" note="订单首次全额实收" />
|
||||
<t-cell title="规则名称" :note="rule.name || '未命名规则'" />
|
||||
<t-cell title="规则状态">
|
||||
<template #note>
|
||||
@@ -30,26 +32,9 @@
|
||||
<t-cell title="规则备注" :note="Text.display(rule.remark)" />
|
||||
</t-cell-group>
|
||||
<t-cell-group v-if="rule" title="积分配置" theme="card" v-entrance-transition="140">
|
||||
<t-cell title="触发类型" :note="getTriggerText(rule.triggerType)" />
|
||||
<t-cell title="积分值模式" :note="getModeText(rule.pointMode)" />
|
||||
<t-cell v-if="rule.pointMode === 'FIXED'" title="固定积分" :note="formatPointValue(rule.pointValue)" />
|
||||
<t-cell v-if="rule.pointMode === 'SOURCE_VALUE'" title="自定义积分范围" :note="sourceValueRangeText" />
|
||||
<t-cell v-if="rule.triggerType === 'AFTER_COUNT_EVERY_TIME'" title="触发阈值" :note="getThresholdText(rule)" />
|
||||
<t-cell v-if="rule.triggerType === 'FIRST_TIME'" title="匹配范围" note="每个客户的首次登记" />
|
||||
<t-cell v-if="rule.conditionJson?.window" title="累计窗口" :note="getWindowText(rule)" />
|
||||
<t-cell v-if="rule.conditionJson?.sourceEventId" title="来源事件">
|
||||
<template #note>
|
||||
<span v-text="sourceEvent?.name || rule.conditionJson.sourceEventId" />
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<t-cell-group
|
||||
v-if="tierTextList.length"
|
||||
title="积分分档"
|
||||
theme="card"
|
||||
v-entrance-transition="210"
|
||||
>
|
||||
<t-cell v-for="(text, index) in tierTextList" :key="index" :title="`第 ${index + 1} 档`" :note="text" />
|
||||
<t-cell title="规则模式" :note="rule.ruleMode === 'GROOVY' ? '脚本规则' : '简单规则'" />
|
||||
<t-cell v-if="rule.ruleMode !== 'GROOVY'" title="固定积分" :note="formatPointValue(rule.pointValue)" />
|
||||
<t-cell v-else title="执行脚本" :note="rule.groovyScriptId || '未绑定脚本'" />
|
||||
</t-cell-group>
|
||||
<t-cell-group v-if="rule" title="其他配置" theme="card" v-entrance-transition="280">
|
||||
<t-cell title="规则优先级" :note="formatNumber(rule.priority)" />
|
||||
@@ -82,8 +67,7 @@ import PointAPI from "@/api/PointAPI";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import type { PointRule, PointRuleStatus, PointRuleMode, PointRuleTriggerType } from "@/types/Point";
|
||||
import { formatEventNumberValue } from "@/utils/eventValue";
|
||||
import type { PointRule, PointRuleStatus } from "@/types/Point";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { Message } from "@/utils/message";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
@@ -118,31 +102,16 @@ const storeName = computed(() => {
|
||||
|
||||
const rule = ref<PointRule>();
|
||||
const event = ref<EventItem>();
|
||||
const sourceEvent = ref<EventItem>();
|
||||
const isLoading = ref(false);
|
||||
const isDeleting = ref(false);
|
||||
|
||||
const condition = computed(() => rule.value?.conditionJson);
|
||||
const isConsumeRule = computed(() => (rule.value?.pointValue ?? 0) < 0);
|
||||
const rulePointText = computed(() => {
|
||||
if (rule.value?.pointMode === "TIER") {
|
||||
return condition.value?.tiers?.length ? `${condition.value.tiers.length} 档积分` : "分档积分";
|
||||
}
|
||||
if (rule.value?.pointMode === "SOURCE_VALUE") {
|
||||
return "按登记时填写的积分变化";
|
||||
if (rule.value?.ruleMode === "GROOVY") {
|
||||
return "脚本规则";
|
||||
}
|
||||
return formatPointValue(rule.value?.pointValue);
|
||||
});
|
||||
const sourceValueRangeText = computed(() => {
|
||||
const minText = condition.value?.min === undefined ? "不限" : formatPointValue(condition.value.min);
|
||||
const maxText = condition.value?.max === undefined ? "不限" : formatPointValue(condition.value.max);
|
||||
return `最小值:${minText};最大值:${maxText}`;
|
||||
});
|
||||
const tierTextList = computed(() => (condition.value?.tiers || []).map(tier => {
|
||||
const minText = formatEventNumberValue(sourceEvent.value, tier.min);
|
||||
const maxText = tier.max === undefined || tier.max === null ? "以上" : formatEventNumberValue(sourceEvent.value, tier.max);
|
||||
return `${minText}~${maxText}:${formatPointValue(tier.pointValue)}`;
|
||||
}));
|
||||
|
||||
async function loadStoreScope(): Promise<void> {
|
||||
if (isGlobalManager.value) {
|
||||
@@ -166,13 +135,11 @@ async function loadRule(): Promise<void> {
|
||||
if (!ruleId) {
|
||||
rule.value = undefined;
|
||||
event.value = undefined;
|
||||
sourceEvent.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
rule.value = undefined;
|
||||
event.value = undefined;
|
||||
sourceEvent.value = undefined;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await loadStoreScope();
|
||||
@@ -181,20 +148,13 @@ async function loadRule(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventId = rule.value.eventId || "";
|
||||
const sourceEventId = rule.value.conditionJson?.sourceEventId || "";
|
||||
const [eventResult, sourceEventResult] = await Promise.all([
|
||||
eventId ? loadOptionalEvent(eventId, "积分规则登记事件加载失败") : Promise.resolve(undefined),
|
||||
sourceEventId && sourceEventId !== eventId
|
||||
? loadOptionalEvent(sourceEventId, "积分规则来源事件加载失败")
|
||||
: Promise.resolve(undefined)
|
||||
]);
|
||||
event.value = eventResult;
|
||||
sourceEvent.value = sourceEventResult || (sourceEventId === eventId ? eventResult : undefined);
|
||||
const triggerRefId = rule.value.triggerRefId || "";
|
||||
event.value = triggerRefId && rule.value.triggerSourceType === "EVENT_RECORD"
|
||||
? await loadOptionalEvent(triggerRefId, "积分规则触发事件加载失败")
|
||||
: undefined;
|
||||
} catch (error) {
|
||||
rule.value = undefined;
|
||||
event.value = undefined;
|
||||
sourceEvent.value = undefined;
|
||||
console.warn("积分规则详情加载失败", error);
|
||||
Message.error("积分规则加载失败");
|
||||
} finally {
|
||||
@@ -226,24 +186,6 @@ function getStatusTheme(status?: PointRuleStatus): "default" | "success" | "warn
|
||||
return "default";
|
||||
}
|
||||
|
||||
function getTriggerText(triggerType?: PointRuleTriggerType): string {
|
||||
return ({
|
||||
EVERY_TIME: "每次登记",
|
||||
FIRST_TIME: "首次登记",
|
||||
AFTER_COUNT_EVERY_TIME: "达到阈值后每次登记",
|
||||
SOURCE_VALUE: "登记时自定义积分",
|
||||
CUSTOMER_EVENT_VALUE_TIER: "按来源事件累计分档"
|
||||
} as Record<string, string>)[triggerType || ""] || "未知触发类型";
|
||||
}
|
||||
|
||||
function getModeText(pointMode?: PointRuleMode): string {
|
||||
return ({
|
||||
FIXED: "固定积分",
|
||||
SOURCE_VALUE: "使用登记时填写的积分",
|
||||
TIER: "分档积分"
|
||||
} as Record<string, string>)[pointMode || ""] || "未知积分模式";
|
||||
}
|
||||
|
||||
function formatPointValue(value?: number | null): string {
|
||||
if (value === undefined || value === null) {
|
||||
return "未配置";
|
||||
@@ -254,19 +196,6 @@ function formatPointValue(value?: number | null): string {
|
||||
return `${0 < value ? "发放" : "消耗"} ${Math.abs(value)} 分`;
|
||||
}
|
||||
|
||||
function getThresholdText(value: PointRule): string {
|
||||
const threshold = value.conditionJson?.threshold;
|
||||
return threshold === undefined || threshold === null ? "未配置" : `${threshold} 次`;
|
||||
}
|
||||
|
||||
function getWindowText(value: PointRule): string {
|
||||
const window = value.conditionJson?.window;
|
||||
if (!window) {
|
||||
return "未配置";
|
||||
}
|
||||
return `${window.amount} ${window.unit === "YEAR" ? "年" : window.unit}`;
|
||||
}
|
||||
|
||||
function formatNumber(value?: number): string {
|
||||
return value === undefined || value === null ? "未配置" : String(value);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
<t-form-item v-if="isGlobalManager" label="门店" required-mark :arrow="isCreateMode" @click="openStorePicker">
|
||||
<span class="picker-text" v-text="selectedStoreName" />
|
||||
</t-form-item>
|
||||
<t-form-item label="登记事件" required-mark arrow @click="openEventPicker">
|
||||
<t-form-item label="触发来源" required-mark arrow @click="openTriggerSourcePicker">
|
||||
<span class="picker-text" v-text="getTriggerSourceText(formModel.triggerSourceType)" />
|
||||
</t-form-item>
|
||||
<t-form-item v-if="formModel.triggerSourceType === 'EVENT_RECORD'" label="触发事件" required-mark arrow @click="openEventPicker">
|
||||
<span class="picker-text" v-text="selectedEventName" />
|
||||
</t-form-item>
|
||||
<t-form-item v-else label-width="0" class="form-tips">订单首次全额实收时触发,同一订单每条规则只生效一次</t-form-item>
|
||||
<t-form-item label="规则名称" required-mark>
|
||||
<t-input v-model.trim="formModel.name" align="right" borderless placeholder="例如:每次登记加 10 分" />
|
||||
</t-form-item>
|
||||
@@ -19,16 +23,10 @@
|
||||
</t-form-item>
|
||||
</card-container>
|
||||
<card-container v-entrance-transition="70">
|
||||
<t-form-item label="触发类型" arrow @click="openTriggerPicker">
|
||||
<span class="picker-text" v-text="getTriggerText(formModel.triggerType)" />
|
||||
</t-form-item>
|
||||
<t-form-item label="积分值模式" :arrow="formModel.triggerType !== 'CUSTOMER_EVENT_VALUE_TIER'" @click="openModePicker">
|
||||
<span class="picker-text" v-text="getModeText(formModel.pointMode)" />
|
||||
</t-form-item>
|
||||
<t-form-item v-if="formModel.pointMode === 'FIXED'" label="积分动作" arrow @click="openPointActionPicker">
|
||||
<t-form-item label="积分动作" arrow @click="openPointActionPicker">
|
||||
<span class="picker-text" v-text="getPointActionText(formModel.pointAction)" />
|
||||
</t-form-item>
|
||||
<t-form-item v-if="formModel.pointMode === 'FIXED'" label="积分数量" required-mark>
|
||||
<t-form-item label="积分数量" required-mark>
|
||||
<t-input
|
||||
v-model.trim="formModel.pointValueText"
|
||||
type="number"
|
||||
@@ -37,36 +35,6 @@
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="formModel.triggerType === 'AFTER_COUNT_EVERY_TIME'" label="触发阈值" required-mark>
|
||||
<t-input
|
||||
v-model.trim="formModel.thresholdText"
|
||||
type="number"
|
||||
align="right"
|
||||
borderless
|
||||
placeholder="累计登记达到几次"
|
||||
/>
|
||||
</t-form-item>
|
||||
<div v-if="formModel.pointMode === 'SOURCE_VALUE'" class="source-condition">
|
||||
<div class="source-title">自定义积分范围</div>
|
||||
<t-form-item label="积分动作" arrow @click="openPointActionPicker">
|
||||
<span class="picker-text" v-text="getPointActionText(formModel.pointAction)" />
|
||||
</t-form-item>
|
||||
<div class="source-inputs">
|
||||
<t-input v-model.trim="formModel.minText" type="number" align="right" borderless placeholder="最小值" />
|
||||
<span>至</span>
|
||||
<t-input v-model.trim="formModel.maxText" type="number" align="right" borderless placeholder="最大值" />
|
||||
</div>
|
||||
<span class="tips">登记时输入数量,系统会按选定动作执行积分变化</span>
|
||||
</div>
|
||||
<template v-if="formModel.triggerType === 'CUSTOMER_EVENT_VALUE_TIER'">
|
||||
<t-form-item label="来源事件" required-mark arrow @click="openSourceEventPicker">
|
||||
<span class="picker-text" v-text="selectedSourceEventName" />
|
||||
</t-form-item>
|
||||
<t-form-item label="累计窗口(年)" label-width="8rem">
|
||||
<t-input v-model.trim="formModel.windowAmountText" type="number" align="right" borderless />
|
||||
</t-form-item>
|
||||
<point-rule-tier-editor v-model="formModel.tiers" :value-unit="selectedSourceValueUnit" />
|
||||
</template>
|
||||
</card-container>
|
||||
<card-container v-entrance-transition="140">
|
||||
<t-form-item label="生效开始" arrow @click="openBeginAtPicker">
|
||||
@@ -102,18 +70,18 @@
|
||||
<t-picker
|
||||
v-model="eventPickerDraft"
|
||||
:columns="eventPickerColumns"
|
||||
title="选择登记事件"
|
||||
title="选择触发事件"
|
||||
@cancel="eventPickerVisible = false"
|
||||
@confirm="confirmEventPicker"
|
||||
/>
|
||||
</t-popup>
|
||||
<t-popup v-model="sourceEventPickerVisible" placement="bottom">
|
||||
<t-popup v-model="triggerSourcePickerVisible" placement="bottom">
|
||||
<t-picker
|
||||
v-model="sourceEventPickerDraft"
|
||||
:columns="sourceEventPickerColumns"
|
||||
title="选择来源事件"
|
||||
@cancel="sourceEventPickerVisible = false"
|
||||
@confirm="confirmSourceEventPicker"
|
||||
v-model="triggerSourcePickerDraft"
|
||||
:columns="triggerSourcePickerColumns"
|
||||
title="触发来源"
|
||||
@cancel="triggerSourcePickerVisible = false"
|
||||
@confirm="confirmTriggerSourcePicker"
|
||||
/>
|
||||
</t-popup>
|
||||
<t-popup v-model="statusPickerVisible" placement="bottom">
|
||||
@@ -125,24 +93,6 @@
|
||||
@confirm="confirmStatusPicker"
|
||||
/>
|
||||
</t-popup>
|
||||
<t-popup v-model="triggerPickerVisible" placement="bottom">
|
||||
<t-picker
|
||||
v-model="triggerPickerDraft"
|
||||
:columns="triggerPickerColumns"
|
||||
title="触发类型"
|
||||
@cancel="triggerPickerVisible = false"
|
||||
@confirm="confirmTriggerPicker"
|
||||
/>
|
||||
</t-popup>
|
||||
<t-popup v-model="modePickerVisible" placement="bottom">
|
||||
<t-picker
|
||||
v-model="modePickerDraft"
|
||||
:columns="modePickerColumns"
|
||||
title="积分值模式"
|
||||
@cancel="modePickerVisible = false"
|
||||
@confirm="confirmModePicker"
|
||||
/>
|
||||
</t-popup>
|
||||
<t-popup v-model="pointActionPickerVisible" placement="bottom">
|
||||
<t-picker
|
||||
v-model="pointActionPickerDraft"
|
||||
@@ -181,29 +131,17 @@ import { Time } from "timi-web";
|
||||
import { useUserStore } from "timi-web-api";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import PointAPI from "@/api/PointAPI";
|
||||
import PointRuleTierEditor from "@/pages/point/component/PointRuleTierEditor.vue";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import type {
|
||||
PointAction,
|
||||
PointRule,
|
||||
PointRuleMode,
|
||||
PointRuleStatus,
|
||||
PointRuleTier,
|
||||
PointRuleTierForm,
|
||||
PointRuleTriggerType
|
||||
PointRuleTriggerSourceType
|
||||
} from "@/types/Point";
|
||||
import { getEventValueUnitText } from "@/utils/eventValue";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
import {
|
||||
formatTierBoundaryValue,
|
||||
getTierMinValue,
|
||||
isLastTier,
|
||||
resolveNonNegativeInteger,
|
||||
resolveTierBoundaryValue
|
||||
} from "@/utils/pointRule";
|
||||
import { Message } from "@/utils/message";
|
||||
|
||||
type PickerValue = string | number;
|
||||
@@ -232,19 +170,12 @@ const isSaving = ref(false);
|
||||
const formModel = reactive({
|
||||
id: "",
|
||||
storeId: "",
|
||||
eventId: "",
|
||||
triggerSourceType: "EVENT_RECORD" as PointRuleTriggerSourceType,
|
||||
triggerRefId: "",
|
||||
name: "",
|
||||
status: "DRAFT" as PointRuleStatus,
|
||||
triggerType: "EVERY_TIME" as PointRuleTriggerType,
|
||||
pointMode: "FIXED" as PointRuleMode,
|
||||
pointAction: "EARN" as PointAction,
|
||||
pointValueText: "",
|
||||
thresholdText: "",
|
||||
minText: "",
|
||||
maxText: "",
|
||||
sourceEventId: "",
|
||||
windowAmountText: "1",
|
||||
tiers: [] as PointRuleTierForm[],
|
||||
beginAtText: "",
|
||||
endAtText: "",
|
||||
exclusive: false,
|
||||
@@ -255,7 +186,7 @@ const formModel = reactive({
|
||||
|
||||
const eventList = ref<EventItem[]>([]);
|
||||
const selectedStoreName = computed(() => currentStoreStore.storeList.find(item => item.id === formModel.storeId)?.name || "请选择门店");
|
||||
const selectedEventName = computed(() => eventList.value.find(item => item.id === formModel.eventId)?.name || "请选择登记事件");
|
||||
const selectedEventName = computed(() => eventList.value.find(item => item.id === formModel.triggerRefId)?.name || "请选择触发事件");
|
||||
const storePickerVisible = ref(false);
|
||||
const storePickerDraft = ref<PickerValue[]>([]);
|
||||
const storePickerColumns = computed(() => [
|
||||
@@ -270,36 +201,6 @@ const eventPickerColumns = computed(() => [
|
||||
.map(item => ({ label: item.name || "未命名事件", value: item.id || "" }))
|
||||
.filter(item => item.value)
|
||||
]);
|
||||
const sourceEventPickerVisible = ref(false);
|
||||
const sourceEventPickerDraft = ref<PickerValue[]>([]);
|
||||
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(() => [
|
||||
sourceEventList.value
|
||||
.map(item => ({ label: `${item.name || "未命名事件"}(${getEventValueUnitText(item.valueUnit)})`, value: item.id || "" }))
|
||||
.filter(item => item.value)
|
||||
]);
|
||||
const selectedSourceEventName = computed(() => selectedSourceEvent.value
|
||||
? `${selectedSourceEvent.value.name || "未命名事件"}(${getEventValueUnitText(selectedSourceEvent.value.valueUnit)})`
|
||||
: "请选择来源事件");
|
||||
|
||||
let tierKey = 0;
|
||||
|
||||
function createTierForm(tier?: PointRuleTier, isLast = false, valueUnit?: EventItem["valueUnit"]): PointRuleTierForm {
|
||||
const pointValue = tier?.pointValue;
|
||||
return {
|
||||
key: tierKey++,
|
||||
minText: tier?.min === undefined || tier.min === null ? "0" : formatTierBoundaryValue(tier.min, valueUnit),
|
||||
maxText: !isLast && tier?.max !== undefined && tier.max !== null ? formatTierBoundaryValue(tier.max, valueUnit) : "",
|
||||
pointAction: pointValue !== undefined && pointValue < 0 ? "CONSUME" : "EARN",
|
||||
pointValueText: pointValue === undefined ? "0" : String(Math.abs(pointValue))
|
||||
};
|
||||
}
|
||||
|
||||
if (!formModel.tiers.length) {
|
||||
formModel.tiers.push(createTierForm(undefined, true));
|
||||
}
|
||||
|
||||
function openStorePicker(): void {
|
||||
if (!isCreateMode.value || !storePickerColumns.value[0]?.length) {
|
||||
@@ -311,14 +212,14 @@ function openStorePicker(): void {
|
||||
|
||||
async function confirmStorePicker(value: Array<string | number>): Promise<void> {
|
||||
const storeId = String(value[0] || "");
|
||||
if (storeId) {
|
||||
if (storeId && storeId !== formModel.storeId) {
|
||||
formModel.storeId = storeId;
|
||||
formModel.eventId = "";
|
||||
formModel.triggerRefId = "";
|
||||
try {
|
||||
await loadEventList();
|
||||
} catch {
|
||||
eventList.value = [];
|
||||
Message.error("登记事件加载失败");
|
||||
Message.error("触发事件加载失败");
|
||||
}
|
||||
}
|
||||
storePickerVisible.value = false;
|
||||
@@ -326,47 +227,21 @@ async function confirmStorePicker(value: Array<string | number>): Promise<void>
|
||||
|
||||
function openEventPicker(): void {
|
||||
if (!eventPickerColumns.value[0]?.length) {
|
||||
Message.warning("暂无可选登记事件");
|
||||
Message.warning("暂无可选触发事件");
|
||||
return;
|
||||
}
|
||||
eventPickerDraft.value = [formModel.eventId];
|
||||
eventPickerDraft.value = [formModel.triggerRefId];
|
||||
eventPickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmEventPicker(value: Array<string | number>): void {
|
||||
const eventId = String(value[0] || "");
|
||||
if (eventId) {
|
||||
formModel.eventId = eventId;
|
||||
formModel.triggerRefId = eventId;
|
||||
}
|
||||
eventPickerVisible.value = false;
|
||||
}
|
||||
|
||||
function openSourceEventPicker(): void {
|
||||
if (formModel.triggerType !== "CUSTOMER_EVENT_VALUE_TIER") {
|
||||
return;
|
||||
}
|
||||
if (!sourceEventPickerColumns.value[0]?.length) {
|
||||
Message.warning("暂无可用的 NUMBER 来源事件");
|
||||
return;
|
||||
}
|
||||
sourceEventPickerDraft.value = [formModel.sourceEventId];
|
||||
sourceEventPickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmSourceEventPicker(value: Array<string | number>): void {
|
||||
const sourceEventId = String(value[0] || "");
|
||||
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)];
|
||||
Message.warning("来源事件单位已变化,原分档已清空,请重新配置");
|
||||
}
|
||||
formModel.sourceEventId = sourceEventId;
|
||||
}
|
||||
sourceEventPickerVisible.value = false;
|
||||
}
|
||||
|
||||
async function prepareStoreScope(): Promise<void> {
|
||||
if (isGlobalManager.value) {
|
||||
await currentStoreStore.loadStoreList();
|
||||
@@ -383,12 +258,10 @@ async function prepareStoreScope(): Promise<void> {
|
||||
async function loadEventList(): Promise<void> {
|
||||
const options = isGlobalManager.value ? { storeId: formModel.storeId } : undefined;
|
||||
const result = await EventAPI.list({ index: 0, size: 1000 }, options);
|
||||
eventList.value = result.list || [];
|
||||
const requiredEventIdList = [formModel.eventId, formModel.sourceEventId].filter((id, index, list) => id && list.indexOf(id) === index);
|
||||
for (const eventId of requiredEventIdList) {
|
||||
if (!eventList.value.some(item => item.id === eventId)) {
|
||||
eventList.value.push(await EventAPI.detail(eventId));
|
||||
}
|
||||
// 退役消费事件不能作为触发对象
|
||||
eventList.value = (result.list || []).filter(item => item.bizType !== "LEGACY_CONSUMPTION");
|
||||
if (formModel.triggerRefId && !eventList.value.some(item => item.id === formModel.triggerRefId)) {
|
||||
eventList.value.push(await EventAPI.detail(formModel.triggerRefId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,31 +272,16 @@ const statusOptions: Array<{ label: string; value: PointRuleStatus }> = [
|
||||
{ label: "启用", value: "ACTIVE" },
|
||||
{ label: "停用", value: "INACTIVE" }
|
||||
];
|
||||
const triggerOptions: Array<{ label: string; value: PointRuleTriggerType }> = [
|
||||
{ label: "每次登记", value: "EVERY_TIME" },
|
||||
{ label: "首次登记", value: "FIRST_TIME" },
|
||||
{ label: "达到阈值后每次登记", value: "AFTER_COUNT_EVERY_TIME" },
|
||||
{ label: "登记时自定义积分", value: "SOURCE_VALUE" },
|
||||
{ label: "按来源事件累计分档", value: "CUSTOMER_EVENT_VALUE_TIER" }
|
||||
const triggerSourceOptions: Array<{ label: string; value: PointRuleTriggerSourceType }> = [
|
||||
{ label: "事件登记", value: "EVENT_RECORD" },
|
||||
{ label: "订单全额实收", value: "SALES_ORDER_PAID" }
|
||||
];
|
||||
const modeOptions = computed<Array<{ label: string; value: PointRuleMode }>>(() => {
|
||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
||||
return [{ label: "分档积分", value: "TIER" }];
|
||||
}
|
||||
if (formModel.triggerType === "SOURCE_VALUE") {
|
||||
return [{ label: "使用登记时填写的积分", value: "SOURCE_VALUE" }];
|
||||
}
|
||||
return [{ label: "固定积分", value: "FIXED" }];
|
||||
});
|
||||
const statusPickerVisible = ref(false);
|
||||
const statusPickerDraft = ref<PickerValue[]>([]);
|
||||
const statusPickerColumns = [statusOptions];
|
||||
const triggerPickerVisible = ref(false);
|
||||
const triggerPickerDraft = ref<PickerValue[]>([]);
|
||||
const triggerPickerColumns = [triggerOptions];
|
||||
const modePickerVisible = ref(false);
|
||||
const modePickerDraft = ref<PickerValue[]>([]);
|
||||
const modePickerColumns = computed(() => [modeOptions.value]);
|
||||
const triggerSourcePickerVisible = ref(false);
|
||||
const triggerSourcePickerDraft = ref<PickerValue[]>([]);
|
||||
const triggerSourcePickerColumns = [triggerSourceOptions];
|
||||
const pointActionOptions: Array<{ label: string; value: PointAction }> = [
|
||||
{ label: "发放积分", value: "EARN" },
|
||||
{ label: "消耗积分", value: "CONSUME" }
|
||||
@@ -445,48 +303,23 @@ function confirmStatusPicker(value: Array<string | number>): void {
|
||||
statusPickerVisible.value = false;
|
||||
}
|
||||
|
||||
function openTriggerPicker(): void {
|
||||
triggerPickerDraft.value = [formModel.triggerType];
|
||||
triggerPickerVisible.value = true;
|
||||
function openTriggerSourcePicker(): void {
|
||||
triggerSourcePickerDraft.value = [formModel.triggerSourceType];
|
||||
triggerSourcePickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmTriggerPicker(value: Array<string | number>): void {
|
||||
const triggerType = value[0];
|
||||
if (typeof triggerType === "string" && triggerOptions.some(item => item.value === triggerType)) {
|
||||
formModel.triggerType = triggerType as PointRuleTriggerType;
|
||||
formModel.pointMode = triggerType === "SOURCE_VALUE" ? "SOURCE_VALUE" : triggerType === "CUSTOMER_EVENT_VALUE_TIER" ? "TIER" : "FIXED";
|
||||
}
|
||||
triggerPickerVisible.value = false;
|
||||
}
|
||||
|
||||
function openModePicker(): void {
|
||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
||||
return;
|
||||
}
|
||||
modePickerDraft.value = [formModel.pointMode];
|
||||
modePickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmModePicker(value: Array<string | number>): void {
|
||||
const pointMode = value[0];
|
||||
if (pointMode === "FIXED" || pointMode === "SOURCE_VALUE" || pointMode === "TIER") {
|
||||
if (!modeOptions.value.some(item => item.value === pointMode)) {
|
||||
return;
|
||||
function confirmTriggerSourcePicker(value: Array<string | number>): void {
|
||||
const triggerSourceType = value[0];
|
||||
if (typeof triggerSourceType === "string" && triggerSourceOptions.some(item => item.value === triggerSourceType)) {
|
||||
formModel.triggerSourceType = triggerSourceType as PointRuleTriggerSourceType;
|
||||
if (formModel.triggerSourceType === "SALES_ORDER_PAID") {
|
||||
formModel.triggerRefId = "";
|
||||
}
|
||||
formModel.pointMode = pointMode;
|
||||
formModel.triggerType = pointMode === "SOURCE_VALUE"
|
||||
? "SOURCE_VALUE"
|
||||
: pointMode === "TIER"
|
||||
? "CUSTOMER_EVENT_VALUE_TIER"
|
||||
: formModel.triggerType === "SOURCE_VALUE" || formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER" ? "EVERY_TIME" : formModel.triggerType;
|
||||
}
|
||||
modePickerVisible.value = false;
|
||||
triggerSourcePickerVisible.value = false;
|
||||
}
|
||||
|
||||
function openPointActionPicker(): void {
|
||||
if (formModel.pointMode === "TIER") {
|
||||
return;
|
||||
}
|
||||
pointActionPickerDraft.value = [formModel.pointAction];
|
||||
pointActionPickerVisible.value = true;
|
||||
}
|
||||
@@ -503,12 +336,8 @@ function getStatusText(status: PointRuleStatus): string {
|
||||
return statusOptions.find(item => item.value === status)?.label || "草稿";
|
||||
}
|
||||
|
||||
function getTriggerText(triggerType: PointRuleTriggerType): string {
|
||||
return triggerOptions.find(item => item.value === triggerType)?.label || "每次登记";
|
||||
}
|
||||
|
||||
function getModeText(pointMode: PointRuleMode): string {
|
||||
return modeOptions.value.find(item => item.value === pointMode)?.label || "固定积分";
|
||||
function getTriggerSourceText(triggerSourceType: PointRuleTriggerSourceType): string {
|
||||
return triggerSourceOptions.find(item => item.value === triggerSourceType)?.label || "事件登记";
|
||||
}
|
||||
|
||||
function getPointActionText(action: PointAction): string {
|
||||
@@ -561,9 +390,9 @@ function resolvePositiveInteger(value: string): number | undefined {
|
||||
return Number.isSafeInteger(numberValue) && 0 < numberValue ? numberValue : undefined;
|
||||
}
|
||||
|
||||
function resolveSignedPointValue(value: string, action: PointAction): number | undefined {
|
||||
const pointValue = resolvePositiveInteger(value);
|
||||
return pointValue === undefined ? undefined : action === "CONSUME" ? -pointValue : pointValue;
|
||||
function resolvePointValue(): number | undefined {
|
||||
const pointValue = resolvePositiveInteger(formModel.pointValueText);
|
||||
return pointValue === undefined ? undefined : formModel.pointAction === "CONSUME" ? -pointValue : pointValue;
|
||||
}
|
||||
|
||||
function resolveTimestamp(value: string): number | undefined {
|
||||
@@ -574,121 +403,23 @@ function resolveTimestamp(value: string): number | undefined {
|
||||
return Number.isFinite(timestamp) ? timestamp : undefined;
|
||||
}
|
||||
|
||||
function buildCondition(): PointRule["conditionJson"] {
|
||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
||||
const valueUnit = selectedSourceValueUnit.value;
|
||||
return {
|
||||
sourceEventId: formModel.sourceEventId,
|
||||
sourceValueField: "numberValue",
|
||||
window: {
|
||||
type: "ROLLING",
|
||||
amount: resolvePositiveInteger(formModel.windowAmountText) || 1,
|
||||
unit: "YEAR"
|
||||
},
|
||||
tiers: formModel.tiers.map((tier, index) => {
|
||||
const min = getTierMinValue(formModel.tiers, index, valueUnit);
|
||||
const result: PointRuleTier = {
|
||||
min: min ?? 0,
|
||||
pointValue: (tier.pointAction === "CONSUME" ? -1 : 1) * (resolveNonNegativeInteger(tier.pointValueText) || 0)
|
||||
};
|
||||
if (!isLastTier(formModel.tiers, index)) {
|
||||
result.max = resolveTierBoundaryValue(tier.maxText, valueUnit);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
};
|
||||
}
|
||||
if (formModel.pointMode === "SOURCE_VALUE") {
|
||||
return {
|
||||
min: resolveSignedPointValue(formModel.minText, formModel.pointAction),
|
||||
max: resolveSignedPointValue(formModel.maxText, formModel.pointAction)
|
||||
};
|
||||
}
|
||||
if (formModel.triggerType === "FIRST_TIME") {
|
||||
return { scope: "CUSTOMER_EVENT" };
|
||||
}
|
||||
if (formModel.triggerType === "AFTER_COUNT_EVERY_TIME") {
|
||||
return {
|
||||
threshold: resolvePositiveInteger(formModel.thresholdText),
|
||||
includeCurrent: true,
|
||||
scope: "CUSTOMER_EVENT"
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
if (!formModel.storeId) {
|
||||
Message.warning("请选择门店");
|
||||
return false;
|
||||
}
|
||||
if (!formModel.eventId) {
|
||||
Message.warning("请选择登记事件");
|
||||
return false;
|
||||
}
|
||||
if (!formModel.name.trim()) {
|
||||
Message.warning("请输入规则名称");
|
||||
return false;
|
||||
}
|
||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
||||
if (formModel.pointMode !== "TIER") {
|
||||
Message.warning("分档规则必须使用分档积分模式");
|
||||
return false;
|
||||
}
|
||||
if (!selectedSourceEvent.value) {
|
||||
Message.warning("请选择 NUMBER 类型的来源事件");
|
||||
return false;
|
||||
}
|
||||
if (selectedSourceValueUnit.value !== "CENT" && selectedSourceValueUnit.value !== "COUNT") {
|
||||
Message.warning("来源事件必须配置数值单位");
|
||||
return false;
|
||||
}
|
||||
if (!resolvePositiveInteger(formModel.windowAmountText)) {
|
||||
Message.warning("累计窗口必须是正整数");
|
||||
return false;
|
||||
}
|
||||
if (!formModel.tiers.length || !(formModel.tiers.length < 100)) {
|
||||
Message.warning("分档数量必须是 1-100 档");
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < formModel.tiers.length; i++) {
|
||||
const tier = formModel.tiers[i];
|
||||
const min = getTierMinValue(formModel.tiers, i, selectedSourceValueUnit.value);
|
||||
const pointValue = resolveNonNegativeInteger(tier.pointValueText);
|
||||
if (min === undefined) {
|
||||
Message.warning(`第 ${i + 1} 档的上一档最大值无效`);
|
||||
return false;
|
||||
}
|
||||
if (pointValue === undefined) {
|
||||
Message.warning(`第 ${i + 1} 档积分数量必须是大于等于 0 的整数`);
|
||||
return false;
|
||||
}
|
||||
if (!isLastTier(formModel.tiers, i)) {
|
||||
const max = resolveTierBoundaryValue(tier.maxText, selectedSourceValueUnit.value);
|
||||
if (max === undefined || max < min) {
|
||||
Message.warning(selectedSourceValueUnit.value === "CENT"
|
||||
? `第 ${i + 1} 档最大值必须是不小于最小值且最多两位小数`
|
||||
: `第 ${i + 1} 档最大值必须是不小于最小值的整数`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (formModel.pointMode === "FIXED" && !resolvePositiveInteger(formModel.pointValueText)) {
|
||||
if (formModel.triggerSourceType === "EVENT_RECORD" && !formModel.triggerRefId) {
|
||||
Message.warning("请选择触发事件");
|
||||
return false;
|
||||
}
|
||||
if (!resolvePositiveInteger(formModel.pointValueText)) {
|
||||
Message.warning("积分数量必须是正整数");
|
||||
return false;
|
||||
}
|
||||
if (formModel.triggerType === "AFTER_COUNT_EVERY_TIME" && !resolvePositiveInteger(formModel.thresholdText)) {
|
||||
Message.warning("请输入有效的触发阈值");
|
||||
return false;
|
||||
}
|
||||
if (formModel.pointMode === "SOURCE_VALUE") {
|
||||
const min = resolvePositiveInteger(formModel.minText);
|
||||
const max = resolvePositiveInteger(formModel.maxText);
|
||||
if (!min || !max || max < min) {
|
||||
Message.warning("积分数量范围必须是正整数,且最大值不能小于最小值");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const beginAt = resolveTimestamp(formModel.beginAtText);
|
||||
const endAt = resolveTimestamp(formModel.endAtText);
|
||||
if (beginAt !== undefined && endAt !== undefined && endAt < beginAt) {
|
||||
@@ -707,13 +438,14 @@ async function saveRule(): Promise<void> {
|
||||
const payload: PointRule = {
|
||||
id: formModel.id || undefined,
|
||||
storeId: formModel.storeId,
|
||||
eventId: formModel.eventId,
|
||||
ruleMode: "SIMPLE",
|
||||
triggerSourceType: formModel.triggerSourceType,
|
||||
triggerRefType: formModel.triggerSourceType === "EVENT_RECORD" ? "EVENT" : undefined,
|
||||
triggerRefId: formModel.triggerSourceType === "EVENT_RECORD" ? formModel.triggerRefId : undefined,
|
||||
name: formModel.name.trim(),
|
||||
status: formModel.status,
|
||||
triggerType: formModel.triggerType,
|
||||
pointMode: formModel.pointMode,
|
||||
pointValue: formModel.pointMode === "FIXED" ? resolveSignedPointValue(formModel.pointValueText, formModel.pointAction) : null,
|
||||
conditionJson: buildCondition(),
|
||||
pointValue: resolvePointValue() ?? null,
|
||||
groovyScriptId: undefined,
|
||||
beginAt: resolveTimestamp(formModel.beginAtText),
|
||||
endAt: resolveTimestamp(formModel.endAtText),
|
||||
exclusive: formModel.exclusive,
|
||||
@@ -742,37 +474,19 @@ async function fillForm(): Promise<void> {
|
||||
Object.assign(formModel, {
|
||||
id: current.id || "",
|
||||
storeId: current.storeId || formModel.storeId,
|
||||
eventId: current.eventId || "",
|
||||
triggerSourceType: current.triggerSourceType || "EVENT_RECORD",
|
||||
triggerRefId: current.triggerRefId || "",
|
||||
name: current.name || "",
|
||||
status: current.status === "ACTIVE" ? "ACTIVE" : current.status === "INACTIVE" ? "INACTIVE" : "DRAFT",
|
||||
triggerType: current.triggerType || "EVERY_TIME",
|
||||
pointMode: current.pointMode || "FIXED",
|
||||
pointAction: current.pointValue !== undefined && current.pointValue !== null
|
||||
? current.pointValue < 0 ? "CONSUME" : "EARN"
|
||||
: current.conditionJson?.min !== undefined && current.conditionJson.min < 0 ? "CONSUME" : "EARN",
|
||||
pointAction: current.pointValue !== undefined && current.pointValue !== null && current.pointValue < 0 ? "CONSUME" : "EARN",
|
||||
pointValueText: current.pointValue === undefined || current.pointValue === null ? "" : String(Math.abs(current.pointValue)),
|
||||
thresholdText: current.conditionJson?.threshold === undefined ? "" : String(current.conditionJson.threshold),
|
||||
minText: current.conditionJson?.min === undefined ? "" : String(Math.abs(current.conditionJson.min)),
|
||||
maxText: current.conditionJson?.max === undefined ? "" : String(Math.abs(current.conditionJson.max)),
|
||||
sourceEventId: current.conditionJson?.sourceEventId || "",
|
||||
windowAmountText: current.conditionJson?.window?.amount === undefined ? "1" : String(current.conditionJson.window.amount),
|
||||
tiers: [],
|
||||
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
|
||||
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
||||
exclusive: current.exclusive ?? false,
|
||||
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();
|
||||
if (!formModel.tiers.length) {
|
||||
formModel.tiers.push(createTierForm(undefined, true, selectedSourceValueUnit.value));
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -821,54 +535,10 @@ onUnmounted(() => navBarStore.clearRight());
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.source-condition {
|
||||
padding: .75rem 1rem 1rem;
|
||||
|
||||
.source-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.source-inputs {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: .5rem;
|
||||
|
||||
.t-input {
|
||||
flex: 1;
|
||||
border: 1px solid var(--td-component-border);
|
||||
padding: .25rem .5rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tier-condition {
|
||||
padding: .75rem 1rem 1rem;
|
||||
|
||||
.window-row {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
|
||||
.t-input {
|
||||
flex: 1;
|
||||
border: 1px solid var(--td-component-border);
|
||||
padding: .25rem .5rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.window-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
.form-tips {
|
||||
padding: 0 1rem;
|
||||
font-size: .8125rem;
|
||||
color: #52607a;
|
||||
font-size: .75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -880,15 +550,9 @@ onUnmounted(() => navBarStore.clearRight());
|
||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||
|
||||
.picker-text,
|
||||
.source-condition p {
|
||||
.form-tips {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.source-inputs .t-input {
|
||||
background: rgba(15, 23, 42, .72);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="description">
|
||||
<div v-text="`登记事件:${item.eventId ? eventNameMap.get(item.eventId) : '未绑定事件'}`"></div>
|
||||
<div v-text="`触发类型:${getTriggerText(item)}`"></div>
|
||||
<div v-text="`触发来源:${getTriggerSourceText(item)}`"></div>
|
||||
<div v-text="`触发对象:${getTriggerRefText(item)}`"></div>
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
@@ -220,20 +220,20 @@ async function loadEventList(): Promise<void> {
|
||||
|
||||
// ---------- 展示与导航 ----------
|
||||
|
||||
function getTriggerText(rule: PointRule): string {
|
||||
const map: Record<string, string> = {
|
||||
EVERY_TIME: "每次登记",
|
||||
FIRST_TIME: "首次登记",
|
||||
AFTER_COUNT_EVERY_TIME: `达到 ${rule.conditionJson?.threshold ?? 0} 次后每次登记`,
|
||||
SOURCE_VALUE: "登记时自定义积分",
|
||||
CUSTOMER_EVENT_VALUE_TIER: `按 ${rule.conditionJson?.window?.amount ?? 1} 年累计分档`
|
||||
};
|
||||
return map[rule.triggerType || ""] || "未知触发类型";
|
||||
function getTriggerSourceText(rule: PointRule): string {
|
||||
return rule.triggerSourceType === "SALES_ORDER_PAID" ? "订单全额实收" : "事件登记";
|
||||
}
|
||||
|
||||
function getTriggerRefText(rule: PointRule): string {
|
||||
if (rule.triggerSourceType === "SALES_ORDER_PAID") {
|
||||
return "订单";
|
||||
}
|
||||
return rule.triggerRefId ? eventNameMap.value.get(rule.triggerRefId) || "未知事件" : "未绑定事件";
|
||||
}
|
||||
|
||||
function getPointText(rule: PointRule): string {
|
||||
if (rule.pointMode === "TIER") {
|
||||
return "分档";
|
||||
if (rule.ruleMode === "GROOVY") {
|
||||
return "脚本";
|
||||
}
|
||||
if (rule.pointValue === undefined || rule.pointValue === null) {
|
||||
return "?";
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<template #description>
|
||||
<div class="description">
|
||||
<span v-text="'积分值:' + getPointText(item)" />
|
||||
<span v-text="'登记事件:' + (eventNameMap.get(item.eventId || '') || '未绑定事件')" />
|
||||
<span v-text="'触发对象:' + (item.triggerRefId ? eventNameMap.get(item.triggerRefId) || '未知事件' : '任意订单')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #note>
|
||||
@@ -66,11 +66,11 @@ const sortDirty = ref(false);
|
||||
const eventNameMap = computed(() => new Map(eventList.value.filter(item => item.id).map(item => [item.id as string, item.name])));
|
||||
|
||||
function getPointText(rule: PointRule): string {
|
||||
if (rule.pointMode === "TIER") {
|
||||
return "分档";
|
||||
if (rule.ruleMode === "GROOVY") {
|
||||
return "脚本";
|
||||
}
|
||||
if (rule.pointValue === undefined || rule.pointValue === null) {
|
||||
return "按登记值";
|
||||
return "未配置";
|
||||
}
|
||||
return (0 < rule.pointValue ? "+" : "") + rule.pointValue;
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<div class="tier-editor">
|
||||
<div class="title">
|
||||
<div v-text="`累计数值分档(${valueUnitText})`" />
|
||||
<div class="tips gray" v-text="`首档起点可自定义,末档不设上限;分档单位为${valueUnitText},积分为 0 不产生流水`" />
|
||||
</div>
|
||||
<div class="list">
|
||||
<div
|
||||
v-for="(tier, index) in tiers"
|
||||
:key="tier.key"
|
||||
class="card"
|
||||
@click="openTierEditor(index)"
|
||||
>
|
||||
<div class="summary">
|
||||
<span class="index" v-text="`第 ${index + 1} 档`" />
|
||||
<strong v-text="getTierRangeText(index)" />
|
||||
</div>
|
||||
<div
|
||||
class="point text-bold"
|
||||
:class="{ 'consume': tier.pointAction === 'CONSUME' }"
|
||||
v-text="getTierPointText(tier)"
|
||||
/>
|
||||
<t-button
|
||||
v-if="1 < tiers.length"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
shape="square"
|
||||
@click.stop="removeTier(index)"
|
||||
>
|
||||
<t-icon name="close" />
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
<t-button class="add-tier" variant="outline" size="small" content="增加分档" @click="addTier" />
|
||||
</div>
|
||||
<bottom-popup
|
||||
v-model="tierEditorVisible"
|
||||
:title="tierEditorTitle"
|
||||
@left-button-click="closeTierEditor"
|
||||
@right-button-click="confirmTierEditor"
|
||||
>
|
||||
<div class="tier-editor-popup">
|
||||
<t-form v-if="tierEditorIndex >= 0" class="form" label-align="left" label-width="10rem">
|
||||
<t-form-item v-if="tierEditorIndex < 1" :label="`最小值(${valueUnitText})`" required-mark>
|
||||
<t-input
|
||||
v-model.trim="tierEditorDraft.minText"
|
||||
type="number"
|
||||
:step="valueUnit === 'CENT' ? '0.01' : '1'"
|
||||
align="right"
|
||||
borderless
|
||||
:placeholder="valueUnit === 'CENT' ? '最多两位小数' : '请输入非负整数'"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item v-else :label="`最小值(${valueUnitText})`" content-align="right">
|
||||
<span class="gray" v-text="getTierMinText(tiers, tierEditorIndex, valueUnit)" />
|
||||
</t-form-item>
|
||||
<t-form-item
|
||||
v-if="!isLastTier(tiers, tierEditorIndex) || tierEditorAdding"
|
||||
:label="`${tierEditorAdding ? '当前档最大值' : '最大值'}(${valueUnitText})`"
|
||||
required-mark
|
||||
>
|
||||
<t-input
|
||||
v-model.trim="tierEditorDraft.maxText"
|
||||
type="number"
|
||||
:step="valueUnit === 'CENT' ? '0.01' : '1'"
|
||||
align="right"
|
||||
borderless
|
||||
:placeholder="valueUnit === 'CENT' ? '最多两位小数' : '请输入整数'"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="积分动作" content-align="right">
|
||||
<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 label="积分数量" required-mark>
|
||||
<t-input
|
||||
v-model.trim="tierEditorDraft.pointValueText"
|
||||
type="number"
|
||||
align="right"
|
||||
borderless
|
||||
placeholder="请输入数量,可填写 0"
|
||||
/>
|
||||
</t-form-item>
|
||||
<div class="tips">积分为 0 时规则仍会命中,但系统不会创建积分流水。</div>
|
||||
</t-form>
|
||||
</div>
|
||||
</bottom-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import type { EventValueUnit } from "@/types/EventItem";
|
||||
import type { PointAction, PointRuleTierForm } from "@/types/Point";
|
||||
import { formatTierBoundaryValue, getTierMinText, getTierMinValue, isLastTier, resolveNonNegativeInteger, resolveTierBoundaryValue } from "@/utils/pointRule";
|
||||
import { Message } from "@/utils/message";
|
||||
|
||||
defineOptions({
|
||||
name: "PointRuleTierEditor"
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
valueUnit?: EventValueUnit | null;
|
||||
}>();
|
||||
const tiers = defineModel<PointRuleTierForm[]>({ required: true });
|
||||
const tierEditorVisible = ref(false);
|
||||
const tierEditorIndex = ref(-1);
|
||||
const tierEditorAdding = ref(false);
|
||||
const tierEditorDraft = reactive({
|
||||
minText: "",
|
||||
maxText: "",
|
||||
pointAction: "EARN" as PointAction,
|
||||
pointValueText: ""
|
||||
});
|
||||
const tierEditorTitle = computed(() => {
|
||||
if (tierEditorIndex.value < 0) {
|
||||
return "编辑分档";
|
||||
}
|
||||
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 {
|
||||
const tier = tiers.value[index];
|
||||
if (!tier) {
|
||||
return "待填写";
|
||||
}
|
||||
const minText = getTierMinText(tiers.value, index, valueUnit.value);
|
||||
if (isLastTier(tiers.value, index)) {
|
||||
return `${minText} 及以上`;
|
||||
}
|
||||
const max = resolveTierBoundaryValue(tier.maxText, valueUnit.value);
|
||||
return max === undefined ? `${minText} - 待填写` : `${minText}-${formatTierBoundaryValue(max, valueUnit.value)}`;
|
||||
}
|
||||
|
||||
function getTierPointText(tier: PointRuleTierForm): string {
|
||||
const pointValue = resolveNonNegativeInteger(tier.pointValueText);
|
||||
if (pointValue === undefined) {
|
||||
return "待填写";
|
||||
}
|
||||
return `${tier.pointAction === "CONSUME" ? "-" : "+"}${pointValue} 分`;
|
||||
}
|
||||
|
||||
function openTierEditor(index: number, adding = false): void {
|
||||
const tier = tiers.value[index];
|
||||
if (!tier) {
|
||||
return;
|
||||
}
|
||||
tierEditorIndex.value = index;
|
||||
tierEditorAdding.value = adding;
|
||||
tierEditorDraft.minText = index < 1 ? tier.minText : "";
|
||||
tierEditorDraft.maxText = adding ? "" : tier.maxText;
|
||||
tierEditorDraft.pointAction = tier.pointAction === "CONSUME" ? "CONSUME" : "EARN";
|
||||
tierEditorDraft.pointValueText = tier.pointValueText;
|
||||
tierEditorVisible.value = true;
|
||||
}
|
||||
|
||||
function closeTierEditor(): void {
|
||||
tierEditorAdding.value = false;
|
||||
tierEditorVisible.value = false;
|
||||
}
|
||||
|
||||
function confirmTierEditor(): void {
|
||||
const index = tierEditorIndex.value;
|
||||
const tier = tiers.value[index];
|
||||
if (!tier) {
|
||||
return;
|
||||
}
|
||||
const min = index < 1
|
||||
? resolveTierBoundaryValue(tierEditorDraft.minText, valueUnit.value)
|
||||
: getTierMinValue(tiers.value, index, valueUnit.value);
|
||||
const pointValue = resolveNonNegativeInteger(tierEditorDraft.pointValueText);
|
||||
if (min === undefined) {
|
||||
Message.warning(index < 1
|
||||
? valueUnit.value === "CENT" ? "首档最小值必须是非负且最多两位小数" : "首档最小值必须是非负整数"
|
||||
: "请先修正上一档的最大值");
|
||||
return;
|
||||
}
|
||||
if (pointValue === undefined) {
|
||||
Message.warning("积分数量必须是大于等于 0 的整数,允许填写 0");
|
||||
return;
|
||||
}
|
||||
const shouldAddTier = tierEditorAdding.value && isLastTier(tiers.value, index);
|
||||
if (!isLastTier(tiers.value, index) || shouldAddTier) {
|
||||
const max = resolveTierBoundaryValue(tierEditorDraft.maxText, valueUnit.value);
|
||||
if (max === undefined || max < min) {
|
||||
Message.warning(valueUnit.value === "CENT" ? "当前档最大值必须是不小于最小值且最多两位小数" : "当前档最大值必须是不小于最小值的整数");
|
||||
return;
|
||||
}
|
||||
tier.maxText = formatTierBoundaryValue(max, valueUnit.value);
|
||||
} else {
|
||||
tier.maxText = "";
|
||||
}
|
||||
if (index < 1) {
|
||||
tier.minText = formatTierBoundaryValue(min, valueUnit.value);
|
||||
}
|
||||
tier.pointValueText = String(pointValue);
|
||||
tier.pointAction = tierEditorDraft.pointAction;
|
||||
if (shouldAddTier) {
|
||||
tiers.value.push(createTierForm());
|
||||
}
|
||||
closeTierEditor();
|
||||
}
|
||||
|
||||
function addTier(): void {
|
||||
if (!(tiers.value.length < 100)) {
|
||||
Message.warning("最多配置 100 档");
|
||||
return;
|
||||
}
|
||||
const lastTierIndex = tiers.value.length - 1;
|
||||
if (!(lastTierIndex < 0)) {
|
||||
openTierEditor(lastTierIndex, true);
|
||||
return;
|
||||
}
|
||||
tiers.value.push(createTierForm());
|
||||
}
|
||||
|
||||
function removeTier(index: number): void {
|
||||
if (!(1 < tiers.value.length)) {
|
||||
return;
|
||||
}
|
||||
tiers.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function createTierForm(): PointRuleTierForm {
|
||||
const key = tiers.value.reduce((max, tier) => Math.max(max, tier.key), -1) + 1;
|
||||
return {
|
||||
key,
|
||||
minText: "0",
|
||||
maxText: "",
|
||||
pointAction: "EARN",
|
||||
pointValueText: "0"
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tier-editor {
|
||||
padding: 1rem;
|
||||
|
||||
.tips {
|
||||
font-size: .75rem;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
gap: .5rem;
|
||||
display: grid;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: .5rem .75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-component-border);
|
||||
border-radius: .75rem;
|
||||
|
||||
.summary {
|
||||
gap: .25rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
strong {
|
||||
font-size: .9375rem;
|
||||
}
|
||||
|
||||
.index {
|
||||
color: var(--td-brand-color);
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.point {
|
||||
gap: .25rem;
|
||||
color: var(--td-brand-color);
|
||||
display: flex;
|
||||
font-size: 1.5rem;
|
||||
align-items: baseline;
|
||||
white-space: nowrap;
|
||||
|
||||
&.consume {
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-tier {
|
||||
width: 100%;
|
||||
margin-top: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tier-editor-popup {
|
||||
|
||||
.form {
|
||||
padding: .5rem 0 1rem 0;
|
||||
|
||||
.tips {
|
||||
color: #52607a;
|
||||
padding: 0 1rem;
|
||||
font-size: .75rem;
|
||||
line-height: 1.5;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.tier-editor {
|
||||
.tips,
|
||||
.points span {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(15, 23, 42, .72);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
|
||||
.tier-editor-popup {
|
||||
background: #0f172a;
|
||||
|
||||
:deep(.t-input) {
|
||||
background: rgba(15, 23, 42, .72);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
|
||||
.form {
|
||||
.picker-text,
|
||||
.tips {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user