Refactor recurring investment form and API payloads:
- Simplify the creation form by removing price type and CT offset fields, defaulting to market orders. - Update `buildScheduleInvestmentCreateBody` and related functions to handle new payload structure with `ctOffsetMinutes` set to null. - Consolidate end type options into a dropdown, integrating 'GTC' as a selection. - Adjust internationalization files to reflect new labels and hints for the updated form. - Document changes in `IMPACT_ANALYSIS.md` and `TASK_SUMMARY.md` for clarity on the updates.
This commit is contained in:
+69
-1
@@ -239,7 +239,7 @@
|
||||
| 位置 | 差异 |
|
||||
|------|------|
|
||||
| 表单 | 新增 `investmentType`、`orderPriceType`、`frequencyType`、`frequencyDay`、`ctOffsetMinutes`;按金额/数量切换输入;周期仅 DAILY/WEEKLY/MONTHLY。 |
|
||||
| `buildScheduleInvestmentCreateBody` | 将表单映射为 `ScheduleInvestmentCreateBody`(含 `endType` DATE/COUNT/NONE、`totalExecutionCount`、`productCode`)。 |
|
||||
| `buildScheduleInvestmentCreateBody` | 将表单映射为 `ScheduleInvestmentCreateBody`(含 `endType` NO_END/END_DATE/COUNT、`totalExecutionCount`、`productCode`)。 |
|
||||
| `createScheduleInvestmentPlan` | `POST /trade/schedule-investment/plan/create`;确认建立时调用,成功后仍写入本地示例列表。 |
|
||||
|
||||
### 3. 调用方与影响范围
|
||||
@@ -1017,3 +1017,71 @@
|
||||
- 移动宽:执行记录为卡片,无横向挤压。
|
||||
- 平板 / 窄桌面:表格可横滑,见提示文案;首列固定。
|
||||
- 宽桌面:弹窗加宽后六列表格可读。
|
||||
|
||||
---
|
||||
|
||||
## 补充:创建表单隐藏价格类型与 CT 偏移(2026-10-13)
|
||||
|
||||
### 1. 改动概览
|
||||
|
||||
- **背景与目标**:创建计划固定市价单,不再展示「订单价格类型」「交易时间偏移」;创建与「预览下次执行」请求显式传 `ctOffsetMinutes: null`(待后端支持缺省/0 后可再调整)。
|
||||
- **涉及模块**:`RecurringInvestmentView.vue`、`scheduleInvestmentPayload.ts`、`scheduleInvestment.ts` 类型、三语 `recurring.*`。
|
||||
- **改动类型**:功能调整(表单简化)。
|
||||
|
||||
### 2. 方法级改动分析
|
||||
|
||||
| 位置 | 差异 |
|
||||
|------|------|
|
||||
| 创建表单 UI | 移除价格类型、限价、CT 偏移控件;投资金额/数量下增加 `orderPriceTypeMarketHint`。 |
|
||||
| `buildScheduleInvestmentCreateBody` | 固定 `ctOffsetMinutes: null`;`orderPriceType` 固定 `MARKET`。 |
|
||||
| `buildNextExecutionPreviewBody` | 固定 `ctOffsetMinutes: null`。 |
|
||||
| `ScheduleInvestmentCreateBody` | `ctOffsetMinutes: number \| null`,创建时传 `null`。 |
|
||||
| 确认弹窗 | 移除 CT 偏移预览行;仍展示订单价格类型(市价)。 |
|
||||
| `getScheduleInvestmentCtTimeOptions` | 创建表单不再调用(详情仍可读接口返回的偏移)。 |
|
||||
|
||||
### 3. 调用方与影响范围
|
||||
|
||||
- 仅创建/预览路径;计划详情、列表仍展示后端返回的 `ctOffsetMinutes`。
|
||||
- **破坏性变更**:否(对外接口字段减少可选传参,行为与产品约定一致)。
|
||||
|
||||
### 4. 风险与回滚
|
||||
|
||||
- **风险级别**:低;依赖后端对缺省 `ctOffsetMinutes` 的处理与文档一致。
|
||||
- **回滚方式是否简单**:是(恢复表单项与 payload 字段即可)。
|
||||
|
||||
### 5. 验证与测试
|
||||
|
||||
- `npm run build` 通过。
|
||||
- 创建表单:无价格类型/偏移字段,金额下有两行 hint;预览下次执行仅需交易所与周期。
|
||||
- 网络:create / next-execution 请求体 `ctOffsetMinutes` 为 `null`。
|
||||
|
||||
---
|
||||
|
||||
## 补充:结束类型 GTC 下拉项(2026-10-13)
|
||||
|
||||
- **背景**:将「无结束日期」勾选并入结束类型 `el-select`,文案为「GTC」。
|
||||
- **改动类型**:功能优化(UI/表单)。
|
||||
|
||||
### 2. 方法级改动分析
|
||||
|
||||
| 位置 | 差异 |
|
||||
|------|------|
|
||||
| `EndMode` | 新增 `'none'`。 |
|
||||
| `RecurringInvestmentView` | `endModeOptions` 含 date / none / count;移除 `noEnd` 表单项与勾选框;`date` 模式必填结束日期。 |
|
||||
| `resolveEndType` | `none`→`NO_END`、`date`→`END_DATE`、`count`→`COUNT`。 |
|
||||
| `scheduleInvestmentPlanMapper` | API `NO_END`(兼容 `NONE`)映射 `endMode: 'none'`。 |
|
||||
| i18n | `recurring.endByGtc`、`recurring.endGtcHint`。 |
|
||||
|
||||
### 3. 调用方与影响范围
|
||||
|
||||
- 创建计划、确认预览、列表/详情结束条件展示文案统一为 GTC 选项。
|
||||
- **破坏性变更**:是(创建请求 `endType` 由 `NONE`/`DATE` 改为 `NO_END`/`END_DATE`;列表仍兼容旧枚举读取)。
|
||||
|
||||
### 4. 风险与回滚
|
||||
|
||||
- **风险级别**:低。
|
||||
- **回滚方式是否简单**:是。
|
||||
|
||||
### 5. 验证与测试
|
||||
|
||||
- `npm run build`;表单三种结束类型切换与提交 payload `endType` 符合预期。
|
||||
|
||||
+10
-1
@@ -151,7 +151,7 @@
|
||||
|
||||
## 补充:结束类型合一控件
|
||||
|
||||
- 「特定日期 / 期數」与结束日期或期数输入合并为 `field-combo`;日期模式下保留「无结束日期」勾选。
|
||||
- 「特定日期 / GTC / 期数」与结束日期或期数输入合并为 `field-combo`;无结束日期改为下拉项 `endByGtc`(`endMode: none`),不再使用独立勾选框;表单标签为「结束类型」;接口 `endType` 为 `NO_END` / `END_DATE` / `COUNT`。
|
||||
|
||||
---
|
||||
|
||||
@@ -298,3 +298,12 @@
|
||||
|
||||
- 详情弹窗按断点加宽;移动端执行记录卡片列表,平板/桌面表格横滑 + 首列 sticky + 滑动提示。
|
||||
- 参见 `IMPACT_ANALYSIS.md` 对应补充。
|
||||
|
||||
---
|
||||
|
||||
## 补充:创建表单隐藏价格类型与 CT 偏移(2026-10-13)
|
||||
|
||||
- 创建表单不展示「订单价格类型」「交易时间偏移」,固定市价;在投资金额/数量 hint 下增加市价说明 `orderPriceTypeMarketHint`。
|
||||
- 创建计划与 `plan/next-execution` 请求体 `ctOffsetMinutes` 传 `null`;预览校验文案改为「请先选择交易所与定投周期」。
|
||||
- 确认弹窗去掉 CT 偏移行;计划详情仍展示后端返回的偏移。
|
||||
- 详见 `IMPACT_ANALYSIS.md` 本节补充。
|
||||
|
||||
@@ -85,6 +85,8 @@ export default {
|
||||
MARKET: 'Market',
|
||||
LIMIT: 'Limit',
|
||||
},
|
||||
orderPriceTypeMarketHint:
|
||||
'Recurring orders are placed as market orders after the open on each execution date; fills follow actual trades.',
|
||||
limitPrice: 'Limit price',
|
||||
limitPricePlaceholder: 'e.g. 350.5',
|
||||
limitPriceHint: 'Required for limit orders; must be greater than 0',
|
||||
@@ -117,7 +119,9 @@ export default {
|
||||
pleaseSelect: 'Please select',
|
||||
endModeLabel: 'End type',
|
||||
endByDate: 'Specific date',
|
||||
endByGtc: 'GTC',
|
||||
endByCount: 'Periods',
|
||||
endGtcHint: 'The plan keeps running until you terminate it',
|
||||
endDate: 'End date',
|
||||
noEndDate: 'No end date',
|
||||
endCount: 'Periods',
|
||||
@@ -126,7 +130,7 @@ export default {
|
||||
endCountValue: '{n} periods',
|
||||
notesTitle: 'Notes',
|
||||
notes: {
|
||||
n1: 'Recurring orders are placed as market orders at the open on each execution date; fills follow actual trades.',
|
||||
n1: 'Recurring orders are placed as market orders after the open on each execution date; fills follow actual trades.',
|
||||
n2: 'Fractional shares are not supported. Quantity is rounded down to whole shares, which may leave residual cash.',
|
||||
n3: 'If the execution date is a non-trading day, the order is deferred to the next trading day.',
|
||||
n4: 'If available funds are insufficient, that period’s order will not be executed; the plan continues.',
|
||||
@@ -135,7 +139,7 @@ export default {
|
||||
reset: 'Reset',
|
||||
previewNextExecution: 'Preview next run',
|
||||
previewNextExecutionTitle: 'Next execution preview',
|
||||
previewNextExecutionFill: 'Select exchange, frequency, and CT offset first',
|
||||
previewNextExecutionFill: 'Select exchange and frequency first',
|
||||
previewScheduledDate: 'Scheduled date',
|
||||
previewExecutionDate: 'Execution date',
|
||||
previewExecutionDateTime: 'Execution time',
|
||||
|
||||
@@ -85,6 +85,7 @@ export default {
|
||||
MARKET: '市价',
|
||||
LIMIT: '限价',
|
||||
},
|
||||
orderPriceTypeMarketHint: '定期投资指示于执行日开市后以市价单执行,成交价以实际成交为准。',
|
||||
limitPrice: '限价',
|
||||
limitPricePlaceholder: '例如 350.5',
|
||||
limitPriceHint: '限价单须填写大于 0 的价格',
|
||||
@@ -117,7 +118,9 @@ export default {
|
||||
pleaseSelect: '请选择',
|
||||
endModeLabel: '结束类型',
|
||||
endByDate: '特定日期',
|
||||
endByGtc: 'GTC',
|
||||
endByCount: '期数',
|
||||
endGtcHint: '计划将持续执行,直至您手动终止',
|
||||
endDate: '结束日期',
|
||||
noEndDate: '无结束日期',
|
||||
endCount: '期数',
|
||||
@@ -126,7 +129,7 @@ export default {
|
||||
endCountValue: '期数 {n} 期',
|
||||
notesTitle: '注意事项',
|
||||
notes: {
|
||||
n1: '定期投资指示于执行日开市时以市价单执行,成交价以实际成交为准。',
|
||||
n1: '定期投资指示于执行日开市后以市价单执行,成交价以实际成交为准。',
|
||||
n2: '本功能不支持不足一股的股份。定期投资的买入股数将向下取整至整股,因此可能产生碎股(零股)。',
|
||||
n3: '如执行日为非交易日,将顺延至下一个交易日执行。',
|
||||
n4: '账户可用资金不足时,该期指示将不获执行,计划仍会继续。',
|
||||
@@ -135,7 +138,7 @@ export default {
|
||||
reset: '重设',
|
||||
previewNextExecution: '预览下次执行',
|
||||
previewNextExecutionTitle: '下次执行预览',
|
||||
previewNextExecutionFill: '请先选择交易所、定投周期与时间偏移',
|
||||
previewNextExecutionFill: '请先选择交易所与定投周期',
|
||||
previewScheduledDate: '周期日期',
|
||||
previewExecutionDate: '执行日期',
|
||||
previewExecutionDateTime: '执行时间',
|
||||
|
||||
@@ -85,6 +85,7 @@ export default {
|
||||
MARKET: '市價',
|
||||
LIMIT: '限價',
|
||||
},
|
||||
orderPriceTypeMarketHint: '定期投資指示於執行日開市後以市價單執行,成交價以實際成交為準。',
|
||||
limitPrice: '限價',
|
||||
limitPricePlaceholder: '例如 350.5',
|
||||
limitPriceHint: '限價單須填寫大於 0 的價格',
|
||||
@@ -117,7 +118,9 @@ export default {
|
||||
pleaseSelect: '請選擇',
|
||||
endModeLabel: '結束類型',
|
||||
endByDate: '特定日期',
|
||||
endByGtc: 'GTC',
|
||||
endByCount: '期數',
|
||||
endGtcHint: '計劃將持續執行,直至您手動終止',
|
||||
endDate: '結束日期',
|
||||
noEndDate: '無結束日期',
|
||||
endCount: '期數',
|
||||
@@ -126,7 +129,7 @@ export default {
|
||||
endCountValue: '期數 {n} 期',
|
||||
notesTitle: '注意事項',
|
||||
notes: {
|
||||
n1: '定期投資指示於執行日開市時以市價單執行,成交價以實際成交為準。',
|
||||
n1: '定期投資指示於執行日開市後以市價單執行,成交價以實際成交為準。',
|
||||
n2: '本功能不支援不足一股的股份。定期投資的買入股數將向下取整至整股,因此可能產生碎股(零股)。',
|
||||
n3: '如執行日為非交易日,將順延至下一個交易日執行。',
|
||||
n4: '帳戶可用資金不足時,該期指示將不獲執行,計劃仍會繼續。',
|
||||
@@ -135,7 +138,7 @@ export default {
|
||||
reset: '重設',
|
||||
previewNextExecution: '預覽下次執行',
|
||||
previewNextExecutionTitle: '下次執行預覽',
|
||||
previewNextExecutionFill: '請先選擇交易所、定投週期與時間偏移',
|
||||
previewNextExecutionFill: '請先選擇交易所與定投週期',
|
||||
previewScheduledDate: '週期日期',
|
||||
previewExecutionDate: '執行日期',
|
||||
previewExecutionDateTime: '執行時間',
|
||||
|
||||
Vendored
-1
@@ -14,7 +14,6 @@ declare module 'vue' {
|
||||
AppPage: typeof import('./../components/layout/AppPage.vue')['default']
|
||||
AssetChipPicker: typeof import('./../components/recurring/AssetChipPicker.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
|
||||
@@ -9,7 +9,7 @@ export type IntervalKey =
|
||||
| 'semiannual'
|
||||
| 'annual'
|
||||
|
||||
export type EndMode = 'date' | 'count'
|
||||
export type EndMode = 'date' | 'count' | 'none'
|
||||
|
||||
export type PlanStatus = 'active' | 'created' | 'cancelled'
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export type OrderPriceType = 'MARKET' | 'LIMIT'
|
||||
|
||||
export type FrequencyType = 'DAILY' | 'WEEKLY' | 'MONTHLY'
|
||||
|
||||
export type ScheduleEndType = 'DATE' | 'COUNT' | 'NONE'
|
||||
export type ScheduleEndType = 'NO_END' | 'END_DATE' | 'COUNT'
|
||||
|
||||
export type WeekdayCode = 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT' | 'SUN'
|
||||
|
||||
@@ -117,8 +117,8 @@ export interface ScheduleInvestmentNextExecutionRequest {
|
||||
frequencyType: FrequencyType
|
||||
/** WEEKLY / MONTHLY 时必填 */
|
||||
frequencyDay?: string
|
||||
/** 空或 0 表示 CT 开市时刻;最大 240 */
|
||||
ctOffsetMinutes?: number
|
||||
/** null / 0 表示 CT 开市时刻;最大 240 */
|
||||
ctOffsetMinutes?: number | null
|
||||
/** 空表示交易所交易日 */
|
||||
startDate?: string
|
||||
}
|
||||
@@ -223,5 +223,6 @@ export interface ScheduleInvestmentCreateBody {
|
||||
endType: ScheduleEndType
|
||||
endDate?: string
|
||||
totalExecutionCount?: number
|
||||
ctOffsetMinutes: number
|
||||
/** null 表示 CT 开市默认(后端未支持省略时显式传 null) */
|
||||
ctOffsetMinutes: number | null
|
||||
}
|
||||
|
||||
@@ -20,11 +20,10 @@ export interface ScheduleFormPayloadInput {
|
||||
frequencyType: FrequencyType
|
||||
frequencyDay: string
|
||||
startDate: string
|
||||
endMode: 'date' | 'count'
|
||||
endMode: 'date' | 'count' | 'none'
|
||||
endDate: string
|
||||
noEnd: boolean
|
||||
endCount: number | null
|
||||
ctOffsetMinutes: number
|
||||
clientId: string | null
|
||||
accountId: string | null
|
||||
}
|
||||
@@ -33,10 +32,10 @@ export function resolveEndType(input: Pick<ScheduleFormPayloadInput, 'endMode' |
|
||||
if (input.endMode === 'count') {
|
||||
return 'COUNT'
|
||||
}
|
||||
if (input.noEnd || !input.endDate.trim()) {
|
||||
return 'NONE'
|
||||
if (input.endMode === 'none' || input.noEnd) {
|
||||
return 'NO_END'
|
||||
}
|
||||
return 'DATE'
|
||||
return 'END_DATE'
|
||||
}
|
||||
|
||||
export function buildScheduleInvestmentCreateBody(
|
||||
@@ -57,7 +56,7 @@ export function buildScheduleInvestmentCreateBody(
|
||||
frequencyType: input.frequencyType,
|
||||
startDate: input.startDate,
|
||||
endType,
|
||||
ctOffsetMinutes: input.ctOffsetMinutes,
|
||||
ctOffsetMinutes: null,
|
||||
}
|
||||
|
||||
if (input.investmentType === 'AMOUNT' && input.amountPerExecution != null) {
|
||||
@@ -78,7 +77,7 @@ export function buildScheduleInvestmentCreateBody(
|
||||
}
|
||||
}
|
||||
|
||||
if (endType === 'DATE') {
|
||||
if (endType === 'END_DATE') {
|
||||
body.endDate = input.endDate
|
||||
} else {
|
||||
body.endDate = ''
|
||||
|
||||
@@ -35,11 +35,25 @@ export function isPlanRunningStatus(status: string): boolean {
|
||||
return mapApiPlanStatus(status) !== 'cancelled'
|
||||
}
|
||||
|
||||
export function normalizeScheduleEndType(raw: string): ScheduleEndType | null {
|
||||
const normalized = raw.trim().toUpperCase()
|
||||
if (normalized === 'COUNT') {
|
||||
return 'COUNT'
|
||||
}
|
||||
if (normalized === 'END_DATE' || normalized === 'DATE') {
|
||||
return 'END_DATE'
|
||||
}
|
||||
if (normalized === 'NO_END' || normalized === 'NONE') {
|
||||
return 'NO_END'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveEndFields(record: ScheduleInvestmentPlanRecord): Pick<
|
||||
RecurringPlan,
|
||||
'endMode' | 'endDate' | 'endCount' | 'noEnd' | 'endDisplay'
|
||||
> {
|
||||
const endType = record.endType as ScheduleEndType
|
||||
const endType = normalizeScheduleEndType(record.endType)
|
||||
if (endType === 'COUNT' && record.totalExecutionCount != null) {
|
||||
return {
|
||||
endMode: 'count',
|
||||
@@ -49,9 +63,9 @@ function resolveEndFields(record: ScheduleInvestmentPlanRecord): Pick<
|
||||
endDisplay: '',
|
||||
}
|
||||
}
|
||||
if (endType === 'NONE' || !record.endDate) {
|
||||
if (endType === 'NO_END' || endType === null || !record.endDate) {
|
||||
return {
|
||||
endMode: 'date',
|
||||
endMode: 'none',
|
||||
endDate: '',
|
||||
endCount: null,
|
||||
noEnd: true,
|
||||
|
||||
@@ -6,7 +6,6 @@ import AppPage from '@/components/layout/AppPage.vue'
|
||||
import AssetChipPicker from '@/components/recurring/AssetChipPicker.vue'
|
||||
import {
|
||||
createScheduleInvestmentPlan,
|
||||
getScheduleInvestmentCtTimeOptions,
|
||||
getScheduleInvestmentExchanges,
|
||||
getScheduleInvestmentPlanDetail,
|
||||
getScheduleInvestmentPlanPage,
|
||||
@@ -32,11 +31,9 @@ import type {
|
||||
import {
|
||||
FREQUENCY_TYPE_KEYS,
|
||||
INVESTMENT_TYPE_KEYS,
|
||||
ORDER_PRICE_TYPE_KEYS,
|
||||
type FrequencyType,
|
||||
type InvestmentType,
|
||||
type OrderPriceType,
|
||||
type ScheduleInvestmentCtTimeOption,
|
||||
type ScheduleInvestmentExchangeItem,
|
||||
type ScheduleInvestmentNextExecutionData,
|
||||
type ScheduleInvestmentPlanDetailPlan,
|
||||
@@ -52,6 +49,7 @@ import {
|
||||
import {
|
||||
mapApiPlanStatus,
|
||||
mapSchedulePlanRecordToRecurringPlan,
|
||||
normalizeScheduleEndType,
|
||||
} from '@/utils/scheduleInvestmentPlanMapper'
|
||||
import {
|
||||
formatFrequencyDayLabel,
|
||||
@@ -74,10 +72,8 @@ interface FormState {
|
||||
limitPrice: string
|
||||
frequencyType: FrequencyType | ''
|
||||
frequencyDay: string
|
||||
ctOffsetMinutes: number
|
||||
endMode: EndMode
|
||||
endDate: string
|
||||
noEnd: boolean
|
||||
endCount: string
|
||||
}
|
||||
|
||||
@@ -122,8 +118,6 @@ const terminateSubmitting = ref(false)
|
||||
const nextExecutionPreviewVisible = ref(false)
|
||||
const nextExecutionPreviewLoading = ref(false)
|
||||
const nextExecutionPreview = ref<ScheduleInvestmentNextExecutionData | null>(null)
|
||||
const ctOffsetOptions = ref<ScheduleInvestmentCtTimeOption[]>([])
|
||||
const ctOffsetLoading = ref(false)
|
||||
|
||||
const PLAN_PAGE_SIZE = 50
|
||||
|
||||
@@ -137,10 +131,8 @@ const form = reactive<FormState>({
|
||||
limitPrice: '',
|
||||
frequencyType: '',
|
||||
frequencyDay: '',
|
||||
ctOffsetMinutes: 30,
|
||||
endMode: 'date',
|
||||
endMode: 'none',
|
||||
endDate: '',
|
||||
noEnd: false,
|
||||
endCount: '',
|
||||
})
|
||||
|
||||
@@ -149,12 +141,10 @@ const fieldError = reactive({
|
||||
startDate: false,
|
||||
amount: false,
|
||||
quantity: false,
|
||||
orderPriceType: false,
|
||||
limitPrice: false,
|
||||
frequencyType: false,
|
||||
frequencyDay: false,
|
||||
ctOffsetMinutes: false,
|
||||
endCount: false,
|
||||
endDate: false,
|
||||
})
|
||||
|
||||
const exchangeSelectOptions = computed(() =>
|
||||
@@ -173,13 +163,6 @@ const frequencyOptions = computed(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const orderPriceOptions = computed(() =>
|
||||
ORDER_PRICE_TYPE_KEYS.map((key) => ({
|
||||
value: key,
|
||||
label: t(`recurring.orderPriceType.${key}`),
|
||||
})),
|
||||
)
|
||||
|
||||
const investmentTypeOptions = computed(() =>
|
||||
INVESTMENT_TYPE_KEYS.map((key) => ({
|
||||
value: key,
|
||||
@@ -197,16 +180,29 @@ const investmentValueHint = computed(() =>
|
||||
|
||||
const endModeOptions = computed(() => [
|
||||
{ value: 'date' as EndMode, label: t('recurring.endByDate') },
|
||||
{ value: 'none' as EndMode, label: t('recurring.endByGtc') },
|
||||
{ value: 'count' as EndMode, label: t('recurring.endByCount') },
|
||||
])
|
||||
|
||||
const endValueFieldError = computed(() =>
|
||||
form.endMode === 'count' ? fieldError.endCount : false,
|
||||
)
|
||||
const endValueFieldError = computed(() => {
|
||||
if (form.endMode === 'count') {
|
||||
return fieldError.endCount
|
||||
}
|
||||
if (form.endMode === 'date') {
|
||||
return fieldError.endDate
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const endValueHint = computed(() =>
|
||||
form.endMode === 'count' ? t('recurring.endCountHint') : t('recurring.noEndDate'),
|
||||
)
|
||||
const endValueHint = computed(() => {
|
||||
if (form.endMode === 'count') {
|
||||
return t('recurring.endCountHint')
|
||||
}
|
||||
if (form.endMode === 'none') {
|
||||
return t('recurring.endGtcHint')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const weekdayOptions = computed(() =>
|
||||
WEEKLY_ISO_DAY_VALUES.map((iso) => {
|
||||
@@ -229,21 +225,6 @@ const showFrequencyDay = computed(
|
||||
() => form.frequencyType === 'WEEKLY' || form.frequencyType === 'MONTHLY',
|
||||
)
|
||||
|
||||
const ctOffsetSelectEnabled = computed(() => ctOffsetOptions.value.length > 0)
|
||||
|
||||
const ctOffsetHintText = computed(() =>
|
||||
ctOffsetSelectEnabled.value ? t('recurring.ctOffsetHint') : t('recurring.ctOffsetPickAssetHint'),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.noEnd,
|
||||
(checked) => {
|
||||
if (checked) {
|
||||
form.endDate = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.frequencyType,
|
||||
(type) => {
|
||||
@@ -266,20 +247,20 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.orderPriceType,
|
||||
(type) => {
|
||||
fieldError.limitPrice = false
|
||||
if (type !== 'LIMIT') {
|
||||
form.limitPrice = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.endMode,
|
||||
() => {
|
||||
(mode) => {
|
||||
fieldError.endCount = false
|
||||
fieldError.endDate = false
|
||||
if (mode === 'none') {
|
||||
form.endDate = ''
|
||||
}
|
||||
if (mode === 'count') {
|
||||
form.endDate = ''
|
||||
}
|
||||
if (mode === 'date') {
|
||||
form.endCount = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -289,65 +270,6 @@ watch(picked, () => {
|
||||
}
|
||||
})
|
||||
|
||||
function clearCtOffsetOptions() {
|
||||
ctOffsetOptions.value = []
|
||||
}
|
||||
|
||||
function syncCtOffsetAfterOptionsLoad() {
|
||||
if (!ctOffsetOptions.value.length) {
|
||||
return
|
||||
}
|
||||
const allowed = new Set(ctOffsetOptions.value.map((item) => item.offsetMinutes))
|
||||
if (!allowed.has(form.ctOffsetMinutes)) {
|
||||
form.ctOffsetMinutes = ctOffsetOptions.value[0].offsetMinutes
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCtOffsetOptions(exchangeCode: string, productCode: string) {
|
||||
ctOffsetLoading.value = true
|
||||
try {
|
||||
const res = await getScheduleInvestmentCtTimeOptions({ exchangeCode, productCode })
|
||||
ctOffsetOptions.value = Array.isArray(res.data?.options) ? res.data.options : []
|
||||
syncCtOffsetAfterOptionsLoad()
|
||||
} catch {
|
||||
ctOffsetOptions.value = []
|
||||
} finally {
|
||||
ctOffsetLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePickedProductCode(): string {
|
||||
const asset = picked.value[0]
|
||||
if (!asset) {
|
||||
return ''
|
||||
}
|
||||
return asset.productCode?.trim() ?? ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [form.exchangeCode, resolvePickedProductCode()] as const,
|
||||
([exchangeCode, productCode]) => {
|
||||
if (!exchangeCode || !productCode) {
|
||||
clearCtOffsetOptions()
|
||||
return
|
||||
}
|
||||
loadCtOffsetOptions(exchangeCode, productCode)
|
||||
},
|
||||
)
|
||||
|
||||
function isCtOffsetValid(offset: number): boolean {
|
||||
if (ctOffsetOptions.value.length) {
|
||||
return ctOffsetOptions.value.some((item) => item.offsetMinutes === offset)
|
||||
}
|
||||
return (
|
||||
offset !== null &&
|
||||
offset !== undefined &&
|
||||
!Number.isNaN(offset) &&
|
||||
offset >= 0 &&
|
||||
offset <= 240
|
||||
)
|
||||
}
|
||||
|
||||
function mapSimpleStockToAsset(item: SimpleStockItem): InvestAsset {
|
||||
const type: AssetType = item.productType === '1' ? 'stock' : 'etf'
|
||||
const engName = item.engName?.trim() ?? ''
|
||||
@@ -537,8 +459,8 @@ function endTextForPlan(plan: RecurringPlan) {
|
||||
if (plan.endMode === 'count' && plan.endCount) {
|
||||
return t('recurring.endCountValue', { n: plan.endCount })
|
||||
}
|
||||
if (plan.noEnd || !plan.endDate) {
|
||||
return t('recurring.noEndDate')
|
||||
if (plan.endMode === 'none' || plan.noEnd || !plan.endDate) {
|
||||
return t('recurring.endByGtc')
|
||||
}
|
||||
return plan.endDate
|
||||
}
|
||||
@@ -547,8 +469,8 @@ function buildEndDisplay() {
|
||||
if (form.endMode === 'count') {
|
||||
return t('recurring.endCountValue', { n: Number.parseInt(form.endCount, 10) })
|
||||
}
|
||||
if (form.noEnd || !form.endDate) {
|
||||
return t('recurring.noEndDate')
|
||||
if (form.endMode === 'none') {
|
||||
return t('recurring.endByGtc')
|
||||
}
|
||||
return form.endDate
|
||||
}
|
||||
@@ -559,12 +481,10 @@ function clearErrors() {
|
||||
fieldError.startDate = false
|
||||
fieldError.amount = false
|
||||
fieldError.quantity = false
|
||||
fieldError.orderPriceType = false
|
||||
fieldError.limitPrice = false
|
||||
fieldError.frequencyType = false
|
||||
fieldError.frequencyDay = false
|
||||
fieldError.ctOffsetMinutes = false
|
||||
fieldError.endCount = false
|
||||
fieldError.endDate = false
|
||||
}
|
||||
|
||||
function validate(): boolean {
|
||||
@@ -596,17 +516,6 @@ function validate(): boolean {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if (!form.orderPriceType) {
|
||||
fieldError.orderPriceType = true
|
||||
ok = false
|
||||
}
|
||||
if (form.orderPriceType === 'LIMIT') {
|
||||
const limit = parseAmountInput(form.limitPrice)
|
||||
if (!form.limitPrice.trim() || Number.isNaN(limit) || limit <= 0) {
|
||||
fieldError.limitPrice = true
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if (!form.frequencyType) {
|
||||
fieldError.frequencyType = true
|
||||
ok = false
|
||||
@@ -615,10 +524,6 @@ function validate(): boolean {
|
||||
fieldError.frequencyDay = true
|
||||
ok = false
|
||||
}
|
||||
if (!isCtOffsetValid(form.ctOffsetMinutes)) {
|
||||
fieldError.ctOffsetMinutes = true
|
||||
ok = false
|
||||
}
|
||||
if (form.endMode === 'count') {
|
||||
const count = Number.parseInt(form.endCount, 10)
|
||||
if (!form.endCount.trim() || Number.isNaN(count) || count < 1) {
|
||||
@@ -626,6 +531,10 @@ function validate(): boolean {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if (form.endMode === 'date' && !form.endDate) {
|
||||
fieldError.endDate = true
|
||||
ok = false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
@@ -634,7 +543,6 @@ function validateNextExecutionPreview(): boolean {
|
||||
fieldError.exchangeCode = false
|
||||
fieldError.frequencyType = false
|
||||
fieldError.frequencyDay = false
|
||||
fieldError.ctOffsetMinutes = false
|
||||
let ok = true
|
||||
|
||||
if (!form.exchangeCode) {
|
||||
@@ -649,11 +557,6 @@ function validateNextExecutionPreview(): boolean {
|
||||
fieldError.frequencyDay = true
|
||||
ok = false
|
||||
}
|
||||
if (!isCtOffsetValid(form.ctOffsetMinutes)) {
|
||||
fieldError.ctOffsetMinutes = true
|
||||
ok = false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -663,12 +566,12 @@ function buildNextExecutionPreviewBody() {
|
||||
exchangeCode: string
|
||||
frequencyType: FrequencyType
|
||||
frequencyDay?: string
|
||||
ctOffsetMinutes: number
|
||||
ctOffsetMinutes: null
|
||||
startDate?: string
|
||||
} = {
|
||||
exchangeCode: form.exchangeCode,
|
||||
frequencyType,
|
||||
ctOffsetMinutes: form.ctOffsetMinutes,
|
||||
ctOffsetMinutes: null,
|
||||
}
|
||||
const apiDay = frequencyDayForApi(frequencyType, form.frequencyDay)
|
||||
if (apiDay) {
|
||||
@@ -722,11 +625,8 @@ function resetForm() {
|
||||
form.limitPrice = ''
|
||||
form.frequencyType = ''
|
||||
form.frequencyDay = ''
|
||||
form.ctOffsetMinutes = 0
|
||||
clearCtOffsetOptions()
|
||||
form.endMode = 'date'
|
||||
form.endMode = 'none'
|
||||
form.endDate = ''
|
||||
form.noEnd = false
|
||||
form.endCount = ''
|
||||
clearErrors()
|
||||
}
|
||||
@@ -750,14 +650,11 @@ function onSubmit() {
|
||||
endMode: form.endMode,
|
||||
endDate: form.endDate,
|
||||
endCount: form.endMode === 'count' ? Number.parseInt(form.endCount, 10) : null,
|
||||
noEnd: form.endMode === 'date' && (form.noEnd || !form.endDate),
|
||||
noEnd: form.endMode === 'none',
|
||||
investmentType: form.investmentType,
|
||||
quantity,
|
||||
orderPriceType: form.orderPriceType,
|
||||
limitPrice:
|
||||
form.orderPriceType === 'LIMIT' ? parseAmountInput(form.limitPrice) : undefined,
|
||||
orderPriceType: 'MARKET',
|
||||
frequencyDay: showFrequencyDay.value ? form.frequencyDay : undefined,
|
||||
ctOffsetMinutes: form.ctOffsetMinutes,
|
||||
}
|
||||
confirmVisible.value = true
|
||||
}
|
||||
@@ -770,24 +667,21 @@ function buildCreateRequestBody(): ReturnType<typeof buildScheduleInvestmentCrea
|
||||
form.investmentType === 'AMOUNT' ? parseAmountInput(form.amount) : null
|
||||
const qtyPerExecution =
|
||||
form.investmentType === 'QUANTITY' ? parseAmountInput(form.quantity) : null
|
||||
const limitPrice =
|
||||
form.orderPriceType === 'LIMIT' ? parseAmountInput(form.limitPrice) : null
|
||||
return buildScheduleInvestmentCreateBody({
|
||||
exchangeCode: form.exchangeCode,
|
||||
asset: picked.value[0],
|
||||
investmentType: form.investmentType,
|
||||
amountPerExecution,
|
||||
qtyPerExecution,
|
||||
orderPriceType: form.orderPriceType,
|
||||
limitPrice,
|
||||
orderPriceType: 'MARKET',
|
||||
limitPrice: null,
|
||||
frequencyType: form.frequencyType,
|
||||
frequencyDay: form.frequencyDay,
|
||||
startDate: form.startDate,
|
||||
endMode: form.endMode,
|
||||
endDate: form.endDate,
|
||||
noEnd: form.noEnd,
|
||||
noEnd: form.endMode === 'none',
|
||||
endCount: form.endMode === 'count' ? Number.parseInt(form.endCount, 10) : null,
|
||||
ctOffsetMinutes: form.ctOffsetMinutes,
|
||||
clientId: authStore.clientId,
|
||||
accountId: authStore.accountId,
|
||||
})
|
||||
@@ -898,14 +792,15 @@ function formatDetailFrequency(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||
}
|
||||
|
||||
function formatDetailEndCondition(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||
if (plan.endType === 'COUNT') {
|
||||
const endType = normalizeScheduleEndType(plan.endType)
|
||||
if (endType === 'COUNT') {
|
||||
const total = plan.totalExecutionCount
|
||||
if (total != null && total !== '') {
|
||||
return t('recurring.endCountValue', { n: Number(total) })
|
||||
}
|
||||
}
|
||||
if (plan.endType === 'NONE' || !plan.endDate) {
|
||||
return t('recurring.noEndDate')
|
||||
if (endType === 'NO_END' || endType === null || !plan.endDate) {
|
||||
return t('recurring.endByGtc')
|
||||
}
|
||||
return plan.endDate
|
||||
}
|
||||
@@ -913,7 +808,7 @@ function formatDetailEndCondition(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||
function formatDetailProgress(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||
const done = Number(plan.executedCount ?? 0)
|
||||
const total = plan.totalExecutionCount
|
||||
if (plan.endType === 'COUNT' && total != null && total !== '') {
|
||||
if (normalizeScheduleEndType(plan.endType) === 'COUNT' && total != null && total !== '') {
|
||||
return t('recurring.planDetail.progressValue', { done, total: Number(total) })
|
||||
}
|
||||
return t('recurring.planDetail.progressOpen', { done })
|
||||
@@ -1077,11 +972,6 @@ const previewOrderPrice = computed(() => {
|
||||
}
|
||||
return typeLabel
|
||||
})
|
||||
const previewOffset = computed(() =>
|
||||
draft.value?.ctOffsetMinutes != null
|
||||
? t('recurring.ctOffsetPreview', { n: draft.value.ctOffsetMinutes })
|
||||
: '',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1307,45 +1197,7 @@ const previewOffset = computed(() =>
|
||||
/>
|
||||
</div>
|
||||
<div class="hint">{{ investmentValueHint }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="field" :class="{ 'field--err': fieldError.orderPriceType }">
|
||||
<label>
|
||||
{{ t('recurring.orderPriceTypeLabel') }}
|
||||
<span class="req">*</span>
|
||||
</label>
|
||||
<el-select
|
||||
v-model="form.orderPriceType"
|
||||
class="control-select"
|
||||
:placeholder="t('recurring.pleaseSelect')"
|
||||
@change="fieldError.orderPriceType = false"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in orderPriceOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col v-if="form.orderPriceType === 'LIMIT'" :xs="24" :md="12">
|
||||
<div class="field" :class="{ 'field--err': fieldError.limitPrice }">
|
||||
<label>
|
||||
{{ t('recurring.limitPrice') }}
|
||||
<span class="req">*</span>
|
||||
</label>
|
||||
<el-input
|
||||
v-model="form.limitPrice"
|
||||
class="control-input"
|
||||
inputmode="decimal"
|
||||
:placeholder="t('recurring.limitPricePlaceholder')"
|
||||
@input="fieldError.limitPrice = false"
|
||||
/>
|
||||
<div class="hint">{{ t('recurring.limitPriceHint') }}</div>
|
||||
<div class="hint">{{ t('recurring.orderPriceTypeMarketHint') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
@@ -1432,37 +1284,11 @@ const previewOffset = computed(() =>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="field" :class="{ 'field--err': fieldError.ctOffsetMinutes }">
|
||||
<label>
|
||||
{{ t('recurring.ctOffsetLabel') }}
|
||||
<span class="req">*</span>
|
||||
</label>
|
||||
<el-select
|
||||
v-model="form.ctOffsetMinutes"
|
||||
class="control-input"
|
||||
filterable
|
||||
:loading="ctOffsetLoading"
|
||||
:disabled="!ctOffsetSelectEnabled"
|
||||
:placeholder="t('recurring.ctOffsetLabel')"
|
||||
@change="fieldError.ctOffsetMinutes = false"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in ctOffsetOptions"
|
||||
:key="option.offsetMinutes"
|
||||
:label="option.displayName"
|
||||
:value="option.offsetMinutes"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="hint">{{ ctOffsetHintText }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="field" :class="{ 'field--err': endValueFieldError }">
|
||||
<label>
|
||||
{{ t('recurring.endPreview') }}
|
||||
<span v-if="form.endMode === 'count'" class="req">*</span>
|
||||
{{ t('recurring.endModeLabel') }}
|
||||
<span v-if="form.endMode === 'count' || form.endMode === 'date'" class="req">*</span>
|
||||
</label>
|
||||
<div class="field-combo">
|
||||
<el-select
|
||||
@@ -1482,13 +1308,12 @@ const previewOffset = computed(() =>
|
||||
v-model="form.endDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="form.noEnd"
|
||||
:placeholder="t('recurring.endDate')"
|
||||
class="field-combo__value field-combo__date"
|
||||
:class="{ 'is-dimmed': form.noEnd }"
|
||||
@change="fieldError.endDate = false"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-else-if="form.endMode === 'count'"
|
||||
v-model="form.endCount"
|
||||
class="field-combo__value"
|
||||
inputmode="numeric"
|
||||
@@ -1496,14 +1321,7 @@ const previewOffset = computed(() =>
|
||||
@input="fieldError.endCount = false"
|
||||
/>
|
||||
</div>
|
||||
<el-checkbox
|
||||
v-if="form.endMode === 'date'"
|
||||
v-model="form.noEnd"
|
||||
class="no-end-check"
|
||||
>
|
||||
{{ t('recurring.noEndDate') }}
|
||||
</el-checkbox>
|
||||
<div v-else class="hint">{{ endValueHint }}</div>
|
||||
<div v-if="endValueHint" class="hint">{{ endValueHint }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -1720,8 +1538,6 @@ const previewOffset = computed(() =>
|
||||
<dd>{{ previewOrderPrice }}</dd>
|
||||
<dt>{{ t('recurring.frequencyLabel') }}</dt>
|
||||
<dd>{{ previewInterval }}</dd>
|
||||
<dt>{{ t('recurring.ctOffsetLabel') }}</dt>
|
||||
<dd class="num">{{ previewOffset }}</dd>
|
||||
<dt>{{ t('recurring.endPreview') }}</dt>
|
||||
<dd>{{ draft.endDisplay }}</dd>
|
||||
</dl>
|
||||
@@ -2338,15 +2154,6 @@ const previewOffset = computed(() =>
|
||||
}
|
||||
}
|
||||
|
||||
.is-dimmed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.no-end-check {
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.end-toggle {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 5173,
|
||||
port: 5177,
|
||||
open: true,
|
||||
allowedHosts: true,
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user