From 1187810eb0fb3aa4fba6a68164fda20e43bf6933 Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 03 六月 2026 11:37:22 +0800
Subject: [PATCH] feat: 新增特殊时段播单策略与行程分享配置
---
src/views/transaction/transactionStrategy/index.vue | 379 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 378 insertions(+), 1 deletions(-)
diff --git a/src/views/transaction/transactionStrategy/index.vue b/src/views/transaction/transactionStrategy/index.vue
index ee35b13..acab23e 100644
--- a/src/views/transaction/transactionStrategy/index.vue
+++ b/src/views/transaction/transactionStrategy/index.vue
@@ -16,6 +16,7 @@
setFalse: endLoading,
} = useBoolean(false)
const message = useMessage()
+const dialog = useDialog()
const router = useRouter()
const route = useRoute()
const tabStore = useTabStore()
@@ -41,6 +42,7 @@
fourPushTime: null, // 四轮推单时间
fivePushDistance: null, // 五轮推单距离
fivePushTime: null, // 五轮推单时间
+ pushTimeList: [] as any[], // 特殊时段播单策略
passwordFreeAmount: null, // 免密授权金额
prepaidLimit: null, // 预付额度 订单大于金额需预付
ruleType: 1, // 规则类型
@@ -74,6 +76,240 @@
const formRef = ref()
const isRoleSelectOpen = ref(false)
const isCityListOpen = ref(false)
+const showSpecialTimeRuleValidation = ref(false)
+
+type PushStrategyFieldKey
+ = | 'onePushDistance'
+ | 'onePushTime'
+ | 'twoPushDistance'
+ | 'twoPushTime'
+ | 'threePushDistance'
+ | 'threePushTime'
+ | 'fourPushDistance'
+ | 'fourPushTime'
+ | 'fivePushDistance'
+ | 'fivePushTime'
+
+interface PushStrategyRoundConfig {
+ label: string
+ distanceKey: Extract<PushStrategyFieldKey, `${string}Distance`>
+ timeKey: Extract<PushStrategyFieldKey, `${string}Time`>
+}
+
+interface SpecialTimeStrategyItem {
+ id?: string | number
+ tradingStrategyId?: string | number
+ localId: number
+ startTime: string | null
+ endTime: string | null
+ onePushDistance: number | null
+ onePushTime: number | null
+ twoPushDistance: number | null
+ twoPushTime: number | null
+ threePushDistance: number | null
+ threePushTime: number | null
+ fourPushDistance: number | null
+ fourPushTime: number | null
+ fivePushDistance: number | null
+ fivePushTime: number | null
+}
+
+const pushStrategyRounds: PushStrategyRoundConfig[] = [
+ { label: '第一轮推单', distanceKey: 'onePushDistance', timeKey: 'onePushTime' },
+ { label: '第二轮推单', distanceKey: 'twoPushDistance', timeKey: 'twoPushTime' },
+ { label: '第三轮推单', distanceKey: 'threePushDistance', timeKey: 'threePushTime' },
+ { label: '第四轮推单', distanceKey: 'fourPushDistance', timeKey: 'fourPushTime' },
+ { label: '第五轮推单', distanceKey: 'fivePushDistance', timeKey: 'fivePushTime' },
+]
+
+const specialTimeStrategyRoundGroups = [
+ pushStrategyRounds.slice(0, 2),
+ pushStrategyRounds.slice(2, 4),
+ pushStrategyRounds.slice(4, 5),
+]
+
+const specialTimeNumericKeys = pushStrategyRounds.flatMap(round => [round.distanceKey, round.timeKey]) as PushStrategyFieldKey[]
+
+let specialTimeStrategySeed = 0
+
+function createSpecialTimeStrategyItem(data: Partial<SpecialTimeStrategyItem> = {}): SpecialTimeStrategyItem {
+ specialTimeStrategySeed += 1
+ return {
+ id: data.id,
+ tradingStrategyId: data.tradingStrategyId,
+ localId: data.localId ?? specialTimeStrategySeed,
+ startTime: data.startTime ?? null,
+ endTime: data.endTime ?? null,
+ onePushDistance: data.onePushDistance ?? null,
+ onePushTime: data.onePushTime ?? null,
+ twoPushDistance: data.twoPushDistance ?? null,
+ twoPushTime: data.twoPushTime ?? null,
+ threePushDistance: data.threePushDistance ?? null,
+ threePushTime: data.threePushTime ?? null,
+ fourPushDistance: data.fourPushDistance ?? null,
+ fourPushTime: data.fourPushTime ?? null,
+ fivePushDistance: data.fivePushDistance ?? null,
+ fivePushTime: data.fivePushTime ?? null,
+ }
+}
+
+function normalizeSpecialTimeStrategyList(source: unknown): SpecialTimeStrategyItem[] {
+ let list: any[] = []
+ if (Array.isArray(source)) {
+ list = source
+ }
+ else if (typeof source === 'string') {
+ try {
+ const parsed = JSON.parse(source)
+ list = Array.isArray(parsed) ? parsed : []
+ }
+ catch {
+ list = []
+ }
+ }
+ return list.map(item => createSpecialTimeStrategyItem(item))
+}
+
+function timeStringToMinutes(time: string | null | undefined) {
+ if (!time)
+ return null
+ const [hourText, minuteText] = time.split(':')
+ const hour = Number(hourText)
+ const minute = Number(minuteText)
+ if (!Number.isInteger(hour) || !Number.isInteger(minute))
+ return null
+ return hour * 60 + minute
+}
+
+function isPositiveNumber(value: unknown) {
+ return typeof value === 'number' && Number.isFinite(value) && value > 0
+}
+
+function getSpecialTimeRuleErrorKey(rule: SpecialTimeStrategyItem, field: string) {
+ return `${rule.localId}-${field}`
+}
+
+function buildSpecialTimeRuleValidationMap(list: SpecialTimeStrategyItem[]) {
+ const errors: Record<string, string> = {}
+ const setError = (rule: SpecialTimeStrategyItem, field: string, text: string) => {
+ const key = getSpecialTimeRuleErrorKey(rule, field)
+ if (!errors[key])
+ errors[key] = text
+ }
+
+ // 先做单行校验,再做区间重叠校验,保证错误提示稳定。
+ list.forEach((rule) => {
+ if (!rule.startTime)
+ setError(rule, 'startTime', '请选择开始时间')
+ if (!rule.endTime)
+ setError(rule, 'endTime', '请选择结束时间')
+
+ const startMinutes = timeStringToMinutes(rule.startTime)
+ const endMinutes = timeStringToMinutes(rule.endTime)
+ if (startMinutes !== null && endMinutes !== null && endMinutes <= startMinutes)
+ setError(rule, 'endTime', '结束时间需大于开始时间')
+
+ specialTimeNumericKeys.forEach((field) => {
+ if (!isPositiveNumber(rule[field]))
+ setError(rule, field, '请输入有效的数值')
+ })
+ })
+
+ const validRules = list
+ .map(rule => ({
+ rule,
+ startMinutes: timeStringToMinutes(rule.startTime),
+ endMinutes: timeStringToMinutes(rule.endTime),
+ }))
+ .filter(item =>
+ item.startMinutes !== null
+ && item.endMinutes !== null
+ && item.endMinutes > item.startMinutes)
+
+ for (let i = 0; i < validRules.length; i += 1) {
+ for (let j = i + 1; j < validRules.length; j += 1) {
+ const current = validRules[i]
+ const next = validRules[j]
+ if (current.startMinutes! < next.endMinutes! && next.startMinutes! < current.endMinutes!) {
+ setError(current.rule, 'startTime', '时段规则存在重叠,请调整时间范围')
+ setError(current.rule, 'endTime', '时段规则存在重叠,请调整时间范围')
+ setError(next.rule, 'startTime', '时段规则存在重叠,请调整时间范围')
+ setError(next.rule, 'endTime', '时段规则存在重叠,请调整时间范围')
+ }
+ }
+ }
+
+ return errors
+}
+
+const specialTimeRuleValidationMap = computed(() => {
+ if (!showSpecialTimeRuleValidation.value)
+ return {}
+ return buildSpecialTimeRuleValidationMap(model.value.pushTimeList || [])
+})
+
+function getSpecialTimeRuleFieldError(rule: SpecialTimeStrategyItem, field: string) {
+ return specialTimeRuleValidationMap.value[getSpecialTimeRuleErrorKey(rule, field)] || ''
+}
+
+function getSpecialTimeRuleFeedback(rule: SpecialTimeStrategyItem, fields: string[]) {
+ return fields.map(field => getSpecialTimeRuleFieldError(rule, field)).find(Boolean) || ''
+}
+
+function getSpecialTimeRuleValidationStatus(rule: SpecialTimeStrategyItem, fields: string[]) {
+ return getSpecialTimeRuleFeedback(rule, fields) ? 'error' : undefined
+}
+
+function validateSpecialTimeRuleList() {
+ showSpecialTimeRuleValidation.value = true
+ const errors = Object.values(buildSpecialTimeRuleValidationMap(model.value.pushTimeList || []))
+ if (!errors.length)
+ return true
+
+ if (errors.includes('时段规则存在重叠,请调整时间范围')) {
+ message.error('时段规则存在重叠,请调整时间范围')
+ return false
+ }
+ if (errors.includes('结束时间需大于开始时间')) {
+ message.error('结束时间需大于开始时间')
+ return false
+ }
+ if (errors.includes('请选择开始时间') || errors.includes('请选择结束时间')) {
+ message.error('请完善特殊时段配置')
+ return false
+ }
+ message.error('请输入有效的数值')
+ return false
+}
+
+function handleAddSpecialTimeRule() {
+ model.value.pushTimeList.push(createSpecialTimeStrategyItem())
+}
+
+function isSpecialTimeRuleEffective(rule: SpecialTimeStrategyItem) {
+ const currentMinutes = dayjs().hour() * 60 + dayjs().minute()
+ const startMinutes = timeStringToMinutes(rule.startTime)
+ const endMinutes = timeStringToMinutes(rule.endTime)
+ if (startMinutes === null || endMinutes === null || endMinutes <= startMinutes)
+ return false
+ return currentMinutes >= startMinutes && currentMinutes < endMinutes
+}
+
+function handleDeleteSpecialTimeRule(rule: SpecialTimeStrategyItem) {
+ dialog.warning({
+ title: '删除确认',
+ content: isSpecialTimeRuleEffective(rule)
+ ? '该时段规则正在生效,删除后订单将按基础策略执行,是否确认删除?'
+ : '确认删除该组特殊时段规则?',
+ positiveText: '确认',
+ negativeText: '取消',
+ onPositiveClick: () => {
+ model.value.pushTimeList = model.value.pushTimeList.filter(
+ (item: SpecialTimeStrategyItem) => item.localId !== rule.localId,
+ )
+ },
+ })
+}
const options = computed(() => [
{
@@ -131,6 +367,7 @@
async function handleSubmit() {
try {
await formRef.value?.validate()
+ showSpecialTimeRuleValidation.value = true
if (!model.value.cancleCount) {
message.error('请输入每日取消订单数')
return
@@ -150,6 +387,8 @@
message.error('请输入播单策略')
return
}
+ if (!validateSpecialTimeRuleList())
+ return
if (!model.value.ruleType) {
message.error('请选择举手规则')
return
@@ -192,6 +431,10 @@
exemptionRole: model.value.exemptionRole.join(','),
exemptionEndTime: model.value.exemptionEndTime ? dayjs(model.value.exemptionEndTime).format('YYYY-MM-DD HH:mm:ss') : null,
exemptionStartTime: model.value.exemptionStartTime ? dayjs(model.value.exemptionStartTime).format('YYYY-MM-DD HH:mm:ss') : null,
+ pushTimeList: (model.value.pushTimeList || []).map((item: SpecialTimeStrategyItem) => {
+ const { localId, ...rest } = item
+ return rest
+ }),
}
delete params.tendencyPreference
delete params.tendencyOrderValueDims
@@ -216,8 +459,15 @@
function getDetail() {
getTradingStrategyDetail().then((res) => {
if (res.code === 200) {
- model.value = { ...res.data, exemptionRole: res.data.exemptionRole ? res.data.exemptionRole.split(',') || [] : [] }
+ const pushTimeList = normalizeSpecialTimeStrategyList(res.data.pushTimeList)
+ model.value = {
+ ...initialModel,
+ ...res.data,
+ exemptionRole: res.data.exemptionRole ? res.data.exemptionRole.split(',') || [] : [],
+ pushTimeList,
+ }
model.value.cityCode = res.data.cityCode ? res.data.cityCode.split(',') || [] : []
+ showSpecialTimeRuleValidation.value = false
// 解析分单倾向配置:从四个属性中找到值为 10 的那个,如果没有则默认为 tendencyOrderValue
if (model.value.tendencyOrderValue === 10) {
@@ -480,6 +730,90 @@
</n-form-item>
</NSpace>
+ <div class="special-time-strategy-section">
+ <NButton type="primary" class="w-240px!" secondary @click="handleAddSpecialTimeRule">
+ <template #icon>
+ <img src="@/assets/pricingManagement/add.svg" class="w-16px h-16px">
+ </template>
+ 添加特殊时段播单策略
+ </NButton>
+
+ <div v-if="model.pushTimeList.length > 0" class="special-time-rule-list">
+ <div
+ v-for="(rule) in model.pushTimeList"
+ :key="rule.localId"
+ class="special-time-rule-item"
+ >
+ <n-form-item
+ label="时段:" required label-style="color: rgba(23, 31, 38, 0.8);"
+ :validation-status="getSpecialTimeRuleValidationStatus(rule, ['startTime', 'endTime'])"
+ :feedback="getSpecialTimeRuleFeedback(rule, ['startTime', 'endTime'])"
+ :show-feedback="Boolean(getSpecialTimeRuleFeedback(rule, ['startTime', 'endTime']))"
+ >
+ <n-time-picker
+ v-model:formatted-value="rule.startTime"
+ format="HH:mm"
+ value-format="HH:mm"
+ clearable
+ close-on-select
+ :actions="['now']"
+ class="w-140px"
+ placeholder="开始时间"
+ />
+ <span class="mx-8px text-[#171F2660]">至</span>
+ <n-time-picker
+ v-model:formatted-value="rule.endTime"
+ format="HH:mm"
+ value-format="HH:mm"
+ clearable
+ close-on-select
+ :actions="['now']"
+ class="w-140px"
+ placeholder="结束时间"
+ />
+ <icon-park-outline-delete class="special-time-delete-btn" @click="handleDeleteSpecialTimeRule(rule)" />
+ </n-form-item>
+
+ <NSpace
+ v-for="(group, groupIndex) in specialTimeStrategyRoundGroups"
+ :key="`${rule.localId}-${groupIndex}`"
+ :size="[24, 16]"
+ >
+ <n-form-item
+ v-for="round in group"
+ :key="`${rule.localId}-${round.distanceKey}`"
+ :label="`${round.label}:`"
+ required
+ label-style="color: rgba(23, 31, 38, 0.8);"
+ :validation-status="getSpecialTimeRuleValidationStatus(rule, [round.distanceKey, round.timeKey])"
+ :feedback="getSpecialTimeRuleFeedback(rule, [round.distanceKey, round.timeKey])"
+ :show-feedback="Boolean(getSpecialTimeRuleFeedback(rule, [round.distanceKey, round.timeKey]))"
+ >
+ 推单距离为
+ <n-input-number
+ v-model:value="rule[round.distanceKey]"
+ min="0"
+ :show-button="false"
+ class="mx-2 w-102px"
+ placeholder="请输入"
+ :input-props="{ onKeydown: handleInputNumberKeyDown }"
+ />
+ 公里,推单时间为
+ <n-input-number
+ v-model:value="rule[round.timeKey]"
+ min="0"
+ :show-button="false"
+ class="mx-2 w-102px"
+ placeholder="请输入"
+ :input-props="{ onKeydown: handleInputNumberKeyDown }"
+ />
+ 秒
+ </n-form-item>
+ </NSpace>
+ </div>
+ </div>
+ </div>
+
<!-- 排队相关配置 -->
<n-form-item
label="排队时,每" path="queuePollingTime" label-style="color: rgba(23, 31, 38, 0.8);"
@@ -721,6 +1055,14 @@
box-sizing: border-box;
}
+.n-button--secondary {
+ width: 84px;
+ box-sizing: border-box;
+ color: #0068FF;
+ border: 1px solid #7ABDFF;
+ background-color: #E6F4FF;
+}
+
.box {
padding: 24px;
background-color: #fff;
@@ -771,4 +1113,39 @@
box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05);
padding: 16px 24px;
}
+
+.special-time-strategy-section {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.special-time-rule-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.special-time-rule-item {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid rgba(23, 31, 38, 0.08);
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+}
+
+.special-time-delete-btn {
+ margin-left: 12px;
+ color: rgba(23, 31, 38, 0.45);
+ cursor: pointer;
+ transition: color 0.2s ease;
+
+ &:hover {
+ color: #0068ff;
+ }
+}
</style>
--
Gitblit v1.7.1