minitest2.0 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/foreign-position.ts +114 -41
- package/src/utils/foreign-forecast.ts +65 -15
- package/src/views/foreign/position/create/index.vue +1 -1
- package/src/views/foreign/position/detail/index.vue +285 -161
- package/src/views/foreign/position/index.vue +45 -26
- package//345/274/200/345/217/221/350/247/204/350/214/203.md +367 -0
|
@@ -49,7 +49,7 @@ type PendingForecast = {
|
|
|
49
49
|
amount: number
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
type SelectorType = 'currency' | 'business' | 'status'
|
|
52
|
+
type SelectorType = 'currency' | 'business' | 'status' | 'filterFTU'
|
|
53
53
|
|
|
54
54
|
type SelectorOption = ForeignOption | ForeignStatusOption
|
|
55
55
|
|
|
@@ -61,12 +61,18 @@ const heroImage = `${import.meta.env.BASE_URL}images/12a73787-86a9-4891-a65f-661
|
|
|
61
61
|
const pageSize = 10
|
|
62
62
|
const backTopThreshold = 520
|
|
63
63
|
const pendingForecastKey = 'foreign-position:pending-create'
|
|
64
|
+
const defaultFilterFTUOption: ForeignOption = { label: '否', value: '' }
|
|
65
|
+
const filterFTUOptions: ForeignOption[] = [
|
|
66
|
+
defaultFilterFTUOption,
|
|
67
|
+
{ label: '是', value: '1' },
|
|
68
|
+
]
|
|
64
69
|
|
|
65
70
|
const queryForm = reactive({
|
|
66
71
|
institution: '',
|
|
67
72
|
currency: '',
|
|
68
73
|
business: '',
|
|
69
74
|
status: '',
|
|
75
|
+
filterFTU: '',
|
|
70
76
|
minAmount: '',
|
|
71
77
|
maxAmount: '',
|
|
72
78
|
})
|
|
@@ -100,11 +106,16 @@ const selectedStatus = computed(
|
|
|
100
106
|
() =>
|
|
101
107
|
foreignStatusOptions.find((item) => item.value === queryForm.status) || allForeignStatusOption,
|
|
102
108
|
)
|
|
109
|
+
const selectedFilterFTU = computed(
|
|
110
|
+
() =>
|
|
111
|
+
filterFTUOptions.find((item) => item.value === queryForm.filterFTU) || defaultFilterFTUOption,
|
|
112
|
+
)
|
|
103
113
|
const selectorTitle = computed(() => {
|
|
104
114
|
const titleMap: Record<SelectorType, string> = {
|
|
105
115
|
currency: '选择币种',
|
|
106
116
|
business: '选择业务类型',
|
|
107
117
|
status: '选择预报状态',
|
|
118
|
+
filterFTU: '筛选FTU',
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
return titleMap[selectorType.value]
|
|
@@ -112,6 +123,7 @@ const selectorTitle = computed(() => {
|
|
|
112
123
|
const selectorOptions = computed<SelectorOption[]>(() => {
|
|
113
124
|
if (selectorType.value === 'currency') return foreignCurrencyOptions
|
|
114
125
|
if (selectorType.value === 'business') return foreignBusinessOptions
|
|
126
|
+
if (selectorType.value === 'filterFTU') return filterFTUOptions
|
|
115
127
|
|
|
116
128
|
return foreignStatusOptions
|
|
117
129
|
})
|
|
@@ -119,7 +131,7 @@ const filterSummary = computed(() => {
|
|
|
119
131
|
const institution = queryForm.institution.trim() || '全部机构'
|
|
120
132
|
const currency = selectedCurrency.value.shortLabel
|
|
121
133
|
|
|
122
|
-
return `${selectedDate.value} · ${institution} · ${currency}`
|
|
134
|
+
return `${selectedDate.value} · ${institution} · ${currency} · FTU${selectedFilterFTU.value.label}`
|
|
123
135
|
})
|
|
124
136
|
|
|
125
137
|
onMounted(async () => {
|
|
@@ -138,7 +150,14 @@ function mapForeignRecordToForecast(record: ForeignForecastRecord, index: number
|
|
|
138
150
|
const currency = resolveForeignCurrency(currencyValue)
|
|
139
151
|
const status = resolveForeignStatus(record.predStatus)
|
|
140
152
|
const counterparty =
|
|
141
|
-
record.competName ||
|
|
153
|
+
record.competName ||
|
|
154
|
+
record.clientName ||
|
|
155
|
+
record.rcvBank ||
|
|
156
|
+
record.competAcctSc ||
|
|
157
|
+
record.rcvBankSc ||
|
|
158
|
+
record.bankSwift ||
|
|
159
|
+
record.swiftCode ||
|
|
160
|
+
'-'
|
|
142
161
|
|
|
143
162
|
return {
|
|
144
163
|
id: index + 1,
|
|
@@ -146,7 +165,7 @@ function mapForeignRecordToForecast(record: ForeignForecastRecord, index: number
|
|
|
146
165
|
date: formatForeignForecastDate(record.tranDate),
|
|
147
166
|
currency: currency.shortLabel,
|
|
148
167
|
currencyUnit: currency.unit,
|
|
149
|
-
business: resolveForeignBusinessType(record.busiType),
|
|
168
|
+
business: resolveForeignBusinessType(record.tranType || record.busiType),
|
|
150
169
|
counterparty,
|
|
151
170
|
amount: toTenThousandAmount(record.predAmt),
|
|
152
171
|
predRefNo: record.predRefNo || '',
|
|
@@ -158,12 +177,13 @@ function mapForeignRecordToForecast(record: ForeignForecastRecord, index: number
|
|
|
158
177
|
function buildQueryForm(): ForeignForecastQueryForm {
|
|
159
178
|
return {
|
|
160
179
|
tranDate: selectedDate.value,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
orgName: queryForm.institution.trim(),
|
|
181
|
+
ccy: queryForm.currency,
|
|
182
|
+
tranType: queryForm.business,
|
|
164
183
|
predStatus: queryForm.status,
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
filter: queryForm.filterFTU,
|
|
185
|
+
startAmt: queryForm.minAmount,
|
|
186
|
+
endAmt: queryForm.maxAmount,
|
|
167
187
|
}
|
|
168
188
|
}
|
|
169
189
|
|
|
@@ -207,6 +227,8 @@ function chooseSelectorOption(option: SelectorOption) {
|
|
|
207
227
|
queryForm.currency = option.value
|
|
208
228
|
} else if (selectorType.value === 'business') {
|
|
209
229
|
queryForm.business = option.value
|
|
230
|
+
} else if (selectorType.value === 'filterFTU') {
|
|
231
|
+
queryForm.filterFTU = option.value
|
|
210
232
|
} else {
|
|
211
233
|
queryForm.status = option.value
|
|
212
234
|
}
|
|
@@ -245,6 +267,7 @@ async function handleReset() {
|
|
|
245
267
|
queryForm.currency = ''
|
|
246
268
|
queryForm.business = ''
|
|
247
269
|
queryForm.status = ''
|
|
270
|
+
queryForm.filterFTU = ''
|
|
248
271
|
queryForm.minAmount = ''
|
|
249
272
|
queryForm.maxAmount = ''
|
|
250
273
|
|
|
@@ -285,20 +308,16 @@ async function loadNextPage() {
|
|
|
285
308
|
|
|
286
309
|
try {
|
|
287
310
|
const response = await queryForeignForecastPage(payload)
|
|
288
|
-
const records =
|
|
311
|
+
const records = response.body.resultInfos || []
|
|
289
312
|
const nextList = records.map((item, index) =>
|
|
290
313
|
mapForeignRecordToForecast(item, forecastList.value.length + index),
|
|
291
314
|
)
|
|
292
315
|
|
|
293
316
|
forecastList.value.push(...nextList)
|
|
294
|
-
forecastTotal.value =
|
|
295
|
-
? forecastList.value.length
|
|
296
|
-
: Number(response.body.totalCount) || forecastList.value.length
|
|
317
|
+
forecastTotal.value = Number(response.body.totalCount) || forecastList.value.length
|
|
297
318
|
currentPage.value += 1
|
|
298
|
-
listFinished.value =
|
|
299
|
-
|
|
300
|
-
: forecastList.value.length >= forecastTotal.value ||
|
|
301
|
-
response.body.resultList.length < pageSize
|
|
319
|
+
listFinished.value =
|
|
320
|
+
forecastList.value.length >= forecastTotal.value || records.length < pageSize
|
|
302
321
|
} catch (error) {
|
|
303
322
|
console.error('[外币预报查询失败]', error)
|
|
304
323
|
listError.value = true
|
|
@@ -309,14 +328,6 @@ async function loadNextPage() {
|
|
|
309
328
|
}
|
|
310
329
|
}
|
|
311
330
|
|
|
312
|
-
function filterRecordsByCurrency(records: ForeignForecastRecord[]) {
|
|
313
|
-
const currency = queryForm.currency.trim().toUpperCase()
|
|
314
|
-
|
|
315
|
-
if (!currency) return records
|
|
316
|
-
|
|
317
|
-
return records.filter((item) => getRecordCurrency(item) === currency)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
331
|
function getRecordCurrency(record: ForeignForecastRecord) {
|
|
321
332
|
return (
|
|
322
333
|
record.ccy ||
|
|
@@ -475,6 +486,13 @@ function isPendingForecast(value: Partial<PendingForecast> | null): value is Pen
|
|
|
475
486
|
<van-icon class="row-arrow" name="arrow" />
|
|
476
487
|
</button>
|
|
477
488
|
|
|
489
|
+
<button class="filter-row" type="button" @click="openSelector('filterFTU')">
|
|
490
|
+
<span class="row-icon"><van-icon name="filter-o" /></span>
|
|
491
|
+
<span class="row-label">筛选FTU</span>
|
|
492
|
+
<span class="row-value muted-value">{{ selectedFilterFTU.label }}</span>
|
|
493
|
+
<van-icon class="row-arrow" name="arrow" />
|
|
494
|
+
</button>
|
|
495
|
+
|
|
478
496
|
<div class="filter-row amount-row">
|
|
479
497
|
<span class="row-icon row-icon-money"><van-icon name="gold-coin-o" /></span>
|
|
480
498
|
<span class="row-label">金额区间</span>
|
|
@@ -599,7 +617,8 @@ function isPendingForecast(value: Partial<PendingForecast> | null): value is Pen
|
|
|
599
617
|
v-if="
|
|
600
618
|
(selectorType === 'currency' && item.value === queryForm.currency) ||
|
|
601
619
|
(selectorType === 'business' && item.value === queryForm.business) ||
|
|
602
|
-
(selectorType === 'status' && item.value === queryForm.status)
|
|
620
|
+
(selectorType === 'status' && item.value === queryForm.status) ||
|
|
621
|
+
(selectorType === 'filterFTU' && item.value === queryForm.filterFTU)
|
|
603
622
|
"
|
|
604
623
|
name="success"
|
|
605
624
|
/>
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# mini2.0 项目开发规范
|
|
2
|
+
|
|
3
|
+
> 适用范围:本仓库所有前端页面、组件、状态、接口和构建配置。
|
|
4
|
+
>
|
|
5
|
+
> 最后更新:2026-07-10。
|
|
6
|
+
|
|
7
|
+
## 1. 技术栈与运行环境
|
|
8
|
+
|
|
9
|
+
| 类别 | 项目选型 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 核心框架 | Vue 3、TypeScript、`<script setup>` |
|
|
12
|
+
| 构建工具 | Vite 8 |
|
|
13
|
+
| UI 组件 | Vant 4、Iconify |
|
|
14
|
+
| 路由 | Vue Router,Hash 路由 |
|
|
15
|
+
| 状态管理 | Pinia、pinia-plugin-persistedstate |
|
|
16
|
+
| 网络请求 | Axios、敏行宿主 `MXCommon.ajax` / `NXCommon.ajax` |
|
|
17
|
+
| 样式处理 | CSS、Less、PostCSS、px-to-viewport |
|
|
18
|
+
| 代码质量 | ESLint、OxcLint、Prettier、vue-tsc |
|
|
19
|
+
|
|
20
|
+
- Node.js 必须满足 `^20.19.0 || >=22.12.0`。
|
|
21
|
+
- 统一使用 npm 和仓库内的 `package-lock.json`,不要混用 pnpm、Yarn 或 Bun。
|
|
22
|
+
- 全局源码使用 UTF-8、LF 换行、2 空格缩进。
|
|
23
|
+
|
|
24
|
+
## 2. 常用命令
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 安装依赖
|
|
28
|
+
npm ci
|
|
29
|
+
|
|
30
|
+
# 本地开发,默认使用 development 环境并监听 8080 端口
|
|
31
|
+
npm run dev
|
|
32
|
+
|
|
33
|
+
# 使用 test 环境启动
|
|
34
|
+
npm run dev:test
|
|
35
|
+
|
|
36
|
+
# 自动修复 ESLint 和 OxcLint 问题
|
|
37
|
+
npm run lint
|
|
38
|
+
|
|
39
|
+
# TypeScript / Vue 类型检查
|
|
40
|
+
npm run type-check
|
|
41
|
+
|
|
42
|
+
# 格式化 src 目录
|
|
43
|
+
npm run format
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
构建命令仅在发布或明确需要构建验证时执行:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run build
|
|
50
|
+
npm run build:test
|
|
51
|
+
npm run build:prod
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2.1 构建副作用
|
|
55
|
+
|
|
56
|
+
项目的 `plugins/bump-version.ts` 会在构建时执行以下操作:
|
|
57
|
+
|
|
58
|
+
1. 自动修改 `src/config/plugin.properties.test` 或 `src/config/plugin.properties.pro` 的版本号;
|
|
59
|
+
2. 输出 `dist/www`、`dist/plugin.properties`;
|
|
60
|
+
3. 生成 `dist/dist_<version_code>.zip`。
|
|
61
|
+
|
|
62
|
+
因此不要把构建产生的版本号变化混入普通功能提交。构建后必须检查 `git diff`,只保留本次任务确实需要的版本文件变更。
|
|
63
|
+
|
|
64
|
+
## 3. 环境配置
|
|
65
|
+
|
|
66
|
+
项目按 Vite mode 读取配置:
|
|
67
|
+
|
|
68
|
+
- `.env.development`:本地开发;
|
|
69
|
+
- `.env.test`:测试环境;
|
|
70
|
+
- `.env.production`:生产环境。
|
|
71
|
+
|
|
72
|
+
当前公共变量:
|
|
73
|
+
|
|
74
|
+
| 变量 | 说明 |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `VITE_APP_ENV` | 当前业务环境标识 |
|
|
77
|
+
| `VITE_API_BASE_URL` | 接口基础地址 |
|
|
78
|
+
| `VITE_API_TIMEOUT` | 请求超时时间,单位毫秒 |
|
|
79
|
+
| `VITE_ENABLE_VUE_DEVTOOLS` | 是否启用 Vue DevTools,设置为 `false` 时关闭 |
|
|
80
|
+
|
|
81
|
+
规范要求:
|
|
82
|
+
|
|
83
|
+
- 只有允许暴露到浏览器的变量才使用 `VITE_` 前缀;
|
|
84
|
+
- 不得在源码、环境文件、日志或提交记录中写入密码、Token、私钥等敏感信息;
|
|
85
|
+
- 新增环境变量时,同时补充 `env.d.ts` 类型和本文档说明;
|
|
86
|
+
- 本地开发接口统一经过 Vite `/oms` 代理,不在页面中硬编码服务地址。
|
|
87
|
+
|
|
88
|
+
## 4. 目录职责
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
src/
|
|
92
|
+
├── api/ # 业务接口、接口入参与响应类型、后端数据归一化
|
|
93
|
+
├── components/ # 跨页面复用的通用组件
|
|
94
|
+
├── config/ # 环境、宿主应用和打包配置
|
|
95
|
+
├── core/ # 基础设施:敏行 JSAPI、双端请求适配等
|
|
96
|
+
├── router/ # 路由表和全局路由守卫
|
|
97
|
+
├── stores/ # Pinia 全局或业务域共享状态
|
|
98
|
+
├── styles/ # 全局样式及第三方组件覆盖
|
|
99
|
+
├── types/ # 跨模块公共类型声明
|
|
100
|
+
├── utils/ # 无页面依赖的通用工具和业务格式化函数
|
|
101
|
+
└── views/ # 页面,按业务域组织
|
|
102
|
+
├── announcement/
|
|
103
|
+
├── dashboard/
|
|
104
|
+
├── foreign/
|
|
105
|
+
├── home/
|
|
106
|
+
├── login/
|
|
107
|
+
├── mine/
|
|
108
|
+
└── rmb/
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 4.1 页面目录规则
|
|
112
|
+
|
|
113
|
+
- 页面必须按业务域归档,人民币业务放在 `views/rmb`,外币业务放在 `views/foreign`。
|
|
114
|
+
- 列表、详情、新增等同一业务的页面放在同一模块下,例如:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
views/rmb/position/
|
|
118
|
+
├── index.vue
|
|
119
|
+
├── create/index.vue
|
|
120
|
+
└── detail/index.vue
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- 移动页面文件时,默认只调整目录和路由 import,不改变已对外使用的 URL。
|
|
124
|
+
- `src/views/home/index.vue` 是首页业务入口清单;新增、删除或调整首页模块时需要同步检查它。
|
|
125
|
+
- 只在多个页面实际复用时抽取到 `components`;页面私有组件应放在对应页面模块内,避免过早全局化。
|
|
126
|
+
|
|
127
|
+
## 5. 命名规范
|
|
128
|
+
|
|
129
|
+
| 对象 | 规范 | 示例 |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| 业务目录 | kebab-case | `position-estimate` |
|
|
132
|
+
| 页面入口 | `index.vue` | `views/rmb/position/index.vue` |
|
|
133
|
+
| 通用组件 | PascalCase | `AppTitleBar.vue` |
|
|
134
|
+
| 变量、函数 | camelCase | `systemWorkDate`、`handleQuery` |
|
|
135
|
+
| 常量 | UPPER_SNAKE_CASE | `DETAIL_TRAN_CODE` |
|
|
136
|
+
| 类型、接口 | PascalCase | `PbcPositionBalance` |
|
|
137
|
+
| Store Hook | `useXxxStore` | `usePositionFilterStore` |
|
|
138
|
+
| 事件处理函数 | `handleXxx` 或 `onXxx` | `handleSubmit`、`onMounted` |
|
|
139
|
+
| CSS 类 | kebab-case,组件内建议 BEM | `app-title-bar__back` |
|
|
140
|
+
|
|
141
|
+
- 布尔值使用 `is`、`has`、`show`、`can`、`should` 等可读前缀。
|
|
142
|
+
- API 字段必须保留后端原始命名时,在 API 层声明类型并集中映射,不把含义不明的缩写继续扩散到视图层。
|
|
143
|
+
- import 优先使用 `@/` 别名,避免多层 `../../../` 相对路径。
|
|
144
|
+
|
|
145
|
+
## 6. TypeScript 规范
|
|
146
|
+
|
|
147
|
+
- 禁止无理由使用 `any`;外部未知数据先使用 `unknown`,再通过类型守卫收窄。
|
|
148
|
+
- 请求入参、响应体、组件 Props、Emits 和 Store 状态必须声明类型。
|
|
149
|
+
- 类型仅用于编译时,应使用 `import type`。
|
|
150
|
+
- 项目启用了 `noUncheckedIndexedAccess`,数组或对象索引结果必须处理 `undefined`。
|
|
151
|
+
- 可选字段在进入计算和展示前应提供明确兜底,不使用非空断言掩盖真实问题。
|
|
152
|
+
- 优先使用联合类型表达有限状态,不用散落的魔法字符串。
|
|
153
|
+
- 共用类型放在 `src/types`;仅被一个 API 使用的类型与对应 API 文件放在一起。
|
|
154
|
+
- 金额等高精度后端字段优先保留为字符串,在格式化或计算边界再显式转换。
|
|
155
|
+
|
|
156
|
+
## 7. Vue 组件规范
|
|
157
|
+
|
|
158
|
+
### 7.1 脚本
|
|
159
|
+
|
|
160
|
+
- 新组件统一使用 `<script setup lang="ts">` 和 Composition API。
|
|
161
|
+
- 单文件组件按“依赖导入 → Props/Emits → 响应式状态 → 计算属性 → 生命周期 → 事件方法”的顺序组织。
|
|
162
|
+
- Props 和 Emits 使用类型声明;有默认值时使用 `withDefaults`。
|
|
163
|
+
- 派生数据使用 `computed`,不要用 `watch` 手动维护可计算状态。
|
|
164
|
+
- 注册了原生事件或定时器时,必须在 `onBeforeUnmount` / `onUnmounted` 中清理。
|
|
165
|
+
- Vue、Vue Router、Pinia、VueUse 常用 API 已配置自动导入;业务模块、工具函数和类型仍需显式 import。
|
|
166
|
+
- Vant 组件已按需自动注册,直接使用 `van-*` 组件,不在页面重复做全量注册。
|
|
167
|
+
|
|
168
|
+
### 7.2 模板
|
|
169
|
+
|
|
170
|
+
- 模板只负责声明展示,不放复杂数据转换或多层业务判断;复杂逻辑放到 `computed` 或函数中。
|
|
171
|
+
- 列表渲染必须提供稳定且唯一的 `:key`,不得默认使用数组下标。
|
|
172
|
+
- 原生 `<button>` 必须设置 `type="button"` 或正确的提交类型。
|
|
173
|
+
- 图标按钮、返回按钮和自定义交互控件必须提供 `aria-label`,并保留键盘可操作性。
|
|
174
|
+
- “隐藏”和“禁用”是不同语义:需求要求隐藏时使用 `v-if`,不要仅设置 `disabled`。
|
|
175
|
+
- 请求中的按钮应绑定 loading 状态,避免重复提交。
|
|
176
|
+
|
|
177
|
+
### 7.3 样式
|
|
178
|
+
|
|
179
|
+
- 页面和组件样式默认使用 `<style scoped>`;真正全局的样式才放入 `src/styles`。
|
|
180
|
+
- 覆盖 Vant 内部样式时使用 `:deep()`,并把影响范围限制在当前页面或组件。
|
|
181
|
+
- 全局 Vant 覆盖统一放在 `src/styles/vant-overrides.css`。
|
|
182
|
+
- 页面最小高度优先使用 `var(--app-page-min-height)`,不要各自重复计算标题栏和 Tabbar 高度。
|
|
183
|
+
- 必须考虑 `env(safe-area-inset-top)` 和 `env(safe-area-inset-bottom)`,避免内容被刘海、标题栏或底部导航遮挡。
|
|
184
|
+
- 主内容在大屏环境下应遵守项目现有的 `750px` 最大宽度约束。
|
|
185
|
+
- PostCSS 以 `375px` 设计稿宽度把大于等于 `2px` 的 px 转为 vw;`1px` 边框会保留。编写尺寸时必须意识到这一转换规则。
|
|
186
|
+
- 优先复用 Vant CSS 变量和项目现有色彩,不随意增加新的全局颜色或层级值。
|
|
187
|
+
|
|
188
|
+
## 8. 路由规范
|
|
189
|
+
|
|
190
|
+
所有页面路由集中在 `src/router/index.ts`:
|
|
191
|
+
|
|
192
|
+
- 页面组件使用动态 import,保持路由级懒加载;
|
|
193
|
+
- `name` 使用唯一的 PascalCase 名称;
|
|
194
|
+
- 需要登录的页面必须配置 `meta.requiresAuth: true`;
|
|
195
|
+
- 标题统一配置在 `meta.title`;
|
|
196
|
+
- 只有需要底部导航的一级页面才设置 `meta.showTabbar: true`;
|
|
197
|
+
- 新增一级页面时,同步检查 `src/App.vue` 中的 Tabbar、无返回按钮路由和标题栏行为;
|
|
198
|
+
- 路由参数使用语义化名称,例如 `:predRefNo`、`:id`;
|
|
199
|
+
- 不在单个页面重复实现登录跳转,统一交给全局路由守卫。
|
|
200
|
+
|
|
201
|
+
示例:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
{
|
|
205
|
+
path: '/example',
|
|
206
|
+
name: 'Example',
|
|
207
|
+
component: () => import('@/views/rmb/example/index.vue'),
|
|
208
|
+
meta: { requiresAuth: true, title: '示例页面' },
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## 9. Pinia 状态规范
|
|
213
|
+
|
|
214
|
+
按状态作用域决定存放位置:
|
|
215
|
+
|
|
216
|
+
1. 仅当前组件使用、离开页面即可丢失:使用组件内 `ref` / `reactive`;
|
|
217
|
+
2. 同一业务的多个页面或组件共享:新建业务 Store;
|
|
218
|
+
3. 需要刷新后保留:在对应业务 Store 中配置持久化;
|
|
219
|
+
4. 全应用通用运行状态和应用设置:放在 `src/stores/app.ts`;
|
|
220
|
+
5. 登录态和用户信息:放在 `src/stores/user.ts`。
|
|
221
|
+
|
|
222
|
+
具体要求:
|
|
223
|
+
|
|
224
|
+
- 统一使用 Setup Store 写法;
|
|
225
|
+
- 页面专属状态不得为了省事全部塞入 `app.ts`,应创建独立的 feature store;
|
|
226
|
+
- 共享业务状态通过 Store action 修改,不让页面到处直接拼装同一业务规则;
|
|
227
|
+
- 持久化必须设置唯一、稳定的 key,并用 `pick` 只保存必要字段;
|
|
228
|
+
- 新增持久化字段时考虑旧数据兼容、默认值和退出登录后的清理策略;
|
|
229
|
+
- 除明确设计的登录态外,不持久化敏感数据、临时响应和可重新计算的数据;
|
|
230
|
+
- `piniaPluginPersistedstate` 已在 `src/main.ts` 注册,不要重复注册。
|
|
231
|
+
|
|
232
|
+
## 10. 接口与请求规范
|
|
233
|
+
|
|
234
|
+
### 10.1 分层
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
页面 / Store
|
|
238
|
+
↓
|
|
239
|
+
src/api/* 业务接口与数据映射
|
|
240
|
+
↓
|
|
241
|
+
src/core/request 双端请求适配
|
|
242
|
+
├── 浏览器:Axios
|
|
243
|
+
└── 敏行宿主:MXCommon.ajax / NXCommon.ajax
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- 页面不得直接调用 Axios 或 `MXCommon.ajax`,统一调用 `src/api` 暴露的业务函数。
|
|
247
|
+
- 业务 API 必须从 `@/core/request` 引入 `request`,确保浏览器和敏行宿主行为一致。
|
|
248
|
+
- GET 查询参数放 `params`,POST/PUT 请求体放 `data`。
|
|
249
|
+
- 登录、刷新 Token 等无需鉴权的接口才允许设置 `skipAuth: true`。
|
|
250
|
+
- 敏行原生请求仅支持 GET、POST、PUT、DELETE;新增 PATCH 接口前必须先确认宿主兼容方案。
|
|
251
|
+
- TAM 交易统一复用 `buildTamRequestPayload` 和 `tamRequest`,交易码定义为带业务说明的常量。
|
|
252
|
+
|
|
253
|
+
示例:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { request } from '@/core/request'
|
|
257
|
+
|
|
258
|
+
export type ExampleQuery = {
|
|
259
|
+
date: string
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export type ExampleResult = {
|
|
263
|
+
amount: string
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function queryExample(params: ExampleQuery) {
|
|
267
|
+
return request<ExampleResult>({
|
|
268
|
+
url: '/example',
|
|
269
|
+
method: 'GET',
|
|
270
|
+
params,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 10.2 响应与错误处理
|
|
276
|
+
|
|
277
|
+
- Axios 请求层统一处理进度条、鉴权头、Token 刷新、HTTP 错误和通用业务 code。
|
|
278
|
+
- API 层负责兼容后端包装结构、字段别名和数据归一化,页面只消费稳定模型。
|
|
279
|
+
- 页面异步操作使用 `try/catch/finally`,并在 `finally` 中恢复 loading 状态。
|
|
280
|
+
- 用户提示统一使用 `showAppToast` / `showAppLoadingToast`,不直接散落 Vant Toast 配置。
|
|
281
|
+
- 不吞掉异常;无法在当前层恢复时继续抛出,让调用方决定页面提示和交互。
|
|
282
|
+
- 日志必须包含可定位的业务上下文,但不得输出密码、完整 Token、敏感账户信息或整段隐私报文。
|
|
283
|
+
|
|
284
|
+
## 11. 业务数据规范
|
|
285
|
+
|
|
286
|
+
### 11.1 金额
|
|
287
|
+
|
|
288
|
+
- 后端返回的人民币金额默认按“元”理解,视图主展示统一通过共享工具转换为“亿元”。
|
|
289
|
+
- 转换前先去除千分位逗号,使用 `src/utils/rmb-forecast.ts` 中的共享函数,避免页面各写一套算法。
|
|
290
|
+
- `0`、`0.00` 是有效业务值,不得用普通真值判断当作空数据。
|
|
291
|
+
- 只有产品需求明确要求时,才在“亿元”下补充完整“元”金额。
|
|
292
|
+
- 保留正负号、精度和异常值兜底,不直接在模板中执行 `Number(...).toFixed(...)`。
|
|
293
|
+
|
|
294
|
+
### 11.2 日期
|
|
295
|
+
|
|
296
|
+
- 页面日期控件使用 `YYYY-MM-DD`;TAM 等接口按约定转换为 `YYYYMMDD`。
|
|
297
|
+
- 系统工作日优先复用 `useAppStore()` 的 `systemWorkDate` / `systemWorkDateCompact`。
|
|
298
|
+
- 日期格式化集中到 API 或 utils 层,不在多个页面重复切片拼接。
|
|
299
|
+
|
|
300
|
+
### 11.3 富文本
|
|
301
|
+
|
|
302
|
+
- 使用 `v-html` 前必须经过白名单清洗,统一复用公告模块的 `sanitizeArticleHtml` 思路。
|
|
303
|
+
- 图片协议、Base64、链接协议和危险属性都必须显式校验。
|
|
304
|
+
- 富文本点击、图片预览等行为优先使用容器事件委托,避免给动态 HTML 注入脚本处理器。
|
|
305
|
+
|
|
306
|
+
## 12. 注释与日志规范
|
|
307
|
+
|
|
308
|
+
- 注释使用中文,重点解释“为什么”和业务约束,不复述代码字面含义。
|
|
309
|
+
- 交易码、字段匹配、跨接口关联、单位转换和兼容逻辑必须补充注释。
|
|
310
|
+
- 公共函数和复杂类型优先使用 JSDoc,说明参数、返回值、单位和异常情况。
|
|
311
|
+
- 临时调试日志在提交前删除;确需保留的联调日志使用稳定前缀,例如 `[人行头寸余额查询]`。
|
|
312
|
+
- 禁止在生产日志中打印认证凭证或完整敏感报文。
|
|
313
|
+
|
|
314
|
+
## 13. 格式化与质量检查
|
|
315
|
+
|
|
316
|
+
代码风格以仓库配置为准:
|
|
317
|
+
|
|
318
|
+
- 不使用分号;
|
|
319
|
+
- 字符串使用单引号;
|
|
320
|
+
- 单行宽度尽量不超过 100;
|
|
321
|
+
- 2 空格缩进;
|
|
322
|
+
- 文件末尾保留换行;
|
|
323
|
+
- 删除行尾空格。
|
|
324
|
+
|
|
325
|
+
注意:`npm run lint` 和 `npm run format` 都会直接修改文件。执行后必须重新检查 diff,避免把无关格式化混入当前任务。
|
|
326
|
+
|
|
327
|
+
项目当前没有统一的自动化测试脚本。功能改动至少应覆盖以下检查:
|
|
328
|
+
|
|
329
|
+
1. 检查修改文件的 ESLint / TypeScript 问题;
|
|
330
|
+
2. 在目标环境手动验证成功、空数据、错误和重复点击场景;
|
|
331
|
+
3. 涉及移动端布局时检查安全区、长文本、小屏和 750px 宽屏表现;
|
|
332
|
+
4. 涉及双端请求时分别考虑浏览器 Axios 与敏行宿主 AJAX 的差异;
|
|
333
|
+
5. 只有明确需要时再执行构建,并处理版本文件副作用。
|
|
334
|
+
|
|
335
|
+
## 14. Git 与发布规范
|
|
336
|
+
|
|
337
|
+
- 一次提交只处理一个明确目标,不夹带无关重构、格式化或生成文件。
|
|
338
|
+
- 提交信息遵循仓库现有 Conventional Commits 风格:
|
|
339
|
+
|
|
340
|
+
```text
|
|
341
|
+
feat(position-filter): 持久化筛选区展开状态
|
|
342
|
+
fix(announcement): 修复详情图片预览
|
|
343
|
+
refactor(router): 调整业务页面目录
|
|
344
|
+
chore(ci): 固定 npm 发布版本
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- 常用类型:`feat`、`fix`、`refactor`、`docs`、`style`、`chore`。
|
|
348
|
+
- 不提交 `dist`、`.playwright-cli`、日志、缓存和 IDE 私有配置。
|
|
349
|
+
- 移动文件后必须检查 `src/router/index.ts` 和全仓旧 import,确保没有残留路径。
|
|
350
|
+
- 推送前检查 `git status` 和 `git diff`,保留用户已有且与任务无关的修改。
|
|
351
|
+
- `main` 分支每次 push 都会触发 `.github/workflows/publish.yml`:自动提升 npm patch 版本、发布 npm 包并回推版本提交与 tag。普通开发不得把“推送 main”当作无副作用操作。
|
|
352
|
+
- 发布流程使用 npm Trusted Publishing;不要在仓库中新增长期 npm Token。
|
|
353
|
+
|
|
354
|
+
## 15. 提交前检查清单
|
|
355
|
+
|
|
356
|
+
- [ ] 修改范围与需求一致,没有顺手改动无关功能。
|
|
357
|
+
- [ ] 页面、API、Store、工具函数放在正确目录。
|
|
358
|
+
- [ ] 新路由已配置唯一名称、标题、鉴权和正确的懒加载路径。
|
|
359
|
+
- [ ] 页面共享或持久化状态已放入独立、职责清晰的 Pinia Store。
|
|
360
|
+
- [ ] 请求通过 `src/api` 和 `@/core/request` 发出,没有绕过双端适配层。
|
|
361
|
+
- [ ] 入参、响应、Props、Emits 和 Store 状态具备 TypeScript 类型。
|
|
362
|
+
- [ ] 金额、日期、空值和 `0` 值按业务约定处理。
|
|
363
|
+
- [ ] 用户提示、loading、异常和重复提交状态完整。
|
|
364
|
+
- [ ] 移动端安全区、标题栏、Tabbar 和响应式宽度未被破坏。
|
|
365
|
+
- [ ] 没有敏感信息、临时日志、生成文件或无关格式化进入 diff。
|
|
366
|
+
- [ ] 如果执行过构建,已确认版本配置文件是否发生非预期变化。
|
|
367
|
+
- [ ] 已查看最终 `git status` 和 `git diff`。
|