| | |
| | | setFalse: endLoading, |
| | | } = useBoolean(false) |
| | | const message = useMessage() |
| | | const dialog = useDialog() |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | const tabStore = useTabStore() |
| | |
| | | fourPushTime: null, // 四轮推单时间 |
| | | fivePushDistance: null, // 五轮推单距离 |
| | | fivePushTime: null, // 五轮推单时间 |
| | | pushTimeList: [] as any[], // 特殊时段播单策略 |
| | | passwordFreeAmount: null, // 免密授权金额 |
| | | prepaidLimit: null, // 预付额度 订单大于金额需预付 |
| | | ruleType: 1, // 规则类型 |
| | |
| | | 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(() => [ |
| | | { |
| | |
| | | async function handleSubmit() { |
| | | try { |
| | | await formRef.value?.validate() |
| | | showSpecialTimeRuleValidation.value = true |
| | | if (!model.value.cancleCount) { |
| | | message.error('请输入每日取消订单数') |
| | | return |
| | |
| | | message.error('请输入播单策略') |
| | | return |
| | | } |
| | | if (!validateSpecialTimeRuleList()) |
| | | return |
| | | if (!model.value.ruleType) { |
| | | message.error('请选择举手规则') |
| | | return |
| | |
| | | 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 |
| | |
| | | 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) { |
| | |
| | | </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);" |
| | |
| | | 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; |
| | |
| | | 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> |