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。 |
|
| 表单 | 新增 `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`;确认建立时调用,成功后仍写入本地示例列表。 |
|
| `createScheduleInvestmentPlan` | `POST /trade/schedule-investment/plan/create`;确认建立时调用,成功后仍写入本地示例列表。 |
|
||||||
|
|
||||||
### 3. 调用方与影响范围
|
### 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 + 滑动提示。
|
- 详情弹窗按断点加宽;移动端执行记录卡片列表,平板/桌面表格横滑 + 首列 sticky + 滑动提示。
|
||||||
- 参见 `IMPACT_ANALYSIS.md` 对应补充。
|
- 参见 `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',
|
MARKET: 'Market',
|
||||||
LIMIT: 'Limit',
|
LIMIT: 'Limit',
|
||||||
},
|
},
|
||||||
|
orderPriceTypeMarketHint:
|
||||||
|
'Recurring orders are placed as market orders after the open on each execution date; fills follow actual trades.',
|
||||||
limitPrice: 'Limit price',
|
limitPrice: 'Limit price',
|
||||||
limitPricePlaceholder: 'e.g. 350.5',
|
limitPricePlaceholder: 'e.g. 350.5',
|
||||||
limitPriceHint: 'Required for limit orders; must be greater than 0',
|
limitPriceHint: 'Required for limit orders; must be greater than 0',
|
||||||
@@ -117,7 +119,9 @@ export default {
|
|||||||
pleaseSelect: 'Please select',
|
pleaseSelect: 'Please select',
|
||||||
endModeLabel: 'End type',
|
endModeLabel: 'End type',
|
||||||
endByDate: 'Specific date',
|
endByDate: 'Specific date',
|
||||||
|
endByGtc: 'GTC',
|
||||||
endByCount: 'Periods',
|
endByCount: 'Periods',
|
||||||
|
endGtcHint: 'The plan keeps running until you terminate it',
|
||||||
endDate: 'End date',
|
endDate: 'End date',
|
||||||
noEndDate: 'No end date',
|
noEndDate: 'No end date',
|
||||||
endCount: 'Periods',
|
endCount: 'Periods',
|
||||||
@@ -126,7 +130,7 @@ export default {
|
|||||||
endCountValue: '{n} periods',
|
endCountValue: '{n} periods',
|
||||||
notesTitle: 'Notes',
|
notesTitle: 'Notes',
|
||||||
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.',
|
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.',
|
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.',
|
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',
|
reset: 'Reset',
|
||||||
previewNextExecution: 'Preview next run',
|
previewNextExecution: 'Preview next run',
|
||||||
previewNextExecutionTitle: 'Next execution preview',
|
previewNextExecutionTitle: 'Next execution preview',
|
||||||
previewNextExecutionFill: 'Select exchange, frequency, and CT offset first',
|
previewNextExecutionFill: 'Select exchange and frequency first',
|
||||||
previewScheduledDate: 'Scheduled date',
|
previewScheduledDate: 'Scheduled date',
|
||||||
previewExecutionDate: 'Execution date',
|
previewExecutionDate: 'Execution date',
|
||||||
previewExecutionDateTime: 'Execution time',
|
previewExecutionDateTime: 'Execution time',
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export default {
|
|||||||
MARKET: '市价',
|
MARKET: '市价',
|
||||||
LIMIT: '限价',
|
LIMIT: '限价',
|
||||||
},
|
},
|
||||||
|
orderPriceTypeMarketHint: '定期投资指示于执行日开市后以市价单执行,成交价以实际成交为准。',
|
||||||
limitPrice: '限价',
|
limitPrice: '限价',
|
||||||
limitPricePlaceholder: '例如 350.5',
|
limitPricePlaceholder: '例如 350.5',
|
||||||
limitPriceHint: '限价单须填写大于 0 的价格',
|
limitPriceHint: '限价单须填写大于 0 的价格',
|
||||||
@@ -117,7 +118,9 @@ export default {
|
|||||||
pleaseSelect: '请选择',
|
pleaseSelect: '请选择',
|
||||||
endModeLabel: '结束类型',
|
endModeLabel: '结束类型',
|
||||||
endByDate: '特定日期',
|
endByDate: '特定日期',
|
||||||
|
endByGtc: 'GTC',
|
||||||
endByCount: '期数',
|
endByCount: '期数',
|
||||||
|
endGtcHint: '计划将持续执行,直至您手动终止',
|
||||||
endDate: '结束日期',
|
endDate: '结束日期',
|
||||||
noEndDate: '无结束日期',
|
noEndDate: '无结束日期',
|
||||||
endCount: '期数',
|
endCount: '期数',
|
||||||
@@ -126,7 +129,7 @@ export default {
|
|||||||
endCountValue: '期数 {n} 期',
|
endCountValue: '期数 {n} 期',
|
||||||
notesTitle: '注意事项',
|
notesTitle: '注意事项',
|
||||||
notes: {
|
notes: {
|
||||||
n1: '定期投资指示于执行日开市时以市价单执行,成交价以实际成交为准。',
|
n1: '定期投资指示于执行日开市后以市价单执行,成交价以实际成交为准。',
|
||||||
n2: '本功能不支持不足一股的股份。定期投资的买入股数将向下取整至整股,因此可能产生碎股(零股)。',
|
n2: '本功能不支持不足一股的股份。定期投资的买入股数将向下取整至整股,因此可能产生碎股(零股)。',
|
||||||
n3: '如执行日为非交易日,将顺延至下一个交易日执行。',
|
n3: '如执行日为非交易日,将顺延至下一个交易日执行。',
|
||||||
n4: '账户可用资金不足时,该期指示将不获执行,计划仍会继续。',
|
n4: '账户可用资金不足时,该期指示将不获执行,计划仍会继续。',
|
||||||
@@ -135,7 +138,7 @@ export default {
|
|||||||
reset: '重设',
|
reset: '重设',
|
||||||
previewNextExecution: '预览下次执行',
|
previewNextExecution: '预览下次执行',
|
||||||
previewNextExecutionTitle: '下次执行预览',
|
previewNextExecutionTitle: '下次执行预览',
|
||||||
previewNextExecutionFill: '请先选择交易所、定投周期与时间偏移',
|
previewNextExecutionFill: '请先选择交易所与定投周期',
|
||||||
previewScheduledDate: '周期日期',
|
previewScheduledDate: '周期日期',
|
||||||
previewExecutionDate: '执行日期',
|
previewExecutionDate: '执行日期',
|
||||||
previewExecutionDateTime: '执行时间',
|
previewExecutionDateTime: '执行时间',
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export default {
|
|||||||
MARKET: '市價',
|
MARKET: '市價',
|
||||||
LIMIT: '限價',
|
LIMIT: '限價',
|
||||||
},
|
},
|
||||||
|
orderPriceTypeMarketHint: '定期投資指示於執行日開市後以市價單執行,成交價以實際成交為準。',
|
||||||
limitPrice: '限價',
|
limitPrice: '限價',
|
||||||
limitPricePlaceholder: '例如 350.5',
|
limitPricePlaceholder: '例如 350.5',
|
||||||
limitPriceHint: '限價單須填寫大於 0 的價格',
|
limitPriceHint: '限價單須填寫大於 0 的價格',
|
||||||
@@ -117,7 +118,9 @@ export default {
|
|||||||
pleaseSelect: '請選擇',
|
pleaseSelect: '請選擇',
|
||||||
endModeLabel: '結束類型',
|
endModeLabel: '結束類型',
|
||||||
endByDate: '特定日期',
|
endByDate: '特定日期',
|
||||||
|
endByGtc: 'GTC',
|
||||||
endByCount: '期數',
|
endByCount: '期數',
|
||||||
|
endGtcHint: '計劃將持續執行,直至您手動終止',
|
||||||
endDate: '結束日期',
|
endDate: '結束日期',
|
||||||
noEndDate: '無結束日期',
|
noEndDate: '無結束日期',
|
||||||
endCount: '期數',
|
endCount: '期數',
|
||||||
@@ -126,7 +129,7 @@ export default {
|
|||||||
endCountValue: '期數 {n} 期',
|
endCountValue: '期數 {n} 期',
|
||||||
notesTitle: '注意事項',
|
notesTitle: '注意事項',
|
||||||
notes: {
|
notes: {
|
||||||
n1: '定期投資指示於執行日開市時以市價單執行,成交價以實際成交為準。',
|
n1: '定期投資指示於執行日開市後以市價單執行,成交價以實際成交為準。',
|
||||||
n2: '本功能不支援不足一股的股份。定期投資的買入股數將向下取整至整股,因此可能產生碎股(零股)。',
|
n2: '本功能不支援不足一股的股份。定期投資的買入股數將向下取整至整股,因此可能產生碎股(零股)。',
|
||||||
n3: '如執行日為非交易日,將順延至下一個交易日執行。',
|
n3: '如執行日為非交易日,將順延至下一個交易日執行。',
|
||||||
n4: '帳戶可用資金不足時,該期指示將不獲執行,計劃仍會繼續。',
|
n4: '帳戶可用資金不足時,該期指示將不獲執行,計劃仍會繼續。',
|
||||||
@@ -135,7 +138,7 @@ export default {
|
|||||||
reset: '重設',
|
reset: '重設',
|
||||||
previewNextExecution: '預覽下次執行',
|
previewNextExecution: '預覽下次執行',
|
||||||
previewNextExecutionTitle: '下次執行預覽',
|
previewNextExecutionTitle: '下次執行預覽',
|
||||||
previewNextExecutionFill: '請先選擇交易所、定投週期與時間偏移',
|
previewNextExecutionFill: '請先選擇交易所與定投週期',
|
||||||
previewScheduledDate: '週期日期',
|
previewScheduledDate: '週期日期',
|
||||||
previewExecutionDate: '執行日期',
|
previewExecutionDate: '執行日期',
|
||||||
previewExecutionDateTime: '執行時間',
|
previewExecutionDateTime: '執行時間',
|
||||||
|
|||||||
Vendored
-1
@@ -14,7 +14,6 @@ declare module 'vue' {
|
|||||||
AppPage: typeof import('./../components/layout/AppPage.vue')['default']
|
AppPage: typeof import('./../components/layout/AppPage.vue')['default']
|
||||||
AssetChipPicker: typeof import('./../components/recurring/AssetChipPicker.vue')['default']
|
AssetChipPicker: typeof import('./../components/recurring/AssetChipPicker.vue')['default']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type IntervalKey =
|
|||||||
| 'semiannual'
|
| 'semiannual'
|
||||||
| 'annual'
|
| 'annual'
|
||||||
|
|
||||||
export type EndMode = 'date' | 'count'
|
export type EndMode = 'date' | 'count' | 'none'
|
||||||
|
|
||||||
export type PlanStatus = 'active' | 'created' | 'cancelled'
|
export type PlanStatus = 'active' | 'created' | 'cancelled'
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export type OrderPriceType = 'MARKET' | 'LIMIT'
|
|||||||
|
|
||||||
export type FrequencyType = 'DAILY' | 'WEEKLY' | 'MONTHLY'
|
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'
|
export type WeekdayCode = 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT' | 'SUN'
|
||||||
|
|
||||||
@@ -117,8 +117,8 @@ export interface ScheduleInvestmentNextExecutionRequest {
|
|||||||
frequencyType: FrequencyType
|
frequencyType: FrequencyType
|
||||||
/** WEEKLY / MONTHLY 时必填 */
|
/** WEEKLY / MONTHLY 时必填 */
|
||||||
frequencyDay?: string
|
frequencyDay?: string
|
||||||
/** 空或 0 表示 CT 开市时刻;最大 240 */
|
/** null / 0 表示 CT 开市时刻;最大 240 */
|
||||||
ctOffsetMinutes?: number
|
ctOffsetMinutes?: number | null
|
||||||
/** 空表示交易所交易日 */
|
/** 空表示交易所交易日 */
|
||||||
startDate?: string
|
startDate?: string
|
||||||
}
|
}
|
||||||
@@ -223,5 +223,6 @@ export interface ScheduleInvestmentCreateBody {
|
|||||||
endType: ScheduleEndType
|
endType: ScheduleEndType
|
||||||
endDate?: string
|
endDate?: string
|
||||||
totalExecutionCount?: number
|
totalExecutionCount?: number
|
||||||
ctOffsetMinutes: number
|
/** null 表示 CT 开市默认(后端未支持省略时显式传 null) */
|
||||||
|
ctOffsetMinutes: number | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ export interface ScheduleFormPayloadInput {
|
|||||||
frequencyType: FrequencyType
|
frequencyType: FrequencyType
|
||||||
frequencyDay: string
|
frequencyDay: string
|
||||||
startDate: string
|
startDate: string
|
||||||
endMode: 'date' | 'count'
|
endMode: 'date' | 'count' | 'none'
|
||||||
endDate: string
|
endDate: string
|
||||||
noEnd: boolean
|
noEnd: boolean
|
||||||
endCount: number | null
|
endCount: number | null
|
||||||
ctOffsetMinutes: number
|
|
||||||
clientId: string | null
|
clientId: string | null
|
||||||
accountId: string | null
|
accountId: string | null
|
||||||
}
|
}
|
||||||
@@ -33,10 +32,10 @@ export function resolveEndType(input: Pick<ScheduleFormPayloadInput, 'endMode' |
|
|||||||
if (input.endMode === 'count') {
|
if (input.endMode === 'count') {
|
||||||
return 'COUNT'
|
return 'COUNT'
|
||||||
}
|
}
|
||||||
if (input.noEnd || !input.endDate.trim()) {
|
if (input.endMode === 'none' || input.noEnd) {
|
||||||
return 'NONE'
|
return 'NO_END'
|
||||||
}
|
}
|
||||||
return 'DATE'
|
return 'END_DATE'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildScheduleInvestmentCreateBody(
|
export function buildScheduleInvestmentCreateBody(
|
||||||
@@ -57,7 +56,7 @@ export function buildScheduleInvestmentCreateBody(
|
|||||||
frequencyType: input.frequencyType,
|
frequencyType: input.frequencyType,
|
||||||
startDate: input.startDate,
|
startDate: input.startDate,
|
||||||
endType,
|
endType,
|
||||||
ctOffsetMinutes: input.ctOffsetMinutes,
|
ctOffsetMinutes: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.investmentType === 'AMOUNT' && input.amountPerExecution != 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
|
body.endDate = input.endDate
|
||||||
} else {
|
} else {
|
||||||
body.endDate = ''
|
body.endDate = ''
|
||||||
|
|||||||
@@ -35,11 +35,25 @@ export function isPlanRunningStatus(status: string): boolean {
|
|||||||
return mapApiPlanStatus(status) !== 'cancelled'
|
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<
|
function resolveEndFields(record: ScheduleInvestmentPlanRecord): Pick<
|
||||||
RecurringPlan,
|
RecurringPlan,
|
||||||
'endMode' | 'endDate' | 'endCount' | 'noEnd' | 'endDisplay'
|
'endMode' | 'endDate' | 'endCount' | 'noEnd' | 'endDisplay'
|
||||||
> {
|
> {
|
||||||
const endType = record.endType as ScheduleEndType
|
const endType = normalizeScheduleEndType(record.endType)
|
||||||
if (endType === 'COUNT' && record.totalExecutionCount != null) {
|
if (endType === 'COUNT' && record.totalExecutionCount != null) {
|
||||||
return {
|
return {
|
||||||
endMode: 'count',
|
endMode: 'count',
|
||||||
@@ -49,9 +63,9 @@ function resolveEndFields(record: ScheduleInvestmentPlanRecord): Pick<
|
|||||||
endDisplay: '',
|
endDisplay: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (endType === 'NONE' || !record.endDate) {
|
if (endType === 'NO_END' || endType === null || !record.endDate) {
|
||||||
return {
|
return {
|
||||||
endMode: 'date',
|
endMode: 'none',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
endCount: null,
|
endCount: null,
|
||||||
noEnd: true,
|
noEnd: true,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import AppPage from '@/components/layout/AppPage.vue'
|
|||||||
import AssetChipPicker from '@/components/recurring/AssetChipPicker.vue'
|
import AssetChipPicker from '@/components/recurring/AssetChipPicker.vue'
|
||||||
import {
|
import {
|
||||||
createScheduleInvestmentPlan,
|
createScheduleInvestmentPlan,
|
||||||
getScheduleInvestmentCtTimeOptions,
|
|
||||||
getScheduleInvestmentExchanges,
|
getScheduleInvestmentExchanges,
|
||||||
getScheduleInvestmentPlanDetail,
|
getScheduleInvestmentPlanDetail,
|
||||||
getScheduleInvestmentPlanPage,
|
getScheduleInvestmentPlanPage,
|
||||||
@@ -32,11 +31,9 @@ import type {
|
|||||||
import {
|
import {
|
||||||
FREQUENCY_TYPE_KEYS,
|
FREQUENCY_TYPE_KEYS,
|
||||||
INVESTMENT_TYPE_KEYS,
|
INVESTMENT_TYPE_KEYS,
|
||||||
ORDER_PRICE_TYPE_KEYS,
|
|
||||||
type FrequencyType,
|
type FrequencyType,
|
||||||
type InvestmentType,
|
type InvestmentType,
|
||||||
type OrderPriceType,
|
type OrderPriceType,
|
||||||
type ScheduleInvestmentCtTimeOption,
|
|
||||||
type ScheduleInvestmentExchangeItem,
|
type ScheduleInvestmentExchangeItem,
|
||||||
type ScheduleInvestmentNextExecutionData,
|
type ScheduleInvestmentNextExecutionData,
|
||||||
type ScheduleInvestmentPlanDetailPlan,
|
type ScheduleInvestmentPlanDetailPlan,
|
||||||
@@ -52,6 +49,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
mapApiPlanStatus,
|
mapApiPlanStatus,
|
||||||
mapSchedulePlanRecordToRecurringPlan,
|
mapSchedulePlanRecordToRecurringPlan,
|
||||||
|
normalizeScheduleEndType,
|
||||||
} from '@/utils/scheduleInvestmentPlanMapper'
|
} from '@/utils/scheduleInvestmentPlanMapper'
|
||||||
import {
|
import {
|
||||||
formatFrequencyDayLabel,
|
formatFrequencyDayLabel,
|
||||||
@@ -74,10 +72,8 @@ interface FormState {
|
|||||||
limitPrice: string
|
limitPrice: string
|
||||||
frequencyType: FrequencyType | ''
|
frequencyType: FrequencyType | ''
|
||||||
frequencyDay: string
|
frequencyDay: string
|
||||||
ctOffsetMinutes: number
|
|
||||||
endMode: EndMode
|
endMode: EndMode
|
||||||
endDate: string
|
endDate: string
|
||||||
noEnd: boolean
|
|
||||||
endCount: string
|
endCount: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +118,6 @@ const terminateSubmitting = ref(false)
|
|||||||
const nextExecutionPreviewVisible = ref(false)
|
const nextExecutionPreviewVisible = ref(false)
|
||||||
const nextExecutionPreviewLoading = ref(false)
|
const nextExecutionPreviewLoading = ref(false)
|
||||||
const nextExecutionPreview = ref<ScheduleInvestmentNextExecutionData | null>(null)
|
const nextExecutionPreview = ref<ScheduleInvestmentNextExecutionData | null>(null)
|
||||||
const ctOffsetOptions = ref<ScheduleInvestmentCtTimeOption[]>([])
|
|
||||||
const ctOffsetLoading = ref(false)
|
|
||||||
|
|
||||||
const PLAN_PAGE_SIZE = 50
|
const PLAN_PAGE_SIZE = 50
|
||||||
|
|
||||||
@@ -137,10 +131,8 @@ const form = reactive<FormState>({
|
|||||||
limitPrice: '',
|
limitPrice: '',
|
||||||
frequencyType: '',
|
frequencyType: '',
|
||||||
frequencyDay: '',
|
frequencyDay: '',
|
||||||
ctOffsetMinutes: 30,
|
endMode: 'none',
|
||||||
endMode: 'date',
|
|
||||||
endDate: '',
|
endDate: '',
|
||||||
noEnd: false,
|
|
||||||
endCount: '',
|
endCount: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -149,12 +141,10 @@ const fieldError = reactive({
|
|||||||
startDate: false,
|
startDate: false,
|
||||||
amount: false,
|
amount: false,
|
||||||
quantity: false,
|
quantity: false,
|
||||||
orderPriceType: false,
|
|
||||||
limitPrice: false,
|
|
||||||
frequencyType: false,
|
frequencyType: false,
|
||||||
frequencyDay: false,
|
frequencyDay: false,
|
||||||
ctOffsetMinutes: false,
|
|
||||||
endCount: false,
|
endCount: false,
|
||||||
|
endDate: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const exchangeSelectOptions = computed(() =>
|
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(() =>
|
const investmentTypeOptions = computed(() =>
|
||||||
INVESTMENT_TYPE_KEYS.map((key) => ({
|
INVESTMENT_TYPE_KEYS.map((key) => ({
|
||||||
value: key,
|
value: key,
|
||||||
@@ -197,16 +180,29 @@ const investmentValueHint = computed(() =>
|
|||||||
|
|
||||||
const endModeOptions = computed(() => [
|
const endModeOptions = computed(() => [
|
||||||
{ value: 'date' as EndMode, label: t('recurring.endByDate') },
|
{ value: 'date' as EndMode, label: t('recurring.endByDate') },
|
||||||
|
{ value: 'none' as EndMode, label: t('recurring.endByGtc') },
|
||||||
{ value: 'count' as EndMode, label: t('recurring.endByCount') },
|
{ value: 'count' as EndMode, label: t('recurring.endByCount') },
|
||||||
])
|
])
|
||||||
|
|
||||||
const endValueFieldError = computed(() =>
|
const endValueFieldError = computed(() => {
|
||||||
form.endMode === 'count' ? fieldError.endCount : false,
|
if (form.endMode === 'count') {
|
||||||
)
|
return fieldError.endCount
|
||||||
|
}
|
||||||
|
if (form.endMode === 'date') {
|
||||||
|
return fieldError.endDate
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
const endValueHint = computed(() =>
|
const endValueHint = computed(() => {
|
||||||
form.endMode === 'count' ? t('recurring.endCountHint') : t('recurring.noEndDate'),
|
if (form.endMode === 'count') {
|
||||||
)
|
return t('recurring.endCountHint')
|
||||||
|
}
|
||||||
|
if (form.endMode === 'none') {
|
||||||
|
return t('recurring.endGtcHint')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
const weekdayOptions = computed(() =>
|
const weekdayOptions = computed(() =>
|
||||||
WEEKLY_ISO_DAY_VALUES.map((iso) => {
|
WEEKLY_ISO_DAY_VALUES.map((iso) => {
|
||||||
@@ -229,21 +225,6 @@ const showFrequencyDay = computed(
|
|||||||
() => form.frequencyType === 'WEEKLY' || form.frequencyType === 'MONTHLY',
|
() => 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(
|
watch(
|
||||||
() => form.frequencyType,
|
() => form.frequencyType,
|
||||||
(type) => {
|
(type) => {
|
||||||
@@ -266,20 +247,20 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => form.orderPriceType,
|
|
||||||
(type) => {
|
|
||||||
fieldError.limitPrice = false
|
|
||||||
if (type !== 'LIMIT') {
|
|
||||||
form.limitPrice = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => form.endMode,
|
() => form.endMode,
|
||||||
() => {
|
(mode) => {
|
||||||
fieldError.endCount = false
|
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 {
|
function mapSimpleStockToAsset(item: SimpleStockItem): InvestAsset {
|
||||||
const type: AssetType = item.productType === '1' ? 'stock' : 'etf'
|
const type: AssetType = item.productType === '1' ? 'stock' : 'etf'
|
||||||
const engName = item.engName?.trim() ?? ''
|
const engName = item.engName?.trim() ?? ''
|
||||||
@@ -537,8 +459,8 @@ function endTextForPlan(plan: RecurringPlan) {
|
|||||||
if (plan.endMode === 'count' && plan.endCount) {
|
if (plan.endMode === 'count' && plan.endCount) {
|
||||||
return t('recurring.endCountValue', { n: plan.endCount })
|
return t('recurring.endCountValue', { n: plan.endCount })
|
||||||
}
|
}
|
||||||
if (plan.noEnd || !plan.endDate) {
|
if (plan.endMode === 'none' || plan.noEnd || !plan.endDate) {
|
||||||
return t('recurring.noEndDate')
|
return t('recurring.endByGtc')
|
||||||
}
|
}
|
||||||
return plan.endDate
|
return plan.endDate
|
||||||
}
|
}
|
||||||
@@ -547,8 +469,8 @@ function buildEndDisplay() {
|
|||||||
if (form.endMode === 'count') {
|
if (form.endMode === 'count') {
|
||||||
return t('recurring.endCountValue', { n: Number.parseInt(form.endCount, 10) })
|
return t('recurring.endCountValue', { n: Number.parseInt(form.endCount, 10) })
|
||||||
}
|
}
|
||||||
if (form.noEnd || !form.endDate) {
|
if (form.endMode === 'none') {
|
||||||
return t('recurring.noEndDate')
|
return t('recurring.endByGtc')
|
||||||
}
|
}
|
||||||
return form.endDate
|
return form.endDate
|
||||||
}
|
}
|
||||||
@@ -559,12 +481,10 @@ function clearErrors() {
|
|||||||
fieldError.startDate = false
|
fieldError.startDate = false
|
||||||
fieldError.amount = false
|
fieldError.amount = false
|
||||||
fieldError.quantity = false
|
fieldError.quantity = false
|
||||||
fieldError.orderPriceType = false
|
|
||||||
fieldError.limitPrice = false
|
|
||||||
fieldError.frequencyType = false
|
fieldError.frequencyType = false
|
||||||
fieldError.frequencyDay = false
|
fieldError.frequencyDay = false
|
||||||
fieldError.ctOffsetMinutes = false
|
|
||||||
fieldError.endCount = false
|
fieldError.endCount = false
|
||||||
|
fieldError.endDate = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(): boolean {
|
function validate(): boolean {
|
||||||
@@ -596,17 +516,6 @@ function validate(): boolean {
|
|||||||
ok = false
|
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) {
|
if (!form.frequencyType) {
|
||||||
fieldError.frequencyType = true
|
fieldError.frequencyType = true
|
||||||
ok = false
|
ok = false
|
||||||
@@ -615,10 +524,6 @@ function validate(): boolean {
|
|||||||
fieldError.frequencyDay = true
|
fieldError.frequencyDay = true
|
||||||
ok = false
|
ok = false
|
||||||
}
|
}
|
||||||
if (!isCtOffsetValid(form.ctOffsetMinutes)) {
|
|
||||||
fieldError.ctOffsetMinutes = true
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
if (form.endMode === 'count') {
|
if (form.endMode === 'count') {
|
||||||
const count = Number.parseInt(form.endCount, 10)
|
const count = Number.parseInt(form.endCount, 10)
|
||||||
if (!form.endCount.trim() || Number.isNaN(count) || count < 1) {
|
if (!form.endCount.trim() || Number.isNaN(count) || count < 1) {
|
||||||
@@ -626,6 +531,10 @@ function validate(): boolean {
|
|||||||
ok = false
|
ok = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (form.endMode === 'date' && !form.endDate) {
|
||||||
|
fieldError.endDate = true
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
@@ -634,7 +543,6 @@ function validateNextExecutionPreview(): boolean {
|
|||||||
fieldError.exchangeCode = false
|
fieldError.exchangeCode = false
|
||||||
fieldError.frequencyType = false
|
fieldError.frequencyType = false
|
||||||
fieldError.frequencyDay = false
|
fieldError.frequencyDay = false
|
||||||
fieldError.ctOffsetMinutes = false
|
|
||||||
let ok = true
|
let ok = true
|
||||||
|
|
||||||
if (!form.exchangeCode) {
|
if (!form.exchangeCode) {
|
||||||
@@ -649,11 +557,6 @@ function validateNextExecutionPreview(): boolean {
|
|||||||
fieldError.frequencyDay = true
|
fieldError.frequencyDay = true
|
||||||
ok = false
|
ok = false
|
||||||
}
|
}
|
||||||
if (!isCtOffsetValid(form.ctOffsetMinutes)) {
|
|
||||||
fieldError.ctOffsetMinutes = true
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,12 +566,12 @@ function buildNextExecutionPreviewBody() {
|
|||||||
exchangeCode: string
|
exchangeCode: string
|
||||||
frequencyType: FrequencyType
|
frequencyType: FrequencyType
|
||||||
frequencyDay?: string
|
frequencyDay?: string
|
||||||
ctOffsetMinutes: number
|
ctOffsetMinutes: null
|
||||||
startDate?: string
|
startDate?: string
|
||||||
} = {
|
} = {
|
||||||
exchangeCode: form.exchangeCode,
|
exchangeCode: form.exchangeCode,
|
||||||
frequencyType,
|
frequencyType,
|
||||||
ctOffsetMinutes: form.ctOffsetMinutes,
|
ctOffsetMinutes: null,
|
||||||
}
|
}
|
||||||
const apiDay = frequencyDayForApi(frequencyType, form.frequencyDay)
|
const apiDay = frequencyDayForApi(frequencyType, form.frequencyDay)
|
||||||
if (apiDay) {
|
if (apiDay) {
|
||||||
@@ -722,11 +625,8 @@ function resetForm() {
|
|||||||
form.limitPrice = ''
|
form.limitPrice = ''
|
||||||
form.frequencyType = ''
|
form.frequencyType = ''
|
||||||
form.frequencyDay = ''
|
form.frequencyDay = ''
|
||||||
form.ctOffsetMinutes = 0
|
form.endMode = 'none'
|
||||||
clearCtOffsetOptions()
|
|
||||||
form.endMode = 'date'
|
|
||||||
form.endDate = ''
|
form.endDate = ''
|
||||||
form.noEnd = false
|
|
||||||
form.endCount = ''
|
form.endCount = ''
|
||||||
clearErrors()
|
clearErrors()
|
||||||
}
|
}
|
||||||
@@ -750,14 +650,11 @@ function onSubmit() {
|
|||||||
endMode: form.endMode,
|
endMode: form.endMode,
|
||||||
endDate: form.endDate,
|
endDate: form.endDate,
|
||||||
endCount: form.endMode === 'count' ? Number.parseInt(form.endCount, 10) : null,
|
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,
|
investmentType: form.investmentType,
|
||||||
quantity,
|
quantity,
|
||||||
orderPriceType: form.orderPriceType,
|
orderPriceType: 'MARKET',
|
||||||
limitPrice:
|
|
||||||
form.orderPriceType === 'LIMIT' ? parseAmountInput(form.limitPrice) : undefined,
|
|
||||||
frequencyDay: showFrequencyDay.value ? form.frequencyDay : undefined,
|
frequencyDay: showFrequencyDay.value ? form.frequencyDay : undefined,
|
||||||
ctOffsetMinutes: form.ctOffsetMinutes,
|
|
||||||
}
|
}
|
||||||
confirmVisible.value = true
|
confirmVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -770,24 +667,21 @@ function buildCreateRequestBody(): ReturnType<typeof buildScheduleInvestmentCrea
|
|||||||
form.investmentType === 'AMOUNT' ? parseAmountInput(form.amount) : null
|
form.investmentType === 'AMOUNT' ? parseAmountInput(form.amount) : null
|
||||||
const qtyPerExecution =
|
const qtyPerExecution =
|
||||||
form.investmentType === 'QUANTITY' ? parseAmountInput(form.quantity) : null
|
form.investmentType === 'QUANTITY' ? parseAmountInput(form.quantity) : null
|
||||||
const limitPrice =
|
|
||||||
form.orderPriceType === 'LIMIT' ? parseAmountInput(form.limitPrice) : null
|
|
||||||
return buildScheduleInvestmentCreateBody({
|
return buildScheduleInvestmentCreateBody({
|
||||||
exchangeCode: form.exchangeCode,
|
exchangeCode: form.exchangeCode,
|
||||||
asset: picked.value[0],
|
asset: picked.value[0],
|
||||||
investmentType: form.investmentType,
|
investmentType: form.investmentType,
|
||||||
amountPerExecution,
|
amountPerExecution,
|
||||||
qtyPerExecution,
|
qtyPerExecution,
|
||||||
orderPriceType: form.orderPriceType,
|
orderPriceType: 'MARKET',
|
||||||
limitPrice,
|
limitPrice: null,
|
||||||
frequencyType: form.frequencyType,
|
frequencyType: form.frequencyType,
|
||||||
frequencyDay: form.frequencyDay,
|
frequencyDay: form.frequencyDay,
|
||||||
startDate: form.startDate,
|
startDate: form.startDate,
|
||||||
endMode: form.endMode,
|
endMode: form.endMode,
|
||||||
endDate: form.endDate,
|
endDate: form.endDate,
|
||||||
noEnd: form.noEnd,
|
noEnd: form.endMode === 'none',
|
||||||
endCount: form.endMode === 'count' ? Number.parseInt(form.endCount, 10) : null,
|
endCount: form.endMode === 'count' ? Number.parseInt(form.endCount, 10) : null,
|
||||||
ctOffsetMinutes: form.ctOffsetMinutes,
|
|
||||||
clientId: authStore.clientId,
|
clientId: authStore.clientId,
|
||||||
accountId: authStore.accountId,
|
accountId: authStore.accountId,
|
||||||
})
|
})
|
||||||
@@ -898,14 +792,15 @@ function formatDetailFrequency(plan: ScheduleInvestmentPlanDetailPlan) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDetailEndCondition(plan: ScheduleInvestmentPlanDetailPlan) {
|
function formatDetailEndCondition(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||||
if (plan.endType === 'COUNT') {
|
const endType = normalizeScheduleEndType(plan.endType)
|
||||||
|
if (endType === 'COUNT') {
|
||||||
const total = plan.totalExecutionCount
|
const total = plan.totalExecutionCount
|
||||||
if (total != null && total !== '') {
|
if (total != null && total !== '') {
|
||||||
return t('recurring.endCountValue', { n: Number(total) })
|
return t('recurring.endCountValue', { n: Number(total) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (plan.endType === 'NONE' || !plan.endDate) {
|
if (endType === 'NO_END' || endType === null || !plan.endDate) {
|
||||||
return t('recurring.noEndDate')
|
return t('recurring.endByGtc')
|
||||||
}
|
}
|
||||||
return plan.endDate
|
return plan.endDate
|
||||||
}
|
}
|
||||||
@@ -913,7 +808,7 @@ function formatDetailEndCondition(plan: ScheduleInvestmentPlanDetailPlan) {
|
|||||||
function formatDetailProgress(plan: ScheduleInvestmentPlanDetailPlan) {
|
function formatDetailProgress(plan: ScheduleInvestmentPlanDetailPlan) {
|
||||||
const done = Number(plan.executedCount ?? 0)
|
const done = Number(plan.executedCount ?? 0)
|
||||||
const total = plan.totalExecutionCount
|
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.progressValue', { done, total: Number(total) })
|
||||||
}
|
}
|
||||||
return t('recurring.planDetail.progressOpen', { done })
|
return t('recurring.planDetail.progressOpen', { done })
|
||||||
@@ -1077,11 +972,6 @@ const previewOrderPrice = computed(() => {
|
|||||||
}
|
}
|
||||||
return typeLabel
|
return typeLabel
|
||||||
})
|
})
|
||||||
const previewOffset = computed(() =>
|
|
||||||
draft.value?.ctOffsetMinutes != null
|
|
||||||
? t('recurring.ctOffsetPreview', { n: draft.value.ctOffsetMinutes })
|
|
||||||
: '',
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1307,45 +1197,7 @@ const previewOffset = computed(() =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint">{{ investmentValueHint }}</div>
|
<div class="hint">{{ investmentValueHint }}</div>
|
||||||
</div>
|
<div class="hint">{{ t('recurring.orderPriceTypeMarketHint') }}</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>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
@@ -1432,37 +1284,11 @@ const previewOffset = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</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">
|
<el-col :xs="24" :md="12">
|
||||||
<div class="field" :class="{ 'field--err': endValueFieldError }">
|
<div class="field" :class="{ 'field--err': endValueFieldError }">
|
||||||
<label>
|
<label>
|
||||||
{{ t('recurring.endPreview') }}
|
{{ t('recurring.endModeLabel') }}
|
||||||
<span v-if="form.endMode === 'count'" class="req">*</span>
|
<span v-if="form.endMode === 'count' || form.endMode === 'date'" class="req">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="field-combo">
|
<div class="field-combo">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -1482,13 +1308,12 @@ const previewOffset = computed(() =>
|
|||||||
v-model="form.endDate"
|
v-model="form.endDate"
|
||||||
type="date"
|
type="date"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
:disabled="form.noEnd"
|
|
||||||
:placeholder="t('recurring.endDate')"
|
:placeholder="t('recurring.endDate')"
|
||||||
class="field-combo__value field-combo__date"
|
class="field-combo__value field-combo__date"
|
||||||
:class="{ 'is-dimmed': form.noEnd }"
|
@change="fieldError.endDate = false"
|
||||||
/>
|
/>
|
||||||
<el-input
|
<el-input
|
||||||
v-else
|
v-else-if="form.endMode === 'count'"
|
||||||
v-model="form.endCount"
|
v-model="form.endCount"
|
||||||
class="field-combo__value"
|
class="field-combo__value"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
@@ -1496,14 +1321,7 @@ const previewOffset = computed(() =>
|
|||||||
@input="fieldError.endCount = false"
|
@input="fieldError.endCount = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<el-checkbox
|
<div v-if="endValueHint" class="hint">{{ endValueHint }}</div>
|
||||||
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>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -1720,8 +1538,6 @@ const previewOffset = computed(() =>
|
|||||||
<dd>{{ previewOrderPrice }}</dd>
|
<dd>{{ previewOrderPrice }}</dd>
|
||||||
<dt>{{ t('recurring.frequencyLabel') }}</dt>
|
<dt>{{ t('recurring.frequencyLabel') }}</dt>
|
||||||
<dd>{{ previewInterval }}</dd>
|
<dd>{{ previewInterval }}</dd>
|
||||||
<dt>{{ t('recurring.ctOffsetLabel') }}</dt>
|
|
||||||
<dd class="num">{{ previewOffset }}</dd>
|
|
||||||
<dt>{{ t('recurring.endPreview') }}</dt>
|
<dt>{{ t('recurring.endPreview') }}</dt>
|
||||||
<dd>{{ draft.endDisplay }}</dd>
|
<dd>{{ draft.endDisplay }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -2338,15 +2154,6 @@ const previewOffset = computed(() =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-dimmed {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-end-check {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.end-toggle {
|
.end-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
port: 5173,
|
port: 5177,
|
||||||
open: true,
|
open: true,
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user