frequencyday的值约定

This commit is contained in:
justin
2026-10-10 14:16:32 +08:00
parent 833990b2b5
commit 6f6509f97d
7 changed files with 213 additions and 23 deletions
+32
View File
@@ -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`。
+8
View File
@@ -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` 对应补充。
-1
View File
@@ -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']
+145
View File
@@ -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
}
+5 -1
View File
@@ -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') {
+3 -1
View File
@@ -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,
+20 -20
View File
@@ -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