accounting-page 0.1.11 → 0.1.16

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/README.md CHANGED
@@ -5,4 +5,66 @@ This template should help get you started developing with Vue 3 and TypeScript i
5
5
  the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
6
6
 
7
7
  Learn more about the recommended Project Setup and IDE Support in
8
- the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
8
+ the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
9
+
10
+ ## Run
11
+
12
+ ### 环境
13
+
14
+ ```shell
15
+ $$env:PATH+=";"+$env:USERPROFILE+"\AppData\Roaming\JetBrains\IntelliJIdea2026.1\node\versions\24.13.0\"
16
+ ```
17
+
18
+ ### 新的开发
19
+
20
+ ```shell
21
+ cd ~\IdeaProjects\ricewines\invest-page\accounting-page
22
+ ```
23
+
24
+ ```shell
25
+ $$env:INVEST_VERSION = (Get-Content "./version.txt" -Raw).Trim()
26
+ ```
27
+
28
+ ```shell
29
+ echo "已设置版本:$env:INVEST_VERSION"
30
+ echo "已设置版本:$env:PATH"
31
+ ```
32
+
33
+ ```shell
34
+ git add .
35
+ ```
36
+
37
+ ```shell
38
+ git commit -m "#16 升级"
39
+ ```
40
+
41
+ ```shell
42
+ git tag -a v$env:INVEST_VERSION -m "发布版本$env:INVEST_VERSION"
43
+ ```
44
+
45
+ ```shell
46
+ git push origin v$env:INVEST_VERSION
47
+ ```
48
+
49
+ ```shell
50
+ git push origin dev_chixh
51
+ ```
52
+
53
+ ### 发布
54
+
55
+ ```shell
56
+ npm login
57
+ ```
58
+
59
+ ```shell
60
+ npm publish
61
+ ```
62
+
63
+ ## TODO
64
+
65
+ - [X] 统计周期:本月首日—今日,报表类型选择资产负债表
66
+ - [X] 科目选择框设置为全屏/最大化
67
+ - [X] 凭证保存成功后,自动新建空白凭证窗口
68
+ - [ ] 科目选择框新增搜索功能(暂时搁置)
69
+ - [X] 金额支持小数点,计算精度保留到分
70
+ - [ ] 输入框点击后,无需聚焦。
package/index.html CHANGED
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="zh">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accounting-page",
3
- "version": "0.1.11",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -10,17 +10,17 @@
10
10
  "update-check": "npm install --no-save npm-check && npm-check -u"
11
11
  },
12
12
  "dependencies": {
13
- "axios": "^1.15.2",
13
+ "axios": "^1.17.0",
14
14
  "vant": "^4.9.24",
15
- "vue": "^3.5.32",
16
- "vue-router": "^5.0.6"
15
+ "vue": "^3.5.35",
16
+ "vue-router": "^5.1.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/node": "^24.12.2",
20
- "@vitejs/plugin-vue": "^6.0.6",
19
+ "@types/node": "^25.9.2",
20
+ "@vitejs/plugin-vue": "^6.0.7",
21
21
  "@vue/tsconfig": "^0.9.1",
22
22
  "typescript": "~6.0.2",
23
- "vite": "^8.0.10",
24
- "vue-tsc": "^3.2.7"
23
+ "vite": "^8.0.16",
24
+ "vue-tsc": "^3.3.3"
25
25
  }
26
26
  }
package/src/App.vue CHANGED
@@ -1,20 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import { ref } from 'vue'
3
-
4
- const active = ref("voucher")
3
+ /// 获取页面路径
4
+ console.log(""+location.pathname)
5
+ let pathname = ref(location.pathname)
6
+ const active = ref(pathname.value)
5
7
  </script>
6
8
 
7
9
  <template>
8
10
  <router-view />
9
- <van-tabbar v-model="active">
10
- <van-tabbar-item icon="orders" to="/voucher" name="voucher">凭证记账</van-tabbar-item>
11
- <van-tabbar-item icon="balance" to="/balance-sheet" name="balance-sheet">资产负债表</van-tabbar-item>
11
+ <van-tabbar v-model="active" fixed>
12
+ <van-tabbar-item icon="balance" to="/balance-sheet" name="/balance-sheet">资产负债表</van-tabbar-item>
13
+ <van-tabbar-item icon="orders" to="/voucher" name="/voucher">凭证记账</van-tabbar-item>
12
14
  </van-tabbar>
13
15
  </template>
14
16
 
15
17
  <style>
16
- body {
17
- margin: 0;
18
- padding: 0;
19
- }
20
18
  </style>
package/src/style.css CHANGED
@@ -1,296 +0,0 @@
1
- :root {
2
- --text: #6b6375;
3
- --text-h: #08060d;
4
- --bg: #fff;
5
- --border: #e5e4e7;
6
- --code-bg: #f4f3ec;
7
- --accent: #aa3bff;
8
- --accent-bg: rgba(170, 59, 255, 0.1);
9
- --accent-border: rgba(170, 59, 255, 0.5);
10
- --social-bg: rgba(244, 243, 236, 0.5);
11
- --shadow:
12
- rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
13
-
14
- --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
15
- --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
16
- --mono: ui-monospace, Consolas, monospace;
17
-
18
- font: 18px/145% var(--sans);
19
- letter-spacing: 0.18px;
20
- color-scheme: light dark;
21
- color: var(--text);
22
- background: var(--bg);
23
- font-synthesis: none;
24
- text-rendering: optimizeLegibility;
25
- -webkit-font-smoothing: antialiased;
26
- -moz-osx-font-smoothing: grayscale;
27
-
28
- @media (max-width: 1024px) {
29
- font-size: 16px;
30
- }
31
- }
32
-
33
- @media (prefers-color-scheme: dark) {
34
- :root {
35
- --text: #9ca3af;
36
- --text-h: #f3f4f6;
37
- --bg: #16171d;
38
- --border: #2e303a;
39
- --code-bg: #1f2028;
40
- --accent: #c084fc;
41
- --accent-bg: rgba(192, 132, 252, 0.15);
42
- --accent-border: rgba(192, 132, 252, 0.5);
43
- --social-bg: rgba(47, 48, 58, 0.5);
44
- --shadow:
45
- rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
46
- }
47
-
48
- #social .button-icon {
49
- filter: invert(1) brightness(2);
50
- }
51
- }
52
-
53
- body {
54
- margin: 0;
55
- }
56
-
57
- h1,
58
- h2 {
59
- font-family: var(--heading);
60
- font-weight: 500;
61
- color: var(--text-h);
62
- }
63
-
64
- h1 {
65
- font-size: 56px;
66
- letter-spacing: -1.68px;
67
- margin: 32px 0;
68
- @media (max-width: 1024px) {
69
- font-size: 36px;
70
- margin: 20px 0;
71
- }
72
- }
73
- h2 {
74
- font-size: 24px;
75
- line-height: 118%;
76
- letter-spacing: -0.24px;
77
- margin: 0 0 8px;
78
- @media (max-width: 1024px) {
79
- font-size: 20px;
80
- }
81
- }
82
- p {
83
- margin: 0;
84
- }
85
-
86
- code,
87
- .counter {
88
- font-family: var(--mono);
89
- display: inline-flex;
90
- border-radius: 4px;
91
- color: var(--text-h);
92
- }
93
-
94
- code {
95
- font-size: 15px;
96
- line-height: 135%;
97
- padding: 4px 8px;
98
- background: var(--code-bg);
99
- }
100
-
101
- .counter {
102
- font-size: 16px;
103
- padding: 5px 10px;
104
- border-radius: 5px;
105
- color: var(--accent);
106
- background: var(--accent-bg);
107
- border: 2px solid transparent;
108
- transition: border-color 0.3s;
109
- margin-bottom: 24px;
110
-
111
- &:hover {
112
- border-color: var(--accent-border);
113
- }
114
- &:focus-visible {
115
- outline: 2px solid var(--accent);
116
- outline-offset: 2px;
117
- }
118
- }
119
-
120
- .hero {
121
- position: relative;
122
-
123
- .base,
124
- .framework,
125
- .vite {
126
- inset-inline: 0;
127
- margin: 0 auto;
128
- }
129
-
130
- .base {
131
- width: 170px;
132
- position: relative;
133
- z-index: 0;
134
- }
135
-
136
- .framework,
137
- .vite {
138
- position: absolute;
139
- }
140
-
141
- .framework {
142
- z-index: 1;
143
- top: 34px;
144
- height: 28px;
145
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
146
- scale(1.4);
147
- }
148
-
149
- .vite {
150
- z-index: 0;
151
- top: 107px;
152
- height: 26px;
153
- width: auto;
154
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
155
- scale(0.8);
156
- }
157
- }
158
-
159
- #app {
160
- width: 1126px;
161
- max-width: 100%;
162
- margin: 0 auto;
163
- text-align: center;
164
- border-inline: 1px solid var(--border);
165
- min-height: 100svh;
166
- display: flex;
167
- flex-direction: column;
168
- box-sizing: border-box;
169
- }
170
-
171
- #center {
172
- display: flex;
173
- flex-direction: column;
174
- gap: 25px;
175
- place-content: center;
176
- place-items: center;
177
- flex-grow: 1;
178
-
179
- @media (max-width: 1024px) {
180
- padding: 32px 20px 24px;
181
- gap: 18px;
182
- }
183
- }
184
-
185
- #next-steps {
186
- display: flex;
187
- border-top: 1px solid var(--border);
188
- text-align: left;
189
-
190
- & > div {
191
- flex: 1 1 0;
192
- padding: 32px;
193
- @media (max-width: 1024px) {
194
- padding: 24px 20px;
195
- }
196
- }
197
-
198
- .icon {
199
- margin-bottom: 16px;
200
- width: 22px;
201
- height: 22px;
202
- }
203
-
204
- @media (max-width: 1024px) {
205
- flex-direction: column;
206
- text-align: center;
207
- }
208
- }
209
-
210
- #docs {
211
- border-right: 1px solid var(--border);
212
-
213
- @media (max-width: 1024px) {
214
- border-right: none;
215
- border-bottom: 1px solid var(--border);
216
- }
217
- }
218
-
219
- #next-steps ul {
220
- list-style: none;
221
- padding: 0;
222
- display: flex;
223
- gap: 8px;
224
- margin: 32px 0 0;
225
-
226
- .logo {
227
- height: 18px;
228
- }
229
-
230
- a {
231
- color: var(--text-h);
232
- font-size: 16px;
233
- border-radius: 6px;
234
- background: var(--social-bg);
235
- display: flex;
236
- padding: 6px 12px;
237
- align-items: center;
238
- gap: 8px;
239
- text-decoration: none;
240
- transition: box-shadow 0.3s;
241
-
242
- &:hover {
243
- box-shadow: var(--shadow);
244
- }
245
- .button-icon {
246
- height: 18px;
247
- width: 18px;
248
- }
249
- }
250
-
251
- @media (max-width: 1024px) {
252
- margin-top: 20px;
253
- flex-wrap: wrap;
254
- justify-content: center;
255
-
256
- li {
257
- flex: 1 1 calc(50% - 8px);
258
- }
259
-
260
- a {
261
- width: 100%;
262
- justify-content: center;
263
- box-sizing: border-box;
264
- }
265
- }
266
- }
267
-
268
- #spacer {
269
- height: 88px;
270
- border-top: 1px solid var(--border);
271
- @media (max-width: 1024px) {
272
- height: 48px;
273
- }
274
- }
275
-
276
- .ticks {
277
- position: relative;
278
- width: 100%;
279
-
280
- &::before,
281
- &::after {
282
- content: '';
283
- position: absolute;
284
- top: -4.5px;
285
- border: 5px solid transparent;
286
- }
287
-
288
- &::before {
289
- left: 0;
290
- border-left-color: var(--border);
291
- }
292
- &::after {
293
- right: 0;
294
- border-right-color: var(--border);
295
- }
296
- }
@@ -1,66 +1,78 @@
1
1
  <template>
2
- <div class="page">
3
- <van-nav-bar title="资产负债表" fixed />
2
+ <div>
3
+ <van-nav-bar title="资产负债" fixed/>
4
+ <van-nav-bar title="资产负债" />
4
5
 
5
- <van-cell-group>
6
- <van-field
6
+ <van-field
7
7
  v-model="startDate"
8
8
  label="开始日期"
9
9
  type="date"
10
10
  value-format="YYYY-MM-DD"
11
- />
12
- <van-field
11
+ />
12
+ <van-field
13
13
  v-model="endDate"
14
14
  label="结束日期"
15
15
  type="date"
16
16
  value-format="YYYY-MM-DD"
17
- />
18
- </van-cell-group>
17
+ />
19
18
 
20
19
  <van-button
21
- type="primary"
22
- block
23
- @click="loadData"
24
- style="margin: 15px"
25
- >
26
- 查询报表
20
+ type="primary"
21
+ block
22
+ @click="loadData">查询报表
27
23
  </van-button>
28
24
 
29
25
  <van-cell-group title="资产">
30
26
  <van-cell
31
- v-for="item in assets"
32
- :key="item.code"
33
- :title="item.name"
34
- :value="item.balance"
27
+ v-for="item in assets"
28
+ :key="item.code"
29
+ :title="item.name"
30
+ :value="item.balance"
35
31
  />
36
32
  </van-cell-group>
37
33
 
38
34
  <van-cell-group title="负债">
39
35
  <van-cell
40
- v-for="item in liabilities"
41
- :key="item.code"
42
- :title="item.name"
43
- :value="item.balance"
36
+ v-for="item in liabilities"
37
+ :key="item.code"
38
+ :title="item.name"
39
+ :value="item.balance"
44
40
  />
45
41
  </van-cell-group>
46
42
 
47
43
  <van-cell-group title="权益">
48
44
  <van-cell
49
- v-for="item in equity"
50
- :key="item.code"
51
- :title="item.name"
52
- :value="item.balance"
45
+ v-for="item in equity"
46
+ :key="item.code"
47
+ :title="item.name"
48
+ :value="item.balance"
53
49
  />
54
50
  </van-cell-group>
51
+ -->
55
52
  </div>
56
53
  </template>
57
54
 
58
55
  <script lang="ts" setup>
59
- import { ref } from 'vue'
56
+ import {onMounted, ref} from 'vue'
60
57
  import axios from 'axios'
61
58
 
62
- const startDate = ref('2025-01-01')
63
- const endDate = ref('2025-12-31')
59
+ // 自动获取:本月首日
60
+ const getMonthFirstDay = () => {
61
+ // 传入 Date,返回【当前系统时区】的 toISOString 格式日期(YYYY-MM-DD)
62
+ let now =new Date()
63
+ let date =new Date(now.getFullYear(), now.getMonth() ,1)
64
+ return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
65
+ .toISOString().split('T')[0];
66
+ }
67
+
68
+ // 自动获取:今日
69
+ const getToday = () => {
70
+ return new Date().toISOString().slice(0, 10)
71
+ }
72
+
73
+ // 默认值:本月首日 — 今日
74
+ const startDate = ref(getMonthFirstDay())
75
+ const endDate = ref(getToday())
64
76
 
65
77
  const assets = ref<Array<{ code: string; name: string; balance: number }>>([])
66
78
  const liabilities = ref<Array<{ code: string; name: string; balance: number }>>([])
@@ -68,15 +80,20 @@ const equity = ref<Array<{ code: string; name: string; balance: number }>>([])
68
80
 
69
81
  const loadData = () => {
70
82
  axios.get('/balance-sheet', {
71
- params: { startDate: startDate.value, endDate: endDate.value }
83
+ params: {startDate: startDate.value, endDate: endDate.value}
72
84
  }).then(res => {
73
85
  assets.value = res.data.assets
74
86
  liabilities.value = res.data.liabilities
75
87
  equity.value = res.data.equity
76
88
  })
77
89
  }
90
+
91
+ // 进入页面自动加载一次
92
+ onMounted(() => {
93
+ loadData()
94
+ })
78
95
  </script>
79
96
 
97
+
80
98
  <style scoped>
81
- .page { padding: 46px 0 0; }
82
99
  </style>
@@ -1,61 +1,62 @@
1
1
  <template>
2
- <div class="page">
2
+ <div class="page-container">
3
3
  <van-nav-bar title="凭证记账" fixed />
4
4
 
5
- <van-form class="form" @submit="submit">
5
+ <van-form @submit="submit" class="content">
6
6
  <van-field
7
- v-model="voucher.voucherNo"
8
- label="凭证号"
9
- placeholder="自动生成"
10
- readonly
7
+ v-model="voucher.voucherNo"
8
+ label="凭证号"
9
+ placeholder="自动生成"
10
+ readonly
11
11
  />
12
12
  <van-field
13
- v-model="voucher.voucherDate"
14
- label="日期"
15
- type="date"
16
- value-format="YYYY-MM-DD"
13
+ v-model="voucher.voucherDate"
14
+ label="日期"
15
+ type="date"
16
+ value-format="YYYY-MM-DD"
17
17
  />
18
18
  <van-field
19
- v-model="voucher.description"
20
- label="摘要"
21
- placeholder="输入备注"
19
+ v-model="voucher.description"
20
+ label="摘要"
21
+ placeholder="输入备注"
22
22
  />
23
23
 
24
24
  <div class="title">凭证明细(可添加多笔)</div>
25
25
 
26
26
  <div
27
- v-for="(entry, index) in entries"
28
- :key="index"
29
- class="entry-group"
27
+ v-for="(entry, index) in entries"
28
+ :key="index"
29
+ class="entry-card"
30
30
  >
31
31
  <van-cell-group :border="false">
32
32
  <van-field
33
- :model-value="entry.accountName"
34
- label="科目"
35
- placeholder="点击选择科目"
36
- is-link
37
- @click="openAccountPicker(index)"
33
+ :model-value="entry.accountName"
34
+ label="科目"
35
+ placeholder="点击选择科目"
36
+ is-link
37
+ @click="openAccountPicker(index)"
38
38
  />
39
+ <!-- 改为支持小数 -->
39
40
  <van-field
40
- v-model="entry.debit"
41
- label="借方金额"
42
- type="digit"
43
- @change="calcTotal"
41
+ v-model="entry.debit"
42
+ label="借方金额"
43
+ type="number"
44
+ @change="calcTotal"
44
45
  />
45
46
  <van-field
46
- v-model="entry.credit"
47
- label="贷方金额"
48
- type="digit"
49
- @change="calcTotal"
47
+ v-model="entry.credit"
48
+ label="贷方金额"
49
+ type="number"
50
+ @change="calcTotal"
50
51
  />
51
52
  </van-cell-group>
52
53
 
53
- <div style="text-align: right; padding: 0 10px">
54
+ <div style="text-align: right; padding: 4px 12px">
54
55
  <van-button
55
- size="small"
56
- type="danger"
57
- plain
58
- @click="removeEntry(index)"
56
+ size="small"
57
+ type="danger"
58
+ plain
59
+ @click="removeEntry(index)"
59
60
  >
60
61
  删除此行
61
62
  </van-button>
@@ -63,11 +64,11 @@
63
64
  </div>
64
65
 
65
66
  <van-button
66
- type="primary"
67
- plain
68
- block
69
- @click="addEntry"
70
- style="margin: 10px 0"
67
+ type="primary"
68
+ plain
69
+ block
70
+ @click="addEntry"
71
+ style="margin: 12px 0"
71
72
  >
72
73
  + 添加一笔分录
73
74
  </van-button>
@@ -78,21 +79,46 @@
78
79
  </van-cell-group>
79
80
 
80
81
  <van-button
81
- type="primary"
82
- block
83
- native-type="submit"
84
- style="margin-top: 20px"
82
+ type="primary"
83
+ block
84
+ native-type="submit"
85
+ style="margin-top: 16px"
85
86
  >
86
87
  保存凭证(借贷必须相等)
87
88
  </van-button>
88
89
  </van-form>
89
90
 
90
- <van-popup v-model:show="showAccountPicker" position="bottom" :style="{ height: '40%' }">
91
- <van-picker
92
- :columns="accountList"
93
- @confirm="onSelectAccount"
94
- title="选择科目"
95
- />
91
+ <!-- 左右分栏科目选择弹窗 -->
92
+ <van-popup v-model:show="showAccountPicker" position="center" round class="account-popup">
93
+ <div class="picker-header">
94
+ <span class="picker-title">选择会计科目</span>
95
+ <van-icon name="cross" size="22" @click="showAccountPicker = false" />
96
+ </div>
97
+ <div class="split-picker">
98
+ <!-- 左侧分类 -->
99
+ <div class="left-category">
100
+ <div
101
+ v-for="cate in categoryList"
102
+ :key="cate.id"
103
+ class="cate-item"
104
+ :class="{ active: activeCateId === cate.id }"
105
+ @click="switchCategory(cate.id)"
106
+ >
107
+ {{ cate.name }}
108
+ </div>
109
+ </div>
110
+ <!-- 右侧科目列表 -->
111
+ <div class="right-account">
112
+ <div
113
+ v-for="acc in currentAccountList"
114
+ :key="acc.id"
115
+ class="acc-item"
116
+ @click="selectAccount(acc)"
117
+ >
118
+ {{ acc.text }}
119
+ </div>
120
+ </div>
121
+ </div>
96
122
  </van-popup>
97
123
  </div>
98
124
  </template>
@@ -103,17 +129,37 @@ import { showToast } from 'vant'
103
129
  import axios from 'axios'
104
130
 
105
131
  const showAccountPicker = ref(false)
106
- const accountList = ref([])
107
132
  const currentEntryIndex = ref(0)
133
+ const rawAccountList = ref<any[]>([])
134
+ const categoryList = ref<any[]>([])
135
+ const currentAccountList = ref<any[]>([])
136
+ const activeCateId = ref<number>()
137
+
138
+ const totalDebit = ref(0)
139
+ const totalCredit = ref(0)
140
+
141
+ // 初始化空白凭证
142
+ const initVoucher = () => {
143
+ return {
144
+ voucherNo: 'PZ' + new Date().getTime(),
145
+ voucherDate: getLocalISODate(new Date()),
146
+ description: '',
147
+ status: '草稿',
148
+ ifrsBasis: 'IFRS',
149
+ entries: []
150
+ }
151
+ }
152
+
153
+ // 本地时区日期
154
+ const getLocalISODate = (date: Date) => {
155
+ return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().split('T')[0]
156
+ }
157
+
158
+ const voucher = reactive<Voucher>(initVoucher())
159
+ const entries = ref([
160
+ { accountName: '', accountId: null, debit: '', credit: '', remark: '' }
161
+ ])
108
162
 
109
- const voucher = reactive<Voucher>({
110
- voucherNo: 'PZ' + new Date().getTime(),
111
- voucherDate: new Date().toISOString().slice(0, 10),
112
- description: '',
113
- status: '草稿',
114
- ifrsBasis: 'IFRS',
115
- entries: []
116
- })
117
163
  interface Voucher {
118
164
  voucherNo: string
119
165
  voucherDate: string
@@ -123,40 +169,60 @@ interface Voucher {
123
169
  entries: any[]
124
170
  }
125
171
 
126
-
127
- const entries = ref([
128
- { accountName: '', accountId: null, debit: 0, credit: 0, remark: '' }
129
- ])
130
-
131
- const totalDebit = ref(0)
132
- const totalCredit = ref(0)
133
-
172
+ // 加载科目
134
173
  onMounted(() => {
135
174
  axios.get('/account/accounts').then(res => {
136
- accountList.value = res.data.map((item: { code: string; name: string; id: number }) => ({
175
+ rawAccountList.value = res.data.map((item: { code: string; name: string; id: number; type?: string }) => ({
137
176
  text: `${item.code} ${item.name}`,
138
- value: item.id
177
+ value: item.id,
178
+ category: item.type
139
179
  }))
180
+ splitCategory(rawAccountList.value)
140
181
  })
141
182
  })
142
183
 
143
- const openAccountPicker = (index: number) => {
144
- currentEntryIndex.value = index
145
- showAccountPicker.value = true
184
+ // 分类处理
185
+ const splitCategory = (list: { category: string }[]) => {
186
+ const cateMap = new Map<string, { id: number; name: string }>()
187
+ list.forEach(item => {
188
+ const cateName = item.category || '其他'
189
+ if (!cateMap.has(cateName)) {
190
+ cateMap.set(cateName, { id: cateMap.size + 1, name: cateName })
191
+ }
192
+ })
193
+ categoryList.value = Array.from(cateMap.values())
194
+ if (categoryList.value.length) {
195
+ activeCateId.value = categoryList.value[0].id
196
+ if (activeCateId.value) switchCategory(activeCateId.value)
197
+ }
198
+ }
199
+
200
+ // 切换分类
201
+ const switchCategory = (cateId: number) => {
202
+ activeCateId.value = cateId
203
+ const targetCate = categoryList.value.find(c => c.id === cateId)
204
+ currentAccountList.value = rawAccountList.value.filter(item => item.category === targetCate?.name)
146
205
  }
147
206
 
148
- const onSelectAccount = ({ selectedOptions }: { selectedOptions: any[] }) => {
207
+ // 选择科目
208
+ const selectAccount = (acc: any) => {
149
209
  const entry = entries.value[currentEntryIndex.value]
150
- entry.accountName = selectedOptions[0].text
151
- entry.accountId = selectedOptions[0].value
210
+ entry.accountName = acc.text
211
+ entry.accountId = acc.value
152
212
  showAccountPicker.value = false
153
213
  }
154
214
 
215
+ const openAccountPicker = (index: number) => {
216
+ currentEntryIndex.value = index
217
+ showAccountPicker.value = true
218
+ }
219
+
155
220
  const addEntry = () => {
156
221
  entries.value.push({
157
- accountName: '', accountId: null, debit: 0, credit: 0, remark: ''
222
+ accountName: '', accountId: null, debit: '', credit: '', remark: ''
158
223
  })
159
224
  }
225
+
160
226
  const removeEntry = (index: number) => {
161
227
  if (entries.value.length === 1) {
162
228
  showToast('至少保留一笔分录')
@@ -166,17 +232,21 @@ const removeEntry = (index: number) => {
166
232
  calcTotal()
167
233
  }
168
234
 
235
+ // 精确计算(保留2位小数)
169
236
  const calcTotal = () => {
170
237
  let debit = 0
171
238
  let credit = 0
172
239
  entries.value.forEach(e => {
173
- debit += Number(e.debit || 0)
174
- credit += Number(e.credit || 0)
240
+ debit += Math.round((parseFloat(e.debit) || 0) * 100)
241
+ credit += Math.round((parseFloat(e.credit) || 0) * 100)
175
242
  })
176
- totalDebit.value = debit
177
- totalCredit.value = credit
243
+ totalDebit.value = Math.round(debit) / 100
244
+ totalCredit.value = Math.round(credit) / 100
178
245
  }
179
246
 
247
+ // ==============================================
248
+ // 保存成功 → 自动新建空白凭证(核心功能)
249
+ // ==============================================
180
250
  const submit = () => {
181
251
  calcTotal()
182
252
  if (totalDebit.value !== totalCredit.value) {
@@ -186,13 +256,19 @@ const submit = () => {
186
256
 
187
257
  voucher.entries = entries.value.map(e => ({
188
258
  account: { id: e.accountId },
189
- debit: e.debit,
190
- credit: e.credit,
259
+ debit: parseFloat(e.debit) || 0,
260
+ credit: parseFloat(e.credit) || 0,
191
261
  description: voucher.description
192
262
  }))
193
263
 
194
264
  axios.post('/voucher/create', voucher).then(() => {
195
265
  showToast('✅ 凭证保存成功')
266
+
267
+ // 自动新开空白凭证
268
+ Object.assign(voucher, initVoucher())
269
+ entries.value = [{ accountName: '', accountId: null, debit: '', credit: '', remark: '' }]
270
+ calcTotal()
271
+
196
272
  }).catch(err => {
197
273
  showToast('❌ 保存失败')
198
274
  console.error(err)
@@ -201,21 +277,81 @@ const submit = () => {
201
277
  </script>
202
278
 
203
279
  <style scoped>
204
- .page {
205
- padding: 46px 10px 80px; /* 修复被底部导航遮挡 */
280
+ .page-container {
281
+ position: fixed;
282
+ top: 0;
283
+ left: 0;
284
+ right: 0;
285
+ bottom: 0;
286
+ overflow-y: auto;
287
+ padding-top: 46px;
288
+ box-sizing: border-box;
289
+ }
290
+
291
+ .content {
292
+ padding: 0 12px 20px;
206
293
  }
294
+
207
295
  .title {
208
- padding: 10px;
296
+ padding: 10px 2px;
209
297
  font-weight: bold;
210
- color: #323233;
211
298
  }
212
- .form {
213
- margin-top: 10px;
214
- }
215
- .entry-group {
299
+
300
+ .entry-card {
216
301
  background: #f7f8fa;
217
302
  border-radius: 8px;
218
- padding: 10px;
303
+ padding: 8px;
219
304
  margin-bottom: 10px;
220
305
  }
306
+
307
+ .account-popup {
308
+ width: 90%;
309
+ height: 70vh;
310
+ }
311
+
312
+ .picker-header {
313
+ display: flex;
314
+ justify-content: space-between;
315
+ align-items: center;
316
+ padding: 12px 16px;
317
+ border-bottom: 1px solid #eee;
318
+ }
319
+
320
+ .picker-title {
321
+ font-size: 16px;
322
+ font-weight: 500;
323
+ }
324
+
325
+ .split-picker {
326
+ display: flex;
327
+ height: calc(100% - 48px);
328
+ }
329
+
330
+ .left-category {
331
+ width: 35%;
332
+ background: #f5f5f5;
333
+ overflow-y: auto;
334
+ }
335
+
336
+ .cate-item {
337
+ padding: 14px 10px;
338
+ text-align: center;
339
+ font-size: 14px;
340
+ }
341
+
342
+ .cate-item.active {
343
+ background: #fff;
344
+ color: #1989fa;
345
+ }
346
+
347
+ .right-account {
348
+ flex: 1;
349
+ overflow-y: auto;
350
+ }
351
+
352
+ .acc-item {
353
+ padding: 14px 16px;
354
+ border-bottom: 1px solid #f5f5f5;
355
+ font-size: 14px;
356
+ }
221
357
  </style>
package/version.txt ADDED
@@ -0,0 +1 @@
1
+ 0.1.16