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