frequencyday的值约定
This commit is contained in:
@@ -706,3 +706,35 @@
|
||||
### 5. 验证与测试
|
||||
|
||||
- `npm run dev`:终端出现 Network URL;同网段可打开页面。
|
||||
|
||||
---
|
||||
|
||||
## 补充:frequencyDay 与后端约定对齐(2026-10-10)
|
||||
|
||||
### 1. 改动概览
|
||||
|
||||
- **背景与目标**:后端 `WEEKLY` 支持 `1`–`7`(ISO 周一=1)、英文全名/三字码;`MONTHLY` 支持 `1`–`31`(可带前导零);`DAILY` 不传 `frequencyDay`。前端提交已用 `MON`…`SUN` 与 `1`…`31`,需兼容接口回显。
|
||||
- **涉及模块**:`src/utils/frequencyDay.ts`(新增)、`scheduleInvestmentPlanMapper.ts`、`RecurringInvestmentView.vue`。
|
||||
- **改动类型**:缺陷修复 / 行为对齐。
|
||||
|
||||
### 2. 方法级改动分析
|
||||
|
||||
| 位置 | 差异 |
|
||||
|------|------|
|
||||
| `frequencyDay.ts` | `normalizeWeeklyDayToCode`、`normalizeMonthlyDay`、`formatFrequencyDayLabel`。 |
|
||||
| `scheduleInvestmentPlanMapper.ts` | 列表映射时将 `frequencyDay` 规范为表单/i18n 可用值。 |
|
||||
| `RecurringInvestmentView.vue` | 计划列表、详情频率、确认预览统一走 `formatFrequencyDayLabel`。 |
|
||||
|
||||
### 3. 调用方与影响范围
|
||||
|
||||
- 创建/预览请求体:`DAILY` 不传 `frequencyDay`;`WEEKLY`/`MONTHLY` 经 `frequencyDayForApi` 仅提交数字字符串(周 `1`–`7`,月 `1`–`31`)。
|
||||
- **破坏性变更**:否。
|
||||
|
||||
### 4. 风险与回滚
|
||||
|
||||
- **风险级别**:低。
|
||||
- **回滚方式是否简单**:是。
|
||||
|
||||
### 5. 验证与测试
|
||||
|
||||
- `npm run build`。
|
||||
|
||||
@@ -225,3 +225,11 @@
|
||||
- 创建表单在选定交易所与资产后请求 `POST /trade/schedule-investment/options/ct-time`(`productCode` 与创建计划一致:`productCodeUnique` 或 `code`)。
|
||||
- 偏移字段改为 `el-select`(`displayName` / `offsetMinutes`);未选资产时禁用并提示先选交易所与资产。
|
||||
- 详见 `IMPACT_ANALYSIS.md` 本节补充;验证:`npm run build`。
|
||||
|
||||
---
|
||||
|
||||
## 补充:frequencyDay 回显归一化(2026-10-10)
|
||||
|
||||
- 新增 `src/utils/frequencyDay.ts`,将接口 `1`/`MONDAY`/`MON` 等 weekly 值与带前导零的 monthly 值规范后用于列表与详情展示。
|
||||
- 创建/预览提交:`WEEKLY` 为 `1`–`7`,`MONTHLY` 为 `1`–`31`;`DAILY` 不传 `frequencyDay`。界面仍显示星期中文名。
|
||||
- 验证:`npm run build`;参见 `IMPACT_ANALYSIS.md` 对应补充。
|
||||
|
||||
Vendored
-1
@@ -20,7 +20,6 @@ declare module 'vue' {
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { FrequencyType, WeekdayCode } from '@/types/scheduleInvestment'
|
||||
import { WEEKDAY_CODES } from '@/types/scheduleInvestment'
|
||||
|
||||
/** ISO:1 = 周一 … 7 = 周日(与后端 resolveWeeklyDay 一致) */
|
||||
export const WEEKLY_ISO_DAY_VALUES = ['1', '2', '3', '4', '5', '6', '7'] as const
|
||||
|
||||
const ISO_WEEKDAY_TO_CODE: Record<number, WeekdayCode> = {
|
||||
1: 'MON',
|
||||
2: 'TUE',
|
||||
3: 'WED',
|
||||
4: 'THU',
|
||||
5: 'FRI',
|
||||
6: 'SAT',
|
||||
7: 'SUN',
|
||||
}
|
||||
|
||||
const FULL_WEEKDAY_TO_CODE: Record<string, WeekdayCode> = {
|
||||
MONDAY: 'MON',
|
||||
TUESDAY: 'TUE',
|
||||
WEDNESDAY: 'WED',
|
||||
THURSDAY: 'THU',
|
||||
FRIDAY: 'FRI',
|
||||
SATURDAY: 'SAT',
|
||||
SUNDAY: 'SUN',
|
||||
}
|
||||
|
||||
/**
|
||||
* 将接口返回的 weekly frequencyDay(1–7 / MONDAY / MON 等)规范为三字码,供表单与 i18n 使用。
|
||||
*/
|
||||
export function normalizeWeeklyDayToCode(raw: string | null | undefined): WeekdayCode | undefined {
|
||||
if (raw == null) {
|
||||
return undefined
|
||||
}
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const upper = trimmed.toUpperCase()
|
||||
if ((WEEKDAY_CODES as readonly string[]).includes(upper)) {
|
||||
return upper as WeekdayCode
|
||||
}
|
||||
|
||||
const fromFull = FULL_WEEKDAY_TO_CODE[upper]
|
||||
if (fromFull) {
|
||||
return fromFull
|
||||
}
|
||||
|
||||
const iso = Number.parseInt(trimmed, 10)
|
||||
if (!Number.isNaN(iso) && iso >= 1 && iso <= 7) {
|
||||
return ISO_WEEKDAY_TO_CODE[iso]
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 规范为 API/表单用的 weekly 数字字符串 `"1"`–`"7"`。 */
|
||||
export function isoWeekdayToCode(iso: string): WeekdayCode | undefined {
|
||||
const n = Number.parseInt(iso, 10)
|
||||
if (Number.isNaN(n) || n < 1 || n > 7) {
|
||||
return undefined
|
||||
}
|
||||
return ISO_WEEKDAY_TO_CODE[n]
|
||||
}
|
||||
|
||||
export function normalizeWeeklyDayToIso(raw: string | null | undefined): string | undefined {
|
||||
const code = normalizeWeeklyDayToCode(raw)
|
||||
if (!code) {
|
||||
return undefined
|
||||
}
|
||||
const iso = (Object.entries(ISO_WEEKDAY_TO_CODE) as [string, WeekdayCode][]).find(
|
||||
([, c]) => c === code,
|
||||
)?.[0]
|
||||
return iso
|
||||
}
|
||||
|
||||
/** 每月执行日:接口可带前导零(如 "09"),表单选项值为 "1"–"31"。 */
|
||||
export function normalizeMonthlyDay(raw: string | null | undefined): string | undefined {
|
||||
if (raw == null) {
|
||||
return undefined
|
||||
}
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) {
|
||||
return undefined
|
||||
}
|
||||
const day = Number.parseInt(trimmed, 10)
|
||||
if (Number.isNaN(day) || day < 1 || day > 31) {
|
||||
return trimmed
|
||||
}
|
||||
return String(day)
|
||||
}
|
||||
|
||||
export function normalizeFrequencyDay(
|
||||
frequencyType: FrequencyType,
|
||||
raw: string | null | undefined,
|
||||
): string | undefined {
|
||||
if (raw == null || raw === '') {
|
||||
return undefined
|
||||
}
|
||||
if (frequencyType === 'WEEKLY') {
|
||||
return normalizeWeeklyDayToIso(raw)
|
||||
}
|
||||
if (frequencyType === 'MONTHLY') {
|
||||
return normalizeMonthlyDay(raw)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 创建/预览请求体:WEEKLY、MONTHLY 仅传数字字符串。 */
|
||||
export function frequencyDayForApi(
|
||||
frequencyType: FrequencyType,
|
||||
raw: string | null | undefined,
|
||||
): string | undefined {
|
||||
if (raw == null || raw === '') {
|
||||
return undefined
|
||||
}
|
||||
if (frequencyType === 'WEEKLY') {
|
||||
return normalizeWeeklyDayToIso(raw)
|
||||
}
|
||||
if (frequencyType === 'MONTHLY') {
|
||||
return normalizeMonthlyDay(raw)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
type TranslateFn = (key: string, params?: Record<string, unknown>) => string
|
||||
|
||||
export function formatFrequencyDayLabel(
|
||||
frequencyType: FrequencyType,
|
||||
frequencyDay: string,
|
||||
t: TranslateFn,
|
||||
): string {
|
||||
if (frequencyType === 'WEEKLY') {
|
||||
const code = normalizeWeeklyDayToCode(frequencyDay)
|
||||
if (code) {
|
||||
return t(`recurring.weekday.${code}`)
|
||||
}
|
||||
return frequencyDay
|
||||
}
|
||||
if (frequencyType === 'MONTHLY') {
|
||||
const day = normalizeMonthlyDay(frequencyDay) ?? frequencyDay
|
||||
return t('recurring.monthDayOption', { day })
|
||||
}
|
||||
return frequencyDay
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ScheduleInvestmentCreateBody,
|
||||
} from '@/types/scheduleInvestment'
|
||||
import type { IntervalKey } from '@/types/recurringInvestment'
|
||||
import { frequencyDayForApi } from '@/utils/frequencyDay'
|
||||
|
||||
export interface ScheduleFormPayloadInput {
|
||||
exchangeCode: string
|
||||
@@ -71,7 +72,10 @@ export function buildScheduleInvestmentCreateBody(
|
||||
}
|
||||
|
||||
if (input.frequencyType === 'WEEKLY' || input.frequencyType === 'MONTHLY') {
|
||||
body.frequencyDay = input.frequencyDay
|
||||
const day = frequencyDayForApi(input.frequencyType, input.frequencyDay)
|
||||
if (day) {
|
||||
body.frequencyDay = day
|
||||
}
|
||||
}
|
||||
|
||||
if (endType === 'DATE') {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ScheduleEndType,
|
||||
ScheduleInvestmentPlanRecord,
|
||||
} from '@/types/scheduleInvestment'
|
||||
import { normalizeFrequencyDay } from '@/utils/frequencyDay'
|
||||
import { frequencyTypeToInterval } from '@/utils/scheduleInvestmentPayload'
|
||||
|
||||
function parseDecimal(value: string | null | undefined): number {
|
||||
@@ -90,6 +91,7 @@ export function mapSchedulePlanRecordToRecurringPlan(
|
||||
record.investmentType === 'QUANTITY' ? parseDecimal(record.qtyPerExecution) : null
|
||||
const endFields = resolveEndFields(record)
|
||||
const frequencyType = record.frequencyType as FrequencyType
|
||||
const frequencyDay = normalizeFrequencyDay(frequencyType, record.frequencyDay ?? undefined)
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
@@ -99,7 +101,7 @@ export function mapSchedulePlanRecordToRecurringPlan(
|
||||
quantity,
|
||||
investmentType: record.investmentType,
|
||||
frequencyType,
|
||||
frequencyDay: record.frequencyDay ?? undefined,
|
||||
frequencyDay,
|
||||
interval: frequencyTypeToInterval(frequencyType),
|
||||
startDate: record.startDate,
|
||||
...endFields,
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
FREQUENCY_TYPE_KEYS,
|
||||
INVESTMENT_TYPE_KEYS,
|
||||
ORDER_PRICE_TYPE_KEYS,
|
||||
WEEKDAY_CODES,
|
||||
type FrequencyType,
|
||||
type InvestmentType,
|
||||
type OrderPriceType,
|
||||
@@ -53,6 +52,12 @@ import {
|
||||
mapApiPlanStatus,
|
||||
mapSchedulePlanRecordToRecurringPlan,
|
||||
} from '@/utils/scheduleInvestmentPlanMapper'
|
||||
import {
|
||||
formatFrequencyDayLabel,
|
||||
frequencyDayForApi,
|
||||
isoWeekdayToCode,
|
||||
WEEKLY_ISO_DAY_VALUES,
|
||||
} from '@/utils/frequencyDay'
|
||||
import {
|
||||
buildScheduleInvestmentCreateBody,
|
||||
frequencyTypeToInterval,
|
||||
@@ -190,10 +195,13 @@ const endValueHint = computed(() =>
|
||||
)
|
||||
|
||||
const weekdayOptions = computed(() =>
|
||||
WEEKDAY_CODES.map((code) => ({
|
||||
value: code,
|
||||
label: t(`recurring.weekday.${code}`),
|
||||
})),
|
||||
WEEKLY_ISO_DAY_VALUES.map((iso) => {
|
||||
const code = isoWeekdayToCode(iso)
|
||||
return {
|
||||
value: iso,
|
||||
label: code ? t(`recurring.weekday.${code}`) : iso,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const monthDayOptions = computed(() =>
|
||||
@@ -228,7 +236,7 @@ watch(
|
||||
form.frequencyDay = ''
|
||||
fieldError.frequencyDay = false
|
||||
if (type === 'WEEKLY') {
|
||||
form.frequencyDay = 'MON'
|
||||
form.frequencyDay = '1'
|
||||
}
|
||||
if (type === 'MONTHLY') {
|
||||
form.frequencyDay = '1'
|
||||
@@ -434,10 +442,7 @@ function intervalTextForPlan(plan: RecurringPlan) {
|
||||
if (plan.frequencyType) {
|
||||
let text = t(`recurring.frequency.${plan.frequencyType}`)
|
||||
if (plan.frequencyDay) {
|
||||
const dayLabel =
|
||||
plan.frequencyType === 'WEEKLY'
|
||||
? t(`recurring.weekday.${plan.frequencyDay}`)
|
||||
: t('recurring.monthDayOption', { day: plan.frequencyDay })
|
||||
const dayLabel = formatFrequencyDayLabel(plan.frequencyType, plan.frequencyDay, t)
|
||||
text = `${text} · ${dayLabel}`
|
||||
}
|
||||
return text
|
||||
@@ -582,8 +587,9 @@ function buildNextExecutionPreviewBody() {
|
||||
frequencyType,
|
||||
ctOffsetMinutes: form.ctOffsetMinutes,
|
||||
}
|
||||
if (showFrequencyDay.value && form.frequencyDay) {
|
||||
body.frequencyDay = form.frequencyDay
|
||||
const apiDay = frequencyDayForApi(frequencyType, form.frequencyDay)
|
||||
if (apiDay) {
|
||||
body.frequencyDay = apiDay
|
||||
}
|
||||
if (form.startDate) {
|
||||
body.startDate = form.startDate
|
||||
@@ -802,10 +808,7 @@ function formatDetailOrderPrice(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||
function formatDetailFrequency(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||
let text = t(`recurring.frequency.${plan.frequencyType}`)
|
||||
if (plan.frequencyDay) {
|
||||
const dayLabel =
|
||||
plan.frequencyType === 'WEEKLY'
|
||||
? t(`recurring.weekday.${plan.frequencyDay}`)
|
||||
: t('recurring.monthDayOption', { day: plan.frequencyDay })
|
||||
const dayLabel = formatFrequencyDayLabel(plan.frequencyType, plan.frequencyDay, t)
|
||||
text = `${text} · ${dayLabel}`
|
||||
}
|
||||
return text
|
||||
@@ -955,10 +958,7 @@ const previewInterval = computed(() => {
|
||||
: 'MONTHLY'
|
||||
let text = t(`recurring.frequency.${freqKey}`)
|
||||
if (draft.value.frequencyDay) {
|
||||
const dayLabel =
|
||||
freqKey === 'WEEKLY'
|
||||
? t(`recurring.weekday.${draft.value.frequencyDay}`)
|
||||
: t('recurring.monthDayOption', { day: draft.value.frequencyDay })
|
||||
const dayLabel = formatFrequencyDayLabel(freqKey, draft.value.frequencyDay, t)
|
||||
text = `${text} · ${dayLabel}`
|
||||
}
|
||||
return text
|
||||
|
||||
Reference in New Issue
Block a user