accounting-page 0.1.26 → 0.1.28
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 +1 -10
- package/package.json +3 -3
- package/src/App.vue +20 -17
- package/src/views/BalanceSheet.vue +98 -98
- package/src/views/VoucherCreate.vue +345 -356
package/README.md
CHANGED
|
@@ -5,13 +5,4 @@ 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).
|
|
9
|
-
|
|
10
|
-
## TODO
|
|
11
|
-
|
|
12
|
-
- [X] 统计周期:本月首日—今日,报表类型选择资产负债表
|
|
13
|
-
- [X] 科目选择框设置为全屏/最大化
|
|
14
|
-
- [X] 凭证保存成功后,自动新建空白凭证窗口
|
|
15
|
-
- [ ] 科目选择框新增搜索功能(暂时搁置)
|
|
16
|
-
- [X] 金额支持小数点,计算精度保留到分
|
|
17
|
-
- [ ] 输入框点击后,无需聚焦。
|
|
8
|
+
the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "accounting-page",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"axios": "^1.19.0",
|
|
14
14
|
"vant": "^4.10.0",
|
|
15
|
-
"vue": "^3.5.
|
|
15
|
+
"vue": "^3.5.41",
|
|
16
16
|
"vue-router": "^5.2.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^26.1.2",
|
|
20
20
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
21
21
|
"@vue/tsconfig": "^0.9.1",
|
|
22
|
-
"typescript": "~6.0.
|
|
22
|
+
"typescript": "~6.0.3",
|
|
23
23
|
"vite": "^8.2.0",
|
|
24
24
|
"vue-tsc": "^3.3.9"
|
|
25
25
|
}
|
package/src/App.vue
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import {ref} from 'vue'
|
|
3
|
-
import {Tabbar as VanTabbar, TabbarItem as VanTabbarItem} from 'vant'
|
|
4
|
-
import {RouterView} from "vue-router";
|
|
5
|
-
|
|
6
|
-
const active = ref(0)
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<template>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {ref} from 'vue'
|
|
3
|
+
import {Tabbar as VanTabbar, TabbarItem as VanTabbarItem} from 'vant'
|
|
4
|
+
import {RouterView} from "vue-router";
|
|
5
|
+
|
|
6
|
+
const active = ref(0)
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<router-view />
|
|
11
|
+
|
|
12
|
+
<div>
|
|
13
|
+
<van-tabbar v-model="active" route placeholder>
|
|
14
|
+
<van-tabbar-item icon="balance" to="/balance-sheet">资产负债表</van-tabbar-item>
|
|
15
|
+
<van-tabbar-item icon="orders" to="/voucher">凭证记账</van-tabbar-item>
|
|
16
|
+
</van-tabbar>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style>
|
|
18
21
|
</style>
|
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<
|
|
3
|
-
<van-nav-bar title="
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const loadData = () => {
|
|
82
|
-
axios.get('/balance-sheet', {
|
|
83
|
-
params: {startDate: startDate.value, endDate: endDate.value}
|
|
84
|
-
}).then(res => {
|
|
85
|
-
assets.value = res.data.assets
|
|
86
|
-
liabilities.value = res.data.liabilities
|
|
87
|
-
equity.value = res.data.equity
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 进入页面自动加载一次
|
|
92
|
-
onMounted(() => {
|
|
93
|
-
loadData()
|
|
94
|
-
})
|
|
95
|
-
</script>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<style scoped>
|
|
1
|
+
<template>
|
|
2
|
+
<van-sticky>
|
|
3
|
+
<van-nav-bar title="资产负债表"/>
|
|
4
|
+
</van-sticky>
|
|
5
|
+
<van-field
|
|
6
|
+
v-model="startDate"
|
|
7
|
+
label="开始日期"
|
|
8
|
+
type="date"
|
|
9
|
+
value-format="YYYY-MM-DD"
|
|
10
|
+
/>
|
|
11
|
+
<van-field
|
|
12
|
+
v-model="endDate"
|
|
13
|
+
label="结束日期"
|
|
14
|
+
type="date"
|
|
15
|
+
value-format="YYYY-MM-DD"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<van-button
|
|
19
|
+
type="primary"
|
|
20
|
+
block
|
|
21
|
+
@click="loadData">查询报表
|
|
22
|
+
</van-button>
|
|
23
|
+
|
|
24
|
+
<van-index-bar :index-list="indexList" :sticky-offset-top="stickyOffsetTop">
|
|
25
|
+
<van-index-anchor index="资产">资产</van-index-anchor>
|
|
26
|
+
<van-cell
|
|
27
|
+
v-for="item in assets"
|
|
28
|
+
:key="item.code"
|
|
29
|
+
:title="item.name"
|
|
30
|
+
:value="item.balance"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<van-index-anchor index="负债">负债</van-index-anchor>
|
|
34
|
+
<van-cell
|
|
35
|
+
v-for="item in liabilities"
|
|
36
|
+
:key="item.code"
|
|
37
|
+
:title="item.name"
|
|
38
|
+
:value="item.balance"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<van-index-anchor index="权益">权益</van-index-anchor>
|
|
42
|
+
<van-cell
|
|
43
|
+
v-for="item in equity"
|
|
44
|
+
:key="item.code"
|
|
45
|
+
:title="item.name"
|
|
46
|
+
:value="item.balance"
|
|
47
|
+
/>
|
|
48
|
+
</van-index-bar>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script lang="ts" setup>
|
|
52
|
+
import {onMounted, ref} from 'vue'
|
|
53
|
+
import axios from 'axios'
|
|
54
|
+
|
|
55
|
+
// 自动获取:本月首日
|
|
56
|
+
const getMonthFirstDay = () => {
|
|
57
|
+
// 传入 Date,返回【当前系统时区】的 toISOString 格式日期(YYYY-MM-DD)
|
|
58
|
+
let now =new Date()
|
|
59
|
+
let date =new Date(now.getFullYear(), now.getMonth() ,1)
|
|
60
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
|
61
|
+
.toISOString().split('T')[0];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 自动获取:今日
|
|
65
|
+
const getToday = () => {
|
|
66
|
+
return new Date().toISOString().slice(0, 10)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 默认值:本月首日 — 今日
|
|
70
|
+
const startDate = ref(getMonthFirstDay())
|
|
71
|
+
const endDate = ref(getToday())
|
|
72
|
+
|
|
73
|
+
const assets = ref<Array<{ code: string; name: string; balance: number }>>([])
|
|
74
|
+
const liabilities = ref<Array<{ code: string; name: string; balance: number }>>([])
|
|
75
|
+
const equity = ref<Array<{ code: string; name: string; balance: number }>>([])
|
|
76
|
+
const indexList = ['资产', '负债', '权益']
|
|
77
|
+
const stickyOffsetTop = Number.parseFloat(
|
|
78
|
+
getComputedStyle(document.documentElement).getPropertyValue('--van-tabbar-height')
|
|
79
|
+
) || 0
|
|
80
|
+
|
|
81
|
+
const loadData = () => {
|
|
82
|
+
axios.get('/balance-sheet', {
|
|
83
|
+
params: {startDate: startDate.value, endDate: endDate.value}
|
|
84
|
+
}).then(res => {
|
|
85
|
+
assets.value = res.data.assets
|
|
86
|
+
liabilities.value = res.data.liabilities
|
|
87
|
+
equity.value = res.data.equity
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 进入页面自动加载一次
|
|
92
|
+
onMounted(() => {
|
|
93
|
+
loadData()
|
|
94
|
+
})
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
<style scoped>
|
|
99
99
|
</style>
|
|
@@ -1,357 +1,346 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<
|
|
3
|
-
<van-nav-bar title="凭证记账"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
readonly
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<van-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
padding:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
background: #
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
}
|
|
1
|
+
<template>
|
|
2
|
+
<van-sticky>
|
|
3
|
+
<van-nav-bar title="凭证记账" />
|
|
4
|
+
</van-sticky>
|
|
5
|
+
|
|
6
|
+
<van-form @submit="submit">
|
|
7
|
+
<van-index-bar :index-list="list.indexList" :sticky-offset-top="stickyOffsetTop">
|
|
8
|
+
<van-index-anchor index="1">凭证基本信息</van-index-anchor>
|
|
9
|
+
|
|
10
|
+
<van-field v-model="voucher.voucherNo" label="凭证号" placeholder="自动生成" readonly />
|
|
11
|
+
<van-field v-model="voucher.voucherDate" label="日期" type="date" value-format="YYYY-MM-DD" />
|
|
12
|
+
<van-field v-model="voucher.description" label="摘要" placeholder="输入备注" />
|
|
13
|
+
|
|
14
|
+
<van-index-anchor index="2">凭证明细(可添加多笔)</van-index-anchor>
|
|
15
|
+
<div v-for="(entry, index) in entries" :key="index" class="entry-card">
|
|
16
|
+
<van-cell-group :border="false">
|
|
17
|
+
<van-field :model-value="entry.accountName" label="科目" placeholder="点击选择科目" is-link
|
|
18
|
+
@click="openAccountPicker(index)" />
|
|
19
|
+
<van-field v-model="entry.debit" label="借方金额" type="number" @change="calcTotal" />
|
|
20
|
+
<van-field v-model="entry.credit" label="贷方金额" type="number" @change="calcTotal" />
|
|
21
|
+
</van-cell-group>
|
|
22
|
+
<van-row justify="end">
|
|
23
|
+
<van-col span="6">
|
|
24
|
+
<van-button size="small" type="danger" plain @click="removeEntry(index)">
|
|
25
|
+
删除此行
|
|
26
|
+
</van-button>
|
|
27
|
+
</van-col>
|
|
28
|
+
</van-row>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<van-button type="primary" plain block @click="addEntry" style="margin: 12px 0">
|
|
32
|
+
+ 添加一笔分录
|
|
33
|
+
</van-button>
|
|
34
|
+
<van-index-anchor index="3">凭证统计</van-index-anchor>
|
|
35
|
+
<van-cell-group style="margin-bottom: 12px">
|
|
36
|
+
<van-cell title="借方合计" :value="totalDebit" />
|
|
37
|
+
<van-cell title="贷方合计" :value="totalCredit" />
|
|
38
|
+
</van-cell-group>
|
|
39
|
+
|
|
40
|
+
<van-button type="primary" block native-type="submit" style="margin: 0 0 12px">保存凭证(借贷必须相等)</van-button>
|
|
41
|
+
</van-index-bar>
|
|
42
|
+
|
|
43
|
+
</van-form>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<!-- 左右分栏科目选择弹窗 -->
|
|
47
|
+
<van-popup v-model:show="showAccountPicker" position="center" round class="account-popup">
|
|
48
|
+
<div class="picker-header">
|
|
49
|
+
<span class="picker-title">选择会计科目</span>
|
|
50
|
+
<van-icon name="cross" size="22" @click="showAccountPicker = false" />
|
|
51
|
+
</div>
|
|
52
|
+
<div class="split-picker">
|
|
53
|
+
<!-- 左侧分类 -->
|
|
54
|
+
<div class="left-category">
|
|
55
|
+
<div v-for="cate in categoryList" :key="cate.id" class="cate-item" :class="{ active: activeCateId === cate.id }"
|
|
56
|
+
@click="switchCategory(cate.id)">
|
|
57
|
+
{{ cate.name }}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<!-- 右侧科目列表 -->
|
|
61
|
+
<div class="right-account">
|
|
62
|
+
<van-field v-model="accountSearch" placeholder="搜索科目/编码" clearable style="margin:8px 12px;" />
|
|
63
|
+
<div v-for="acc in filteredAccountList" :key="acc.value" class="acc-item" @click="selectAccount(acc)">
|
|
64
|
+
{{ acc.text }}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</van-popup>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script lang="ts" setup>
|
|
72
|
+
import { ref, reactive, onMounted, computed } from 'vue'
|
|
73
|
+
import { showToast } from 'vant'
|
|
74
|
+
import axios from 'axios'
|
|
75
|
+
|
|
76
|
+
const stickyOffsetTop = Number.parseFloat(
|
|
77
|
+
getComputedStyle(document.documentElement).getPropertyValue('--van-tabbar-height')
|
|
78
|
+
) || 0
|
|
79
|
+
|
|
80
|
+
const showAccountPicker = ref(false)
|
|
81
|
+
const currentEntryIndex = ref(0)
|
|
82
|
+
const rawAccountList = ref<any[]>([])
|
|
83
|
+
const categoryList = ref<any[]>([])
|
|
84
|
+
const currentAccountList = ref<any[]>([])
|
|
85
|
+
const activeCateId = ref<number>()
|
|
86
|
+
const accountSearch = ref('')
|
|
87
|
+
|
|
88
|
+
const totalDebit = ref(0)
|
|
89
|
+
const totalCredit = ref(0)
|
|
90
|
+
|
|
91
|
+
// 初始化空白凭证
|
|
92
|
+
const initVoucher = () => {
|
|
93
|
+
return {
|
|
94
|
+
voucherNo: 'PZ' + new Date().getTime(),
|
|
95
|
+
voucherDate: getLocalISODate(new Date()),
|
|
96
|
+
description: '',
|
|
97
|
+
status: '草稿',
|
|
98
|
+
ifrsBasis: 'IFRS',
|
|
99
|
+
entries: [],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 本地时区日期
|
|
104
|
+
const getLocalISODate = (date: Date) => {
|
|
105
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().split('T')[0]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const list = reactive<{
|
|
109
|
+
indexList: number[];
|
|
110
|
+
}>({
|
|
111
|
+
indexList: [1, 2, 3]
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const voucher = reactive<Voucher>(initVoucher())
|
|
115
|
+
const entries = ref([
|
|
116
|
+
{ accountName: '', accountId: null, debit: '', credit: '', remark: '' },
|
|
117
|
+
{ accountName: '', accountId: null, debit: '', credit: '', remark: '' }
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
interface Voucher {
|
|
121
|
+
voucherNo: string
|
|
122
|
+
voucherDate: string
|
|
123
|
+
description: string
|
|
124
|
+
status: string
|
|
125
|
+
ifrsBasis: string
|
|
126
|
+
entries: any[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 加载科目
|
|
130
|
+
onMounted(() => {
|
|
131
|
+
|
|
132
|
+
axios.get('/account/accounts').then(res => {
|
|
133
|
+
rawAccountList.value = res.data.map((item: { code: string; name: string; id: number; type?: string }) => ({
|
|
134
|
+
text: `${item.code} ${item.name}`,
|
|
135
|
+
value: item.id,
|
|
136
|
+
category: item.type
|
|
137
|
+
}))
|
|
138
|
+
splitCategory(rawAccountList.value)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// 分类处理
|
|
143
|
+
const splitCategory = (list: { category: string }[]) => {
|
|
144
|
+
const cateMap = new Map<string, { id: number; name: string }>()
|
|
145
|
+
list.forEach(item => {
|
|
146
|
+
const cateName = item.category || '其他'
|
|
147
|
+
if (!cateMap.has(cateName)) {
|
|
148
|
+
cateMap.set(cateName, { id: cateMap.size + 1, name: cateName })
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
categoryList.value = Array.from(cateMap.values())
|
|
152
|
+
if (categoryList.value.length) {
|
|
153
|
+
activeCateId.value = categoryList.value[0].id
|
|
154
|
+
if (activeCateId.value) switchCategory(activeCateId.value)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 切换分类
|
|
159
|
+
const switchCategory = (cateId: number) => {
|
|
160
|
+
activeCateId.value = cateId
|
|
161
|
+
const targetCate = categoryList.value.find(c => c.id === cateId)
|
|
162
|
+
currentAccountList.value = rawAccountList.value.filter(item => item.category === targetCate?.name)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 根据搜索词计算显示的科目列表:
|
|
166
|
+
// - 若有搜索词,则在全部科目中全局匹配(code+name)
|
|
167
|
+
// - 否则显示当前分类的科目
|
|
168
|
+
const filteredAccountList = computed(() => {
|
|
169
|
+
const q = accountSearch.value.trim().toLowerCase()
|
|
170
|
+
if (!q) return currentAccountList.value
|
|
171
|
+
return rawAccountList.value.filter(item => item.text.toLowerCase().includes(q))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// 选择科目
|
|
175
|
+
const selectAccount = (acc: any) => {
|
|
176
|
+
const idx = currentEntryIndex.value
|
|
177
|
+
const entry = entries.value[idx]
|
|
178
|
+
entry.accountName = acc.text
|
|
179
|
+
entry.accountId = acc.value
|
|
180
|
+
showAccountPicker.value = false
|
|
181
|
+
|
|
182
|
+
// 自动平衡逻辑(仅在两笔分录时启用):
|
|
183
|
+
// 当一方已有借/贷金额且另一方金额为空时,选完科目后自动把数值填到对方相反栏位
|
|
184
|
+
if (entries.value.length === 2) {
|
|
185
|
+
const otherIdx = idx === 0 ? 1 : 0
|
|
186
|
+
const other = entries.value[otherIdx]
|
|
187
|
+
|
|
188
|
+
// 优先使用已有的对方金额去填当前项的相反栏位
|
|
189
|
+
if (other.debit !== '' && (entry.credit === '' || entry.credit == null)) {
|
|
190
|
+
entry.credit = other.debit
|
|
191
|
+
calcTotal()
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (other.credit !== '' && (entry.debit === '' || entry.debit == null)) {
|
|
195
|
+
entry.debit = other.credit
|
|
196
|
+
calcTotal()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 否则如果当前项已有金额,则填充另一项的相反栏位
|
|
201
|
+
if (entry.debit !== '' && (other.credit === '' || other.credit == null)) {
|
|
202
|
+
other.credit = entry.debit
|
|
203
|
+
calcTotal()
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (entry.credit !== '' && (other.debit === '' || other.debit == null)) {
|
|
207
|
+
other.debit = entry.credit
|
|
208
|
+
calcTotal()
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const openAccountPicker = (index: number) => {
|
|
215
|
+
currentEntryIndex.value = index
|
|
216
|
+
showAccountPicker.value = true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const addEntry = () => {
|
|
220
|
+
entries.value.push({
|
|
221
|
+
accountName: '', accountId: null, debit: '', credit: '', remark: ''
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const removeEntry = (index: number) => {
|
|
226
|
+
if (entries.value.length === 1) {
|
|
227
|
+
showToast('至少保留一笔分录')
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
entries.value.splice(index, 1)
|
|
231
|
+
calcTotal()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 精确计算(保留2位小数)
|
|
235
|
+
const calcTotal = () => {
|
|
236
|
+
let debit = 0
|
|
237
|
+
let credit = 0
|
|
238
|
+
entries.value.forEach(e => {
|
|
239
|
+
debit += Math.round((parseFloat(e.debit) || 0) * 100)
|
|
240
|
+
credit += Math.round((parseFloat(e.credit) || 0) * 100)
|
|
241
|
+
})
|
|
242
|
+
totalDebit.value = Math.round(debit) / 100
|
|
243
|
+
totalCredit.value = Math.round(credit) / 100
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ==============================================
|
|
247
|
+
// 保存成功 → 自动新建空白凭证(核心功能)
|
|
248
|
+
// ==============================================
|
|
249
|
+
const submit = () => {
|
|
250
|
+
calcTotal()
|
|
251
|
+
if (totalDebit.value !== totalCredit.value) {
|
|
252
|
+
showToast('❌ 借贷必须相等!')
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
voucher.entries = entries.value.map(e => ({
|
|
257
|
+
account: { id: e.accountId },
|
|
258
|
+
debit: parseFloat(e.debit) || 0,
|
|
259
|
+
credit: parseFloat(e.credit) || 0,
|
|
260
|
+
description: voucher.description
|
|
261
|
+
}))
|
|
262
|
+
|
|
263
|
+
axios.post('/voucher/create', voucher).then(() => {
|
|
264
|
+
showToast('✅ 凭证保存成功')
|
|
265
|
+
|
|
266
|
+
// 自动新开空白凭证(两笔起步)
|
|
267
|
+
Object.assign(voucher, initVoucher())
|
|
268
|
+
entries.value = [
|
|
269
|
+
{ accountName: '', accountId: null, debit: '', credit: '', remark: '' },
|
|
270
|
+
{ accountName: '', accountId: null, debit: '', credit: '', remark: '' }
|
|
271
|
+
]
|
|
272
|
+
calcTotal()
|
|
273
|
+
|
|
274
|
+
}).catch(err => {
|
|
275
|
+
showToast('❌ 保存失败')
|
|
276
|
+
console.error(err)
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
</script>
|
|
280
|
+
|
|
281
|
+
<style scoped>
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
.title {
|
|
285
|
+
padding: 10px 10px;
|
|
286
|
+
font-weight: bold;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.entry-card {
|
|
290
|
+
background: #f7f8fa;
|
|
291
|
+
border-radius: 8px;
|
|
292
|
+
padding: 8px;
|
|
293
|
+
margin-bottom: 10px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.account-popup {
|
|
297
|
+
width: 90%;
|
|
298
|
+
height: 70vh;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.picker-header {
|
|
302
|
+
display: flex;
|
|
303
|
+
justify-content: space-between;
|
|
304
|
+
align-items: center;
|
|
305
|
+
padding: 12px 16px;
|
|
306
|
+
border-bottom: 1px solid #eee;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.picker-title {
|
|
310
|
+
font-size: 16px;
|
|
311
|
+
font-weight: 500;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.split-picker {
|
|
315
|
+
display: flex;
|
|
316
|
+
height: calc(100% - 48px);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.left-category {
|
|
320
|
+
width: 35%;
|
|
321
|
+
background: #f5f5f5;
|
|
322
|
+
overflow-y: auto;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.cate-item {
|
|
326
|
+
padding: 14px 10px;
|
|
327
|
+
text-align: center;
|
|
328
|
+
font-size: 14px;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.cate-item.active {
|
|
332
|
+
background: #fff;
|
|
333
|
+
color: #1989fa;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.right-account {
|
|
337
|
+
flex: 1;
|
|
338
|
+
overflow-y: auto;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.acc-item {
|
|
342
|
+
padding: 14px 16px;
|
|
343
|
+
border-bottom: 1px solid #f5f5f5;
|
|
344
|
+
font-size: 14px;
|
|
345
|
+
}
|
|
357
346
|
</style>
|