accounting-page 0.1.18 → 0.1.27

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accounting-page",
3
- "version": "0.1.18",
3
+ "version": "0.1.27",
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.17.0",
14
- "vant": "^4.9.24",
15
- "vue": "^3.5.35",
16
- "vue-router": "^5.1.0"
13
+ "axios": "^1.19.0",
14
+ "vant": "^4.10.0",
15
+ "vue": "^3.5.40",
16
+ "vue-router": "^5.2.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/node": "^25.9.2",
20
- "@vitejs/plugin-vue": "^6.0.7",
19
+ "@types/node": "^26.1.2",
20
+ "@vitejs/plugin-vue": "^6.0.8",
21
21
  "@vue/tsconfig": "^0.9.1",
22
22
  "typescript": "~6.0.2",
23
- "vite": "^8.0.16",
24
- "vue-tsc": "^3.3.3"
23
+ "vite": "^8.2.0",
24
+ "vue-tsc": "^3.3.9"
25
25
  }
26
26
  }
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
- <router-view/>
11
- <van-tabbar v-model="active" route fixed>
12
- <van-tabbar-item icon="balance" to="/balance-sheet">资产负债表</van-tabbar-item>
13
- <van-tabbar-item icon="orders" to="/voucher">凭证记账</van-tabbar-item>
14
- </van-tabbar>
15
- </template>
16
-
17
- <style>
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
- <div>
3
- <van-nav-bar title="资产负债" fixed/>
4
- <van-nav-bar title="资产负债" />
5
-
6
- <van-field
7
- v-model="startDate"
8
- label="开始日期"
9
- type="date"
10
- value-format="YYYY-MM-DD"
11
- />
12
- <van-field
13
- v-model="endDate"
14
- label="结束日期"
15
- type="date"
16
- value-format="YYYY-MM-DD"
17
- />
18
-
19
- <van-button
20
- type="primary"
21
- block
22
- @click="loadData">查询报表
23
- </van-button>
24
-
25
- <van-cell-group title="资产">
26
- <van-cell
27
- v-for="item in assets"
28
- :key="item.code"
29
- :title="item.name"
30
- :value="item.balance"
31
- />
32
- </van-cell-group>
33
-
34
- <van-cell-group title="负债">
35
- <van-cell
36
- v-for="item in liabilities"
37
- :key="item.code"
38
- :title="item.name"
39
- :value="item.balance"
40
- />
41
- </van-cell-group>
42
-
43
- <van-cell-group title="权益">
44
- <van-cell
45
- v-for="item in equity"
46
- :key="item.code"
47
- :title="item.name"
48
- :value="item.balance"
49
- />
50
- </van-cell-group>
51
- -->
52
- </div>
53
- </template>
54
-
55
- <script lang="ts" setup>
56
- import {onMounted, ref} from 'vue'
57
- import axios from 'axios'
58
-
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())
76
-
77
- const assets = ref<Array<{ code: string; name: string; balance: number }>>([])
78
- const liabilities = ref<Array<{ code: string; name: string; balance: number }>>([])
79
- const equity = ref<Array<{ code: string; name: string; balance: number }>>([])
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,299 @@
1
- <template>
2
- <div class="page-container">
3
- <van-nav-bar title="凭证记账" fixed />
4
-
5
- <van-form @submit="submit" class="content">
6
- <van-field
7
- v-model="voucher.voucherNo"
8
- label="凭证号"
9
- placeholder="自动生成"
10
- readonly
11
- />
12
- <van-field
13
- v-model="voucher.voucherDate"
14
- label="日期"
15
- type="date"
16
- value-format="YYYY-MM-DD"
17
- />
18
- <van-field
19
- v-model="voucher.description"
20
- label="摘要"
21
- placeholder="输入备注"
22
- />
23
-
24
- <div class="title">凭证明细(可添加多笔)</div>
25
-
26
- <div
27
- v-for="(entry, index) in entries"
28
- :key="index"
29
- class="entry-card"
30
- >
31
- <van-cell-group :border="false">
32
- <van-field
33
- :model-value="entry.accountName"
34
- label="科目"
35
- placeholder="点击选择科目"
36
- is-link
37
- @click="openAccountPicker(index)"
38
- />
39
- <!-- 改为支持小数 -->
40
- <van-field
41
- v-model="entry.debit"
42
- label="借方金额"
43
- type="number"
44
- @change="calcTotal"
45
- />
46
- <van-field
47
- v-model="entry.credit"
48
- label="贷方金额"
49
- type="number"
50
- @change="calcTotal"
51
- />
52
- </van-cell-group>
53
-
54
- <div style="text-align: right; padding: 4px 12px">
55
- <van-button
56
- size="small"
57
- type="danger"
58
- plain
59
- @click="removeEntry(index)"
60
- >
61
- 删除此行
62
- </van-button>
63
- </div>
64
- </div>
65
-
66
- <van-button
67
- type="primary"
68
- plain
69
- block
70
- @click="addEntry"
71
- style="margin: 12px 0"
72
- >
73
- + 添加一笔分录
74
- </van-button>
75
-
76
- <van-cell-group>
77
- <van-cell title="借方合计" :value="totalDebit" />
78
- <van-cell title="贷方合计" :value="totalCredit" />
79
- </van-cell-group>
80
-
81
- <van-button
82
- type="primary"
83
- block
84
- native-type="submit"
85
- style="margin-top: 16px"
86
- >
87
- 保存凭证(借贷必须相等)
88
- </van-button>
89
- </van-form>
90
-
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>
122
- </van-popup>
123
- </div>
124
- </template>
125
-
126
- <script lang="ts" setup>
127
- import { ref, reactive, onMounted } from 'vue'
128
- import { showToast } from 'vant'
129
- import axios from 'axios'
130
-
131
- const showAccountPicker = ref(false)
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
- ])
162
-
163
- interface Voucher {
164
- voucherNo: string
165
- voucherDate: string
166
- description: string
167
- status: string
168
- ifrsBasis: string
169
- entries: any[]
170
- }
171
-
172
- // 加载科目
173
- onMounted(() => {
174
- axios.get('/account/accounts').then(res => {
175
- rawAccountList.value = res.data.map((item: { code: string; name: string; id: number; type?: string }) => ({
176
- text: `${item.code} ${item.name}`,
177
- value: item.id,
178
- category: item.type
179
- }))
180
- splitCategory(rawAccountList.value)
181
- })
182
- })
183
-
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)
205
- }
206
-
207
- // 选择科目
208
- const selectAccount = (acc: any) => {
209
- const entry = entries.value[currentEntryIndex.value]
210
- entry.accountName = acc.text
211
- entry.accountId = acc.value
212
- showAccountPicker.value = false
213
- }
214
-
215
- const openAccountPicker = (index: number) => {
216
- currentEntryIndex.value = index
217
- showAccountPicker.value = true
218
- }
219
-
220
- const addEntry = () => {
221
- entries.value.push({
222
- accountName: '', accountId: null, debit: '', credit: '', remark: ''
223
- })
224
- }
225
-
226
- const removeEntry = (index: number) => {
227
- if (entries.value.length === 1) {
228
- showToast('至少保留一笔分录')
229
- return
230
- }
231
- entries.value.splice(index, 1)
232
- calcTotal()
233
- }
234
-
235
- // 精确计算(保留2位小数)
236
- const calcTotal = () => {
237
- let debit = 0
238
- let credit = 0
239
- entries.value.forEach(e => {
240
- debit += Math.round((parseFloat(e.debit) || 0) * 100)
241
- credit += Math.round((parseFloat(e.credit) || 0) * 100)
242
- })
243
- totalDebit.value = Math.round(debit) / 100
244
- totalCredit.value = Math.round(credit) / 100
245
- }
246
-
247
- // ==============================================
248
- // 保存成功 → 自动新建空白凭证(核心功能)
249
- // ==============================================
250
- const submit = () => {
251
- calcTotal()
252
- if (totalDebit.value !== totalCredit.value) {
253
- showToast('❌ 借贷必须相等!')
254
- return
255
- }
256
-
257
- voucher.entries = entries.value.map(e => ({
258
- account: { id: e.accountId },
259
- debit: parseFloat(e.debit) || 0,
260
- credit: parseFloat(e.credit) || 0,
261
- description: voucher.description
262
- }))
263
-
264
- axios.post('/voucher/create', voucher).then(() => {
265
- showToast('✅ 凭证保存成功')
266
-
267
- // 自动新开空白凭证
268
- Object.assign(voucher, initVoucher())
269
- entries.value = [{ accountName: '', accountId: null, debit: '', credit: '', remark: '' }]
270
- calcTotal()
271
-
272
- }).catch(err => {
273
- showToast('❌ 保存失败')
274
- console.error(err)
275
- })
276
- }
277
- </script>
278
-
279
- <style scoped>
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;
293
- }
294
-
295
- .title {
296
- padding: 10px 2px;
297
- font-weight: bold;
298
- }
299
-
300
- .entry-card {
301
- background: #f7f8fa;
302
- border-radius: 8px;
303
- padding: 8px;
304
- margin-bottom: 10px;
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
- }
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
+ <div v-for="acc in currentAccountList" :key="acc.id" class="acc-item" @click="selectAccount(acc)">
63
+ {{ acc.text }}
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </van-popup>
68
+ </template>
69
+
70
+ <script lang="ts" setup>
71
+ import { ref, reactive, onMounted } from 'vue'
72
+ import { showToast } from 'vant'
73
+ import axios from 'axios'
74
+
75
+ const stickyOffsetTop = Number.parseFloat(
76
+ getComputedStyle(document.documentElement).getPropertyValue('--van-tabbar-height')
77
+ ) || 0
78
+
79
+ const showAccountPicker = ref(false)
80
+ const currentEntryIndex = ref(0)
81
+ const rawAccountList = ref<any[]>([])
82
+ const categoryList = ref<any[]>([])
83
+ const currentAccountList = ref<any[]>([])
84
+ const activeCateId = ref<number>()
85
+
86
+ const totalDebit = ref(0)
87
+ const totalCredit = ref(0)
88
+
89
+ // 初始化空白凭证
90
+ const initVoucher = () => {
91
+ return {
92
+ voucherNo: 'PZ' + new Date().getTime(),
93
+ voucherDate: getLocalISODate(new Date()),
94
+ description: '',
95
+ status: '草稿',
96
+ ifrsBasis: 'IFRS',
97
+ entries: [],
98
+ }
99
+ }
100
+
101
+ // 本地时区日期
102
+ const getLocalISODate = (date: Date) => {
103
+ return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().split('T')[0]
104
+ }
105
+
106
+ const list = reactive<{
107
+ indexList: number[];
108
+ }>({
109
+ indexList: [1, 2, 3]
110
+ })
111
+
112
+ const voucher = reactive<Voucher>(initVoucher())
113
+ const entries = ref([
114
+ { accountName: '', accountId: null, debit: '', credit: '', remark: '' }
115
+ ])
116
+
117
+ interface Voucher {
118
+ voucherNo: string
119
+ voucherDate: string
120
+ description: string
121
+ status: string
122
+ ifrsBasis: string
123
+ entries: any[]
124
+ }
125
+
126
+ // 加载科目
127
+ onMounted(() => {
128
+
129
+ axios.get('/account/accounts').then(res => {
130
+ rawAccountList.value = res.data.map((item: { code: string; name: string; id: number; type?: string }) => ({
131
+ text: `${item.code} ${item.name}`,
132
+ value: item.id,
133
+ category: item.type
134
+ }))
135
+ splitCategory(rawAccountList.value)
136
+ })
137
+ })
138
+
139
+ // 分类处理
140
+ const splitCategory = (list: { category: string }[]) => {
141
+ const cateMap = new Map<string, { id: number; name: string }>()
142
+ list.forEach(item => {
143
+ const cateName = item.category || '其他'
144
+ if (!cateMap.has(cateName)) {
145
+ cateMap.set(cateName, { id: cateMap.size + 1, name: cateName })
146
+ }
147
+ })
148
+ categoryList.value = Array.from(cateMap.values())
149
+ if (categoryList.value.length) {
150
+ activeCateId.value = categoryList.value[0].id
151
+ if (activeCateId.value) switchCategory(activeCateId.value)
152
+ }
153
+ }
154
+
155
+ // 切换分类
156
+ const switchCategory = (cateId: number) => {
157
+ activeCateId.value = cateId
158
+ const targetCate = categoryList.value.find(c => c.id === cateId)
159
+ currentAccountList.value = rawAccountList.value.filter(item => item.category === targetCate?.name)
160
+ }
161
+
162
+ // 选择科目
163
+ const selectAccount = (acc: any) => {
164
+ const entry = entries.value[currentEntryIndex.value]
165
+ entry.accountName = acc.text
166
+ entry.accountId = acc.value
167
+ showAccountPicker.value = false
168
+ }
169
+
170
+ const openAccountPicker = (index: number) => {
171
+ currentEntryIndex.value = index
172
+ showAccountPicker.value = true
173
+ }
174
+
175
+ const addEntry = () => {
176
+ entries.value.push({
177
+ accountName: '', accountId: null, debit: '', credit: '', remark: ''
178
+ })
179
+ }
180
+
181
+ const removeEntry = (index: number) => {
182
+ if (entries.value.length === 1) {
183
+ showToast('至少保留一笔分录')
184
+ return
185
+ }
186
+ entries.value.splice(index, 1)
187
+ calcTotal()
188
+ }
189
+
190
+ // 精确计算(保留2位小数)
191
+ const calcTotal = () => {
192
+ let debit = 0
193
+ let credit = 0
194
+ entries.value.forEach(e => {
195
+ debit += Math.round((parseFloat(e.debit) || 0) * 100)
196
+ credit += Math.round((parseFloat(e.credit) || 0) * 100)
197
+ })
198
+ totalDebit.value = Math.round(debit) / 100
199
+ totalCredit.value = Math.round(credit) / 100
200
+ }
201
+
202
+ // ==============================================
203
+ // 保存成功 → 自动新建空白凭证(核心功能)
204
+ // ==============================================
205
+ const submit = () => {
206
+ calcTotal()
207
+ if (totalDebit.value !== totalCredit.value) {
208
+ showToast('❌ 借贷必须相等!')
209
+ return
210
+ }
211
+
212
+ voucher.entries = entries.value.map(e => ({
213
+ account: { id: e.accountId },
214
+ debit: parseFloat(e.debit) || 0,
215
+ credit: parseFloat(e.credit) || 0,
216
+ description: voucher.description
217
+ }))
218
+
219
+ axios.post('/voucher/create', voucher).then(() => {
220
+ showToast('✅ 凭证保存成功')
221
+
222
+ // 自动新开空白凭证
223
+ Object.assign(voucher, initVoucher())
224
+ entries.value = [{ accountName: '', accountId: null, debit: '', credit: '', remark: '' }]
225
+ calcTotal()
226
+
227
+ }).catch(err => {
228
+ showToast('❌ 保存失败')
229
+ console.error(err)
230
+ })
231
+ }
232
+ </script>
233
+
234
+ <style scoped>
235
+
236
+
237
+ .title {
238
+ padding: 10px 10px;
239
+ font-weight: bold;
240
+ }
241
+
242
+ .entry-card {
243
+ background: #f7f8fa;
244
+ border-radius: 8px;
245
+ padding: 8px;
246
+ margin-bottom: 10px;
247
+ }
248
+
249
+ .account-popup {
250
+ width: 90%;
251
+ height: 70vh;
252
+ }
253
+
254
+ .picker-header {
255
+ display: flex;
256
+ justify-content: space-between;
257
+ align-items: center;
258
+ padding: 12px 16px;
259
+ border-bottom: 1px solid #eee;
260
+ }
261
+
262
+ .picker-title {
263
+ font-size: 16px;
264
+ font-weight: 500;
265
+ }
266
+
267
+ .split-picker {
268
+ display: flex;
269
+ height: calc(100% - 48px);
270
+ }
271
+
272
+ .left-category {
273
+ width: 35%;
274
+ background: #f5f5f5;
275
+ overflow-y: auto;
276
+ }
277
+
278
+ .cate-item {
279
+ padding: 14px 10px;
280
+ text-align: center;
281
+ font-size: 14px;
282
+ }
283
+
284
+ .cate-item.active {
285
+ background: #fff;
286
+ color: #1989fa;
287
+ }
288
+
289
+ .right-account {
290
+ flex: 1;
291
+ overflow-y: auto;
292
+ }
293
+
294
+ .acc-item {
295
+ padding: 14px 16px;
296
+ border-bottom: 1px solid #f5f5f5;
297
+ font-size: 14px;
298
+ }
357
299
  </style>