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:
justin
2026-10-13 14:47:33 +08:00
parent b73db1b193
commit dcb008d381
12 changed files with 186 additions and 279 deletions
+69 -1
View File
@@ -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
View File
@@ -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` 本节补充。
+6 -2
View File
@@ -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',
+5 -2
View File
@@ -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: '执行时间',
+5 -2
View File
@@ -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: '執行時間',
-1
View File
@@ -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']
+1 -1
View File
@@ -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'
+5 -4
View File
@@ -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
}
+6 -7
View File
@@ -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 = ''
+17 -3
View File
@@ -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,
+61 -254
View File
@@ -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
View File
@@ -41,7 +41,7 @@ export default defineConfig(({ mode }) => {
},
server: {
host: true,
port: 5173,
port: 5177,
open: true,
allowedHosts: true,
proxy: {