Update environment configuration and enhance asset loading:

- Change development API proxy target to `https://192.168.3.233` and set base URL for recurring investment.
- Introduce `.env.production` for production environment with similar settings.
- Modify `loadExchangeAssets` to support pagination and scrolling for asset loading, improving user experience.
- Update related documentation and internationalization files for new features.
This commit is contained in:
justin
2026-10-10 16:24:01 +08:00
parent ffba1861be
commit 57b026d7d0
15 changed files with 286 additions and 60 deletions
+3 -2
View File
@@ -1,3 +1,4 @@
# 开发环境 API:留空则请求同源路径,由 vite proxy 转发到 VITE_API_PROXY_TARGET
VITE_API_BASE_URL=
VITE_API_PROXY_TARGET=http://192.168.3.230:18000
VITE_API_BASE_URL=/api
VITE_API_PROXY_TARGET=https://192.168.3.233
VITE_BASE_URL=/recurring-investment
+2
View File
@@ -0,0 +1,2 @@
VITE_API_BASE_URL=/api
VITE_BASE_URL=/recurring-investment
+103 -4
View File
@@ -52,7 +52,7 @@
### 1. 改动概览
- **背景与目标**:与 Order-H5 一致,支持 URL `token` 写入请求头;本地开发代理到 `192.168.3.230:18000`;通过 `getAllList` 加载服务端多语言。
- **背景与目标**:与 Order-H5 一致,支持 URL `token` 写入请求头;本地开发代理到 `192.168.3.233`;通过 `getAllList` 加载服务端多语言。
- **涉及模块**:`vite.config.ts`、`src/api/http.ts`、`src/stores/auth.ts`、`src/stores/serviceI18n.ts`、`src/api/base.ts`、`src/api/scheduleInvestment.ts`、`src/main.ts`、`src/App.vue`。
- **改动类型**:配置变更 + 基础设施。
@@ -60,7 +60,7 @@
| 位置 | 差异 |
|------|------|
| `vite.config.ts` | `/base`、`/trade` 代理至 `VITE_API_PROXY_TARGET`(默认 `http://192.168.3.230:18000`)。 |
| `vite.config.ts` | `/base`、`/trade` 代理至 `VITE_API_PROXY_TARGET`(默认 `https://192.168.3.233`)。 |
| `.env.development` | `VITE_API_BASE_URL` 置空;新增 `VITE_API_PROXY_TARGET`。 |
| `src/api/http.ts` | 请求头 `token`、`Accept-Language`;统一 `code === '0'` 成功处理。 |
| `src/utils/token.ts` / `stores/auth.ts` | URL 优先解析 token 并持久化(localStorage)。 |
@@ -89,7 +89,7 @@
| 配置项 | 含义 | Dev 默认 |
|--------|------|----------|
| `VITE_API_PROXY_TARGET` | Vite 代理目标主机 | `http://192.168.3.230:18000` |
| `VITE_API_PROXY_TARGET` | Vite 代理目标主机 | `https://192.168.3.233` |
| `VITE_API_BASE_URL` | Axios baseURL,空则同源 | 空 |
---
@@ -146,7 +146,7 @@
### 1. 改动概览
- **背景与目标**:创建表单在选定交易所后,从 `POST /product/product/getSimpleStockPage` 拉取可投标的,请求体 `exchangeCodes` 为交易所下拉选中值;支持 `searchName` 与 `isUpdatedToday: true`。
- **背景与目标**:创建表单在选定交易所后,从 `POST /product/product/getSimpleStockPage` 拉取可投标的,请求体 `exchangeCodes` 为交易所下拉选中值;支持 `searchName`。
- **涉及模块**:`src/api/product.ts`、`vite.config.ts`(`/product` 代理)、`RecurringInvestmentView.vue`、`AssetChipPicker.vue`、`InvestAsset` 类型、i18n。
- **改动类型**:功能新增。
@@ -738,3 +738,102 @@
### 5. 验证与测试
- `npm run build`。
---
## 补充:VITE_BASE_URL 接入 Vite base(2026-10-10)
### 1. 改动概览
- **背景与目标**:`.env.development` 已配置 `VITE_BASE_URL=/recurring-investment`,需写入 Vite `base`,使静态资源与 Vue Router 子路径部署一致。
- **涉及模块**:`vite.config.ts`、`src/env.d.ts`、`.env.production`、`README.md`。
- **改动类型**:配置变更。
### 2. 方法级改动分析
| 位置 | 差异 |
|------|------|
| `vite.config.ts` | `loadEnv` 读取 `VITE_BASE_URL`,经 `resolveViteBase` 设置 `base`(子路径末尾补 `/`)。 |
| `.env.production` | 新增,与开发环境一致的 `VITE_BASE_URL` / `VITE_API_BASE_URL`。 |
| `src/router/index.ts` | 无改;已使用 `createWebHistory(import.meta.env.BASE_URL)`。 |
### 3. 调用方与影响范围
- 开发与生产入口 URL 为 `/recurring-investment/`(非根路径 `/`)。
- 网关/Nginx 需将应用挂载到同子路径或做等价转发。
- **破坏性变更**:是(若此前按根路径 `/` 访问或部署,需改为带子路径)。
### 4. 风险与回滚
- **风险级别**:低。
- **回滚**:删除或清空 `VITE_BASE_URL`,移除 `vite.config.ts` 的 `base` 绑定。
- **回滚方式是否简单**:是。
### 5. 验证与测试
- `npm run build`;本地 `npm run dev` 访问 `http://localhost:5173/recurring-investment/`。
---
## 补充:仅创建计划请求体携带 clientId / accountId(2026-10-10)
### 1. 改动概览
- **背景与目标**:除「新建定投计划」外,其余交易/产品/基础接口不应在请求体中附带 URL 解析的 `clientId`、`accountId`。
- **涉及模块**:`src/api/http.ts`。
- **改动类型**:缺陷修复。
### 2. 方法级改动分析
| 位置 | 差异 |
|------|------|
| `src/api/http.ts` | 移除请求拦截器对 POST `data` 的全局合并 `clientId`/`accountId`。 |
| `src/utils/scheduleInvestmentPayload.ts` / `RecurringInvestmentView.vue` | 无改;创建计划仍通过 `buildScheduleInvestmentCreateBody` 显式写入两字段。 |
### 3. 调用方与影响范围
- 经 `http` 的列表、详情、终止、CT 选项、下次执行预览、`getSimpleStockPage`、`getAllList` 等不再自动附带账户上下文。
- **破坏性变更**:否(与后端契约对齐;创建接口行为不变)。
### 4. 风险与回滚
- **风险级别**:低。
- **回滚**:恢复 `http.ts` 拦截器中的合并逻辑。
- **回滚方式是否简单**:是。
### 5. 验证与测试
- `npm run build`;创建计划请求体仍含 `clientId`/`accountId`;其它 POST 仅含业务字段。
---
## 补充:资产下拉分页与滚动加载(2026-10-10)
### 1. 改动概览
- **背景与目标**:创建表单资产搜索不再单次请求 `size: 100000`;改为每页 50 条,下拉滚动触底追加加载。
- **涉及模块**:`RecurringInvestmentView.vue`、`AssetChipPicker.vue`、i18n。
- **改动类型**:性能优化 / 体验改进。
### 2. 方法级改动分析
| 位置 | 差异 |
|------|------|
| `RecurringInvestmentView.vue` | `loadExchangeAssets` 支持 `current` 递增与 `append`;维护 `assetHasMore` / `assetPage` / `assetSearchKeyword`;`onAssetLoadMore` 处理触底。 |
| `AssetChipPicker.vue` | `.asset-drop` 滚动监听,触发 `loadMore`;展示 `hasMore` / `loadingMore` 底部提示。 |
| i18n | 新增 `recurring.assetScrollHint`。 |
### 3. 调用方与影响范围
- 仅定期投资创建表单资产晶片;搜索防抖仍传 `searchName` 并重置为第一页。
- **破坏性变更**:否。
### 4. 风险与回滚
- **风险级别**:低;若后端 `current` 为 1 起始需与现用 0 起始对齐(与原实现一致)。
- **回滚**:恢复单次大 `size` 请求并移除 `loadMore` 链路。
- **回滚方式是否简单**:是。
### 5. 验证与测试
- `npm run build`;交易所切换、搜索、下拉滚动加载更多。
+3 -2
View File
@@ -14,7 +14,7 @@ npm install
npm run dev
```
默认开发地址:`http://localhost:5173/`。`vite.config.ts` 已设置 `server.host: true`,启动后终端会打印 **Network** 地址,同网段设备可用本机 IP 访问(如 `http://192.168.x.x:5173/`);经内网穿透或自定义域名访问时依赖 `allowedHosts: true`。
默认开发地址:`http://localhost:5173/recurring-investment/`(由 `.env.development` 的 `VITE_BASE_URL` 决定,对应 Vite `base` 与 Vue Router `history`)。`vite.config.ts` 已设置 `server.host: true`,启动后终端会打印 **Network** 地址,同网段设备可用本机 IP 访问(如 `http://192.168.x.x:5173/recurring-investment/`);经内网穿透或自定义域名访问时依赖 `allowedHosts: true`。
未带查询参数时,语言为繁体(`lang=tw`)、主题为深色(`theme=dark`)。
@@ -29,7 +29,8 @@ npm run preview # 预览生产构建
| 文件 | 变量 | 说明 |
|------|------|------|
| `.env.development` | `VITE_API_BASE_URL` | 接口基础路径,默认 `/api` |
| `.env.development` / `.env.production` | `VITE_API_BASE_URL` | 接口基础路径,默认 `/api` |
| 同上 | `VITE_BASE_URL` | 应用部署子路径,默认 `/recurring-investment`(写入 Vite `base`) |
## URL 查询参数
+24 -2
View File
@@ -32,7 +32,7 @@
## 补充:getSimpleStockPage 资产搜索
- 交易所下拉选中后请求标的;`exchangeCodes: [exchangeCode]`,`isUpdatedToday: true`。
- 交易所下拉选中后请求标的;`exchangeCodes: [exchangeCode]`。
- 晶片搜索框防抖调用同一接口并传 `searchName`。
- 映射字段含 `productKey`、`productCodeUnique` 等,便于后续创建计划入参。
@@ -54,7 +54,7 @@
## 补充:Token、代理与服务端多语言
- URL 参数 `token` 写入请求头 `token`(参考 Order-H5);路由跳转保留 query(含 token)。
- 开发代理:`/base/*` → `getAllList` 等;`/trade/*` → `schedule-investment/exchanges` 等(目标 `192.168.3.230:18000`,可改 `.env.development`)。
- 开发代理:`/base/*` → `getAllList` 等;`/trade/*` → `schedule-investment/exchanges` 等(目标 `192.168.3.233`,可改 `.env.development`)。
- 有 token 时启动加载 `getSystemI18nAllList`;组件内用 `useServiceI18n().si18n('I18N_ID')` 解析服务端文案。
- `getScheduleInvestmentExchanges()` 已封装,待业务页面对接。
- `npm run build` 通过。
@@ -233,3 +233,25 @@
- 新增 `src/utils/frequencyDay.ts`,将接口 `1`/`MONDAY`/`MON` 等 weekly 值与带前导零的 monthly 值规范后用于列表与详情展示。
- 创建/预览提交:`WEEKLY` 为 `1`–`7`,`MONTHLY` 为 `1`–`31`;`DAILY` 不传 `frequencyDay`。界面仍显示星期中文名。
- 验证:`npm run build`;参见 `IMPACT_ANALYSIS.md` 对应补充。
---
## 补充:VITE_BASE_URL 子路径(2026-10-10)
- `vite.config.ts` 将 `VITE_BASE_URL` 映射为 Vite `base`;新增 `.env.production`。
- 开发/预览入口:`/recurring-investment/`。
- 验证:`npm run build`。
---
## 补充:HTTP 不再全局注入 clientId / accountId(2026-10-10)
- 移除 `src/api/http.ts` 对全部 POST 请求体的自动合并;仅 `POST /trade/schedule-investment/plan/create` 在 `buildScheduleInvestmentCreateBody` 中携带 URL 的 `clientId`、`accountId`。
- 验证:`npm run build`;参见 `IMPACT_ANALYSIS.md` 对应补充。
---
## 补充:资产下拉滚动分页(2026-10-10)
- `getSimpleStockPage` 每页 50 条;下拉触底 `loadMore` 追加,搜索仍防抖并重置页码。
- 验证:`npm run build`;参见 `IMPACT_ANALYSIS.md` 对应补充。
-23
View File
@@ -1,7 +1,6 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { getToken, removeToken } from '@/utils/token'
function resolveBaseURL(): string {
@@ -32,28 +31,6 @@ http.interceptors.request.use(
} catch {
config.headers['Accept-Language'] = 'zh-TW'
}
try {
const authStore = useAuthStore()
const { clientId, accountId } = authStore
if (clientId || accountId) {
const base =
config.data !== undefined &&
typeof config.data === 'object' &&
config.data !== null &&
!(config.data instanceof FormData)
? { ...(config.data as Record<string, unknown>) }
: {}
if (clientId) {
base.clientId = clientId
}
if (accountId) {
base.accountId = accountId
}
config.data = base
}
} catch {
// pinia 未就绪时跳过
}
return config
},
(error) => Promise.reject(error),
+1 -1
View File
@@ -20,7 +20,7 @@ export interface SimpleStockPageRequest {
size: number
searchName: string
exchangeCodes: string[]
isUpdatedToday: boolean
}
export interface SimpleStockPageData {
+35 -1
View File
@@ -15,11 +15,14 @@ const props = defineProps<{
invalid?: boolean
disabled?: boolean
loading?: boolean
loadingMore?: boolean
hasMore?: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: InvestAsset[]]
search: [keyword: string]
loadMore: []
}>()
const { t, locale } = useI18n()
@@ -30,8 +33,22 @@ function assetDisplayName(asset: InvestAsset) {
const query = ref('')
const open = ref(false)
const wrapRef = ref<HTMLElement | null>(null)
const dropRef = ref<HTMLElement | null>(null)
const inputRef = ref<HTMLInputElement | null>(null)
const SCROLL_LOAD_THRESHOLD_PX = 48
function onDropScroll() {
const el = dropRef.value
if (!el || !props.hasMore || props.loading || props.loadingMore) {
return
}
const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight
if (distanceToBottom <= SCROLL_LOAD_THRESHOLD_PX) {
emit('loadMore')
}
}
const pickedCodes = computed(
() => new Set(props.modelValue.map((item) => investAssetOptionKey(item))),
)
@@ -159,7 +176,13 @@ onBeforeUnmount(() => {
@keydown="onKeydown"
/>
</div>
<div v-show="open" class="asset-drop" role="listbox">
<div
v-show="open"
ref="dropRef"
class="asset-drop"
role="listbox"
@scroll.passive="onDropScroll"
>
<button
v-for="asset in filtered"
:key="investAssetOptionKey(asset)"
@@ -173,6 +196,12 @@ onBeforeUnmount(() => {
</button>
<div v-if="loading" class="asset-none">{{ t('recurring.assetLoading') }}</div>
<div v-else-if="!filtered.length" class="asset-none">{{ t('recurring.assetEmpty') }}</div>
<div v-else-if="loadingMore" class="asset-none asset-none--footer">
{{ t('recurring.assetLoading') }}
</div>
<div v-else-if="hasMore" class="asset-none asset-none--footer">
{{ t('recurring.assetScrollHint') }}
</div>
</div>
</div>
</template>
@@ -346,5 +375,10 @@ onBeforeUnmount(() => {
color: var(--color-muted);
font-size: 12.5px;
text-align: center;
&--footer {
padding: 8px 12px;
font-size: 12px;
}
}
</style>
+1
View File
@@ -3,6 +3,7 @@
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_API_PROXY_TARGET?: string
readonly VITE_BASE_URL?: string
}
interface ImportMeta {
+1
View File
@@ -64,6 +64,7 @@ export default {
assetSupport: 'Supported: stocks, ETFs',
assetEmpty: 'No matching assets',
assetLoading: 'Loading…',
assetScrollHint: 'Scroll for more',
removeAsset: 'Remove {code}',
startDate: 'Start date',
amount: 'Amount',
+1
View File
@@ -64,6 +64,7 @@ export default {
assetSupport: '支持的: 股票、ETF',
assetEmpty: '没有匹配的资产',
assetLoading: '加载中…',
assetScrollHint: '继续滚动加载更多',
removeAsset: '移除 {code}',
startDate: '开始日期',
amount: '金额',
+1
View File
@@ -64,6 +64,7 @@ export default {
assetSupport: '支持的: 股票、ETF',
assetEmpty: '沒有匹配的資產',
assetLoading: '載入中…',
assetScrollHint: '繼續捲動載入更多',
removeAsset: '移除 {code}',
startDate: '開始日期',
amount: '金額',
-1
View File
@@ -20,7 +20,6 @@ declare module 'vue' {
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
+85 -8
View File
@@ -45,6 +45,7 @@ import {
import { formatHkd, parseAmountInput } from '@/utils/formatMoney'
import {
investAssetDisplayName,
investAssetOptionKey,
resolveAppLocale,
scheduleProductLineDisplay,
} from '@/utils/investAssetDisplay'
@@ -90,6 +91,12 @@ const exchangeOptions = ref<ScheduleInvestmentExchangeItem[]>([])
const exchangeLoading = ref(false)
const exchangeAssets = ref<InvestAsset[]>([])
const assetLoading = ref(false)
const assetLoadingMore = ref(false)
const assetHasMore = ref(false)
const assetSearchKeyword = ref('')
const assetPage = ref(0)
/** `getSimpleStockPage` 分页大小(滚动触底加载更多) */
const ASSET_PAGE_SIZE = 50
let assetSearchTimer: ReturnType<typeof setTimeout> | undefined
const picked = ref<InvestAsset[]>([])
const assetInvalid = ref(false)
@@ -351,21 +358,78 @@ function mapSimpleStockToAsset(item: SimpleStockItem): InvestAsset {
}
}
async function loadExchangeAssets(exchangeCode: string, searchName = '') {
assetLoading.value = true
function mergeExchangeAssets(existing: InvestAsset[], incoming: InvestAsset[]): InvestAsset[] {
const seen = new Set(existing.map((item) => investAssetOptionKey(item)))
const next = existing.slice()
for (const item of incoming) {
const key = investAssetOptionKey(item)
if (!seen.has(key)) {
seen.add(key)
next.push(item)
}
}
return next
}
function resolveAssetHasMore(
loadedCount: number,
pageRecordsLength: number,
total: number | undefined,
): boolean {
if (total != null && !Number.isNaN(total)) {
return loadedCount < total
}
return pageRecordsLength >= ASSET_PAGE_SIZE
}
async function loadExchangeAssets(
exchangeCode: string,
searchName = '',
options?: { append?: boolean },
) {
const append = options?.append ?? false
if (append) {
if (assetLoadingMore.value || assetLoading.value || !assetHasMore.value) {
return
}
assetLoadingMore.value = true
} else {
assetLoading.value = true
assetPage.value = 0
assetSearchKeyword.value = searchName
assetHasMore.value = false
}
const page = append ? assetPage.value + 1 : 0
const effectiveSearch = append ? assetSearchKeyword.value : searchName
try {
const res = await getSimpleStockPage({
current: 0,
size: 100000,
searchName,
current: page,
size: ASSET_PAGE_SIZE,
searchName: effectiveSearch,
exchangeCodes: [exchangeCode],
isUpdatedToday: true,
})
exchangeAssets.value = extractSimpleStockRecords(res.data).map(mapSimpleStockToAsset)
const raw = res.data
const records = extractSimpleStockRecords(raw).map(mapSimpleStockToAsset)
const total = raw && !Array.isArray(raw) && raw.total != null ? raw.total : undefined
if (append) {
exchangeAssets.value = mergeExchangeAssets(exchangeAssets.value, records)
assetPage.value = page
} else {
exchangeAssets.value = records
assetPage.value = 0
}
assetHasMore.value = resolveAssetHasMore(exchangeAssets.value.length, records.length, total)
} catch {
exchangeAssets.value = []
if (!append) {
exchangeAssets.value = []
}
assetHasMore.value = false
} finally {
assetLoading.value = false
assetLoadingMore.value = false
}
}
@@ -381,6 +445,13 @@ function onAssetSearch(keyword: string) {
}, 300)
}
function onAssetLoadMore() {
if (!form.exchangeCode) {
return
}
loadExchangeAssets(form.exchangeCode, assetSearchKeyword.value, { append: true })
}
watch(
() => form.exchangeCode,
(code) => {
@@ -388,6 +459,9 @@ watch(
assetInvalid.value = false
if (!code) {
exchangeAssets.value = []
assetHasMore.value = false
assetSearchKeyword.value = ''
assetPage.value = 0
return
}
loadExchangeAssets(code)
@@ -1157,7 +1231,10 @@ const previewOffset = computed(() =>
:invalid="assetInvalid"
:disabled="!form.exchangeCode"
:loading="assetLoading"
:loading-more="assetLoadingMore"
:has-more="assetHasMore"
@search="onAssetSearch"
@load-more="onAssetLoadMore"
/>
<div class="hint">{{ t('recurring.assetSupport') }}</div>
</div>
+26 -16
View File
@@ -5,14 +5,24 @@ import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { defineConfig, loadEnv } from 'vite'
const DEFAULT_PROXY_TARGET = 'http://192.168.3.230:18000'
const DEFAULT_PROXY_TARGET = 'https://192.168.3.233'
/** Vite `base`:与 `VITE_BASE_URL` 对齐,供 `import.meta.env.BASE_URL` 与 Vue Router 使用 */
function resolveViteBase(raw: string | undefined): string {
const trimmed = (raw ?? '/').trim()
if (!trimmed || trimmed === '/') return '/'
const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
return withLeading.endsWith('/') ? withLeading : `${withLeading}/`
}
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const proxyTarget = (env.VITE_API_PROXY_TARGET || DEFAULT_PROXY_TARGET).trim().replace(/\/+$/, '')
const base = resolveViteBase(env.VITE_BASE_URL)
return {
base,
plugins: [
vue(),
AutoImport({
@@ -35,21 +45,21 @@ export default defineConfig(({ mode }) => {
open: true,
allowedHosts: true,
proxy: {
'/base': {
target: proxyTarget,
changeOrigin: true,
secure: false,
},
'/trade': {
target: proxyTarget,
changeOrigin: true,
secure: false,
},
'/product': {
target: proxyTarget,
changeOrigin: true,
secure: false,
},
// '/base': {
// target: proxyTarget,
// changeOrigin: true,
// secure: false,
// },
// '/trade': {
// target: proxyTarget,
// changeOrigin: true,
// secure: false,
// },
// '/product': {
// target: proxyTarget,
// changeOrigin: true,
// secure: false,
// },
'/api': {
target: proxyTarget,
changeOrigin: true,