2 天以前 14363b1ae7cb0d730158ec8dfbee55a85b2fc09f
feat(financial): 实现财务模块数据接口联调

- 科目明细账(detailLedger.vue)集成真实接口,移除模拟数据
- 总账(generalLedger.vue)集成真实接口,移除模拟数据
- 固定资产(fixedAssets.vue)集成完整CRUD接口,移除模拟数据
- 凭证管理(index.vue)集成凭证接口,优化科目选择功能
- 科目管理(index.vue)增加父子科目关系展示和子科目新增功能
- 所有财务模块实现动态科目加载和真实数据交互
已添加5个文件
已修改6个文件
1287 ■■■■ 文件已修改
FINANCIAL_MANAGEMENT_BACKEND_SPEC.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/fixedAsset.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/intangibleAsset.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/ledger.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/voucher.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/fixedAssets.vue 139 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/intangibleAssets.vue 147 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/generalLedger/index.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/detailLedger.vue 103 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/generalLedger.vue 107 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/index.vue 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
FINANCIAL_MANAGEMENT_BACKEND_SPEC.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,233 @@
# è´¢åŠ¡ç®¡ç†åŽç«¯æ–‡æ¡£ï¼ˆä»…è´Ÿè´£æ¨¡å—ï¼‰
更新时间:2026-05-12
适用范围(仅以下 6 ä¸ªæ¨¡å—):
1. å›ºå®šèµ„产(`/financial/fixed-assets`)
2. æ— å½¢èµ„产(`/financial/intangible-assets`)
3. æ€»è´¦ç§‘目(`/financial/general-ledger`)
4. å‡­è¯ï¼ˆ`/financial/voucher`)
5. ç§‘目总账(`/financial/voucher-general-ledger`)
6. ç§‘目明细账(`/financial/voucher-detail-ledger`)
---
## 1. ç»Ÿä¸€çº¦å®š
### 1.1 å“åº”结构
```json
{
  "code": 200,
  "msg": "success",
  "data": {}
}
```
### 1.2 åˆ†é¡µç»“构(如果是分页接口)
请求参数建议:
- `current`(页码)
- `size`(每页条数)
响应建议:
```json
{
  "code": 200,
  "data": {
    "records": [],
    "total": 0
  }
}
```
### 1.3 é‡‘额与精度
- é‡‘额字段建议 `decimal(18,2)`。
- å‰åŽç«¯ç»Ÿä¸€ä¿ç•™ä¸¤ä½å°æ•°ã€‚
---
## 2. æ¨¡å—一:总账科目(已接真实 API)
前端文件:`src/views/financialManagement/generalLedger/index.vue`
API æ–‡ä»¶ï¼š`src/api/financialManagement/accountSubject.js`
### 2.1 æŽ¥å£çŽ°çŠ¶
- `GET /accountSubject/list`
- `POST /accountSubject/add`
- `PUT /accountSubject/edit`
- `DELETE /accountSubject/remove/{ids}`
- `POST /accountSubject/export`
### 2.2 å­—段模型
- `id`
- `subjectCode`(科目编码)
- `subjectName`(科目名称)
- `subjectType`(科目类型)
- `balanceDirection`(余额方向:借方/贷方)
- `status`(0 å¯ç”¨ï¼Œ1 ç¦ç”¨ï¼‰
- `remark`
### 2.3 ä¸šåŠ¡è§„åˆ™
- `subjectCode`、`subjectName`、`subjectType` å¿…填。
- åˆ é™¤éœ€è¦åšå¼•用校验(若已被凭证分录引用,不允许删除)。
---
## 3. æ¨¡å—二:固定资产(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/assets/fixedAssets.vue`
### 3.1 å»ºè®®æŽ¥å£
- `GET /financial/fixedAsset/page`
- `POST /financial/fixedAsset/add`
- `PUT /financial/fixedAsset/update`
- `DELETE /financial/fixedAsset/delete`
- `POST /financial/fixedAsset/depreciate`(按月计提)
### 3.2 å­—段模型
- `id, assetCode, assetName, category, specification`
- `purchaseDate, originalValue, usefulLife, residualRate`
- `accumulatedDepreciation, netValue`
- `location, department, keeper, status, remark`
### 3.3 æ ¸å¿ƒå…¬å¼ï¼ˆå¿…须一致)
- `monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)`
- `accumulatedDepreciation += monthlyDepreciation`
- `netValue = originalValue - accumulatedDepreciation`
### 3.4 çŠ¶æ€å»ºè®®
- `in_use`(在用)
- `idle`(闲置)
- `repair`(维修中)
- `scrapped`(报废)
---
## 4. æ¨¡å—三:无形资产(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/assets/intangibleAssets.vue`
### 4.1 å»ºè®®æŽ¥å£
- `GET /financial/intangibleAsset/page`
- `POST /financial/intangibleAsset/add`
- `PUT /financial/intangibleAsset/update`
- `DELETE /financial/intangibleAsset/delete`
- `POST /financial/intangibleAsset/amortize`(按月摊销)
### 4.2 å­—段模型
- `id, assetCode, assetName, category, certificateNo`
- `acquisitionDate, originalValue, amortizationPeriod, residualRate`
- `accumulatedAmortization, netValue`
- `validityDate, status, description, remark`
### 4.3 æ ¸å¿ƒå…¬å¼ï¼ˆå¿…须一致)
- `monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)`
- `accumulatedAmortization += monthlyAmortization`
- `netValue = originalValue - accumulatedAmortization`
- å½“ `netValue <= 0`:
  - `netValue = 0`
  - `status = amortized`
### 4.4 çŠ¶æ€å»ºè®®
- `in_use`(在用)
- `expired`(到期)
- `amortized`(已摊销完)
---
## 5. æ¨¡å—四:凭证(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/voucher/index.vue`
### 5.1 å»ºè®®æŽ¥å£
- `GET /financial/voucher/page`
- `POST /financial/voucher/add`
- `PUT /financial/voucher/update`
- `POST /financial/voucher/post`(过账)
- `POST /financial/voucher/cancel`(作废)
- `GET /financial/voucher/detail/{id}`
### 5.2 ä¸»è¡¨å­—段
- `id, voucherNo, voucherDate, summary`
- `debit, credit, creator, status, attachmentCount, remark`
### 5.3 åˆ†å½•字段
- `subjectCode, subjectName, summary, debit, credit`
### 5.4 å…³é”®æ ¡éªŒ
- åˆ†å½•至少一条有效行(科目不空,且借方或贷方 > 0)。
- å€Ÿè´·å¹³è¡¡ï¼š`sum(debit) == sum(credit)` ä¸” > 0,不满足禁止保存。
### 5.5 çŠ¶æ€æµè½¬
- `unposted -> posted`
- `unposted -> cancelled`
---
## 6. æ¨¡å—五:科目总账(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/voucher/generalLedger.vue`
### 6.1 å»ºè®®æŽ¥å£
- `GET /financial/ledger/general`
### 6.2 è¯·æ±‚参数
- `subjectCode`(末级或指定科目)
- `startMonth`(YYYY-MM)
- `endMonth`(YYYY-MM)
### 6.3 å“åº”字段
- `date, voucherNo, summary`
- `debit, credit, direction, balance`
### 6.4 è§„则
- ä»…在选择科目后返回数据。
- æ”¯æŒâ€œæœŸåˆä½™é¢ / æœ¬æœˆåˆè®¡ / æœ¬å¹´ç´¯è®¡â€è¡Œï¼ˆå¯é€šè¿‡ `rowType` å­—段区分)。
---
## 7. æ¨¡å—六:科目明细账(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/voucher/detailLedger.vue`
### 7.1 å»ºè®®æŽ¥å£
- `GET /financial/ledger/detail`
### 7.2 è¯·æ±‚参数
- `subjectCode`
- `auxiliaryType`(customer/supplier/department/employee/project)
- `auxiliaryId`
- `startMonth`(YYYY-MM)
- `endMonth`(YYYY-MM)
### 7.3 å“åº”字段
- `date, voucherNo, summary`
- `debit, credit, direction, balance`
### 7.4 è§„则
- å…ˆé€‰ç§‘目,再查明细。
- è¾…助核算条件为可选,但建议后端支持维度过滤。
---
## 8. æŽ¨èæœ€å°è¡¨è®¾è®¡ï¼ˆä»…本范围)
- `fin_account_subject`
- `fin_fixed_asset`
- `fin_intangible_asset`
- `fin_voucher`
- `fin_voucher_entry`
- `fin_ledger_snapshot_general`(可选,做性能优化)
- `fin_ledger_snapshot_detail`(可选,做性能优化)
---
## 9. AI ç”ŸæˆåŽç«¯ä»»åŠ¡é¡ºåºï¼ˆå»ºè®®ï¼‰
1. å…ˆå®Œæˆ **总账科目**(已有 API,最稳定)。
2. å®Œæˆ **凭证 + åˆ†å½• + å€Ÿè´·å¹³è¡¡æ ¡éªŒ + çŠ¶æ€æµè½¬**。
3. å®žçް **科目总账 / ç§‘目明细账** æŸ¥è¯¢ã€‚
4. å®žçް **固定资产折旧** ä¸Ž **无形资产摊销**。
5. è¡¥æµ‹è¯•:
   - å€Ÿè´·å¹³è¡¡æ ¡éªŒ
   - æŠ˜æ—§/摊销公式
   - ç§‘目被引用禁止删除
src/api/financialManagement/fixedAsset.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
import request from "@/utils/request";
// å›ºå®šèµ„产分页查询(current/size)
export function listFixedAssetPage(params) {
  return request({
    url: "/financial/fixedAsset/page",
    method: "get",
    params,
  });
}
// æ–°å¢žå›ºå®šèµ„产
export function addFixedAsset(data) {
  return request({
    url: "/financial/fixedAsset/add",
    method: "post",
    data,
  });
}
// ä¿®æ”¹å›ºå®šèµ„产
export function updateFixedAsset(data) {
  return request({
    url: "/financial/fixedAsset/update",
    method: "put",
    data,
  });
}
// åˆ é™¤å›ºå®šèµ„产(后端要求 ids=1&ids=2 å½¢å¼ï¼‰
export function deleteFixedAsset(ids) {
  const idList = Array.isArray(ids) ? ids : [ids];
  const query = idList
    .filter(id => id !== undefined && id !== null && id !== "")
    .map(id => `ids=${encodeURIComponent(id)}`)
    .join("&");
  return request({
    url: `/financial/fixedAsset/delete?${query}`,
    method: "delete",
  });
}
// æŠ˜æ—§è®¡æï¼ˆ{} è¡¨ç¤ºå…¨éƒ¨åœ¨ç”¨èµ„产)
export function depreciateFixedAsset(data = {}) {
  return request({
    url: "/financial/fixedAsset/depreciate",
    method: "post",
    data,
  });
}
src/api/financialManagement/intangibleAsset.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
import request from "@/utils/request";
// æ— å½¢èµ„产分页查询(current/size)
export function listIntangibleAssetPage(params) {
  return request({
    url: "/financial/intangibleAsset/page",
    method: "get",
    params,
  });
}
// æ–°å¢žæ— å½¢èµ„产
export function addIntangibleAsset(data) {
  return request({
    url: "/financial/intangibleAsset/add",
    method: "post",
    data,
  });
}
// ä¿®æ”¹æ— å½¢èµ„产
export function updateIntangibleAsset(data) {
  return request({
    url: "/financial/intangibleAsset/update",
    method: "put",
    data,
  });
}
// åˆ é™¤æ— å½¢èµ„产(后端要求 ids=1&ids=2 å½¢å¼ï¼‰
export function deleteIntangibleAsset(ids) {
  const idList = Array.isArray(ids) ? ids : [ids];
  const query = idList
    .filter(id => id !== undefined && id !== null && id !== "")
    .map(id => `ids=${encodeURIComponent(id)}`)
    .join("&");
  return request({
    url: `/financial/intangibleAsset/delete?${query}`,
    method: "delete",
  });
}
// æ‘Šé”€è®¡æï¼ˆ{} è¡¨ç¤ºå…¨éƒ¨åœ¨ç”¨èµ„产)
export function amortizeIntangibleAsset(data = {}) {
  return request({
    url: "/financial/intangibleAsset/amortize",
    method: "post",
    data,
  });
}
src/api/financialManagement/ledger.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import request from "@/utils/request";
// ç§‘目总账
export function getGeneralLedger(params) {
  return request({
    url: "/financial/ledger/general",
    method: "get",
    params,
  });
}
// ç§‘目明细账
export function getDetailLedger(params) {
  return request({
    url: "/financial/ledger/detail",
    method: "get",
    params,
  });
}
src/api/financialManagement/voucher.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
import request from "@/utils/request";
// å‡­è¯åˆ†é¡µæŸ¥è¯¢ï¼ˆcurrent/size + è¿‡æ»¤æ¡ä»¶ï¼‰
export function listVoucherPage(params) {
  return request({
    url: "/financial/voucher/page",
    method: "get",
    params,
  });
}
// æ–°å¢žå‡­è¯
export function addVoucher(data) {
  return request({
    url: "/financial/voucher/add",
    method: "post",
    data,
  });
}
// ä¿®æ”¹å‡­è¯ï¼ˆä»…未过账)
export function updateVoucher(data) {
  return request({
    url: "/financial/voucher/update",
    method: "put",
    data,
  });
}
// è¿‡è´¦
export function postVoucher(data) {
  return request({
    url: "/financial/voucher/post",
    method: "post",
    data,
  });
}
// ä½œåºŸ
export function cancelVoucher(data) {
  return request({
    url: "/financial/voucher/cancel",
    method: "post",
    data,
  });
}
// è¯¦æƒ…
export function getVoucherDetail(id) {
  return request({
    url: `/financial/voucher/detail/${id}`,
    method: "get",
  });
}
src/views/financialManagement/assets/fixedAssets.vue
@@ -189,6 +189,13 @@
import { ref, reactive, onMounted, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
  listFixedAssetPage,
  addFixedAsset,
  updateFixedAsset,
  deleteFixedAsset,
  depreciateFixedAsset,
} from "@/api/financialManagement/fixedAsset";
defineOptions({
  name: "固定资产",
@@ -210,13 +217,13 @@
const columns = [
  { label: "资产编号", prop: "assetCode", width: "130" },
  { label: "资产名称", prop: "assetName", width: "150" },
  { label: "资产类别", prop: "category", slot: "category" },
  { label: "资产类别", prop: "category", dataType: "slot", slot: "category" },
  { label: "规格型号", prop: "specification", width: "120" },
  { label: "资产原值", prop: "originalValue", slot: "originalValue" },
  { label: "累计折旧", prop: "accumulatedDepreciation", slot: "accumulatedDepreciation" },
  { label: "资产净值", prop: "netValue", slot: "netValue" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "180", fixed: "right" },
  { label: "资产原值", prop: "originalValue", dataType: "slot", slot: "originalValue" },
  { label: "累计折旧", prop: "accumulatedDepreciation", dataType: "slot", slot: "accumulatedDepreciation" },
  { label: "资产净值", prop: "netValue", dataType: "slot", slot: "netValue" },
  { label: "状态", prop: "status", dataType: "slot", slot: "status" },
  { label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
@@ -226,7 +233,7 @@
const isEdit = ref(false);
const currentId = ref(null);
const form = reactive({
const createDefaultForm = () => ({
  assetCode: "",
  assetName: "",
  category: "",
@@ -244,6 +251,10 @@
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const rules = {
  assetName: [{ required: true, message: "请输入资产名称", trigger: "blur" }],
  category: [{ required: true, message: "请选择资产类别", trigger: "change" }],
@@ -251,13 +262,6 @@
  originalValue: [{ required: true, message: "请输入资产原值", trigger: "blur" }],
  usefulLife: [{ required: true, message: "请输入使用年限", trigger: "blur" }],
};
const mockData = [
  { id: 1, assetCode: "GD2024001", assetName: "办公电脑", category: "electronic", specification: "联想ThinkPad X1", purchaseDate: "2023-01-15", originalValue: 8000, usefulLife: 5, residualRate: 5, accumulatedDepreciation: 1520, netValue: 6480, location: "办公室", department: "财务部", keeper: "张三", status: "in_use", remark: "" },
  { id: 2, assetCode: "GD2024002", assetName: "打印机", category: "electronic", specification: "惠普M479fdw", purchaseDate: "2023-03-20", originalValue: 3500, usefulLife: 5, residualRate: 5, accumulatedDepreciation: 532, netValue: 2968, location: "文印室", department: "行政部", keeper: "李四", status: "in_use", remark: "" },
  { id: 3, assetCode: "GD2024003", assetName: "办公桌椅", category: "furniture", specification: "实木办公桌", purchaseDate: "2023-06-10", originalValue: 2500, usefulLife: 10, residualRate: 5, accumulatedDepreciation: 118.75, netValue: 2381.25, location: "办公室", department: "销售部", keeper: "王五", status: "in_use", remark: "" },
  { id: 4, assetCode: "GD2024004", assetName: "商务车", category: "vehicle", specification: "别克GL8", purchaseDate: "2022-08-01", originalValue: 280000, usefulLife: 10, residualRate: 5, accumulatedDepreciation: 53200, netValue: 226800, location: "停车场", department: "行政部", keeper: "赵六", status: "in_use", remark: "" },
];
const totalOriginalValue = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
@@ -288,35 +292,39 @@
};
const getStatusLabel = (status) => {
  const map = { in_use: "在用", idle: "闲置", scrapped: "报废" };
  return map[status] || status;
  const key = String(status || "").toLowerCase();
  const map = { in_use: "在用", idle: "闲置", repair: "维修中", scrapped: "报废" };
  return map[key] || status;
};
const getStatusType = (status) => {
  const map = { in_use: "success", idle: "warning", scrapped: "info" };
  return map[status] || "";
  const key = String(status || "").toLowerCase();
  const map = { in_use: "success", idle: "warning", repair: "warning", scrapped: "info" };
  return map[key] || "";
};
const calculateNetValue = () => {
  form.netValue = Number((form.originalValue - form.accumulatedDepreciation).toFixed(2));
  const originalValue = Number(form.originalValue || 0);
  const accumulatedDepreciation = Number(form.accumulatedDepreciation || 0);
  form.netValue = Number((originalValue - accumulatedDepreciation).toFixed(2));
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.assetCode) {
    result = result.filter(item => item.assetCode.includes(filters.assetCode));
// è”调约定:分页参数固定为 current/size,返回 data.records/data.total
const getTableData = async () => {
  try {
    const { data } = await listFixedAssetPage({
      current: pagination.currentPage,
      size: pagination.pageSize,
      assetCode: filters.assetCode,
      assetName: filters.assetName,
      category: filters.category,
      status: filters.status,
    });
    dataList.value = data?.records || [];
    pagination.total = Number(data?.total || 0);
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
  if (filters.assetName) {
    result = result.filter(item => item.assetName.includes(filters.assetName));
  }
  if (filters.category) {
    result = result.filter(item => item.category === filters.category);
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
@@ -334,25 +342,15 @@
  getTableData();
};
const buildAssetCode = () => `GD${Date.now().toString().slice(-10)}`;
const add = () => {
  isEdit.value = false;
  currentId.value = null;
  dialogTitle.value = "新增固定资产";
  Object.assign(form, {
    assetCode: "GD" + Date.now().toString().slice(-8),
    assetName: "",
    category: "",
    specification: "",
  Object.assign(form, createDefaultForm(), {
    assetCode: buildAssetCode(),
    purchaseDate: new Date().toISOString().split('T')[0],
    originalValue: 0,
    usefulLife: 5,
    residualRate: 5,
    accumulatedDepreciation: 0,
    netValue: 0,
    location: "",
    department: "",
    keeper: "",
    status: "in_use",
    remark: "",
  });
  dialogVisible.value = true;
};
@@ -361,7 +359,7 @@
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑固定资产";
  Object.assign(form, row);
  Object.assign(form, createDefaultForm(), row);
  dialogVisible.value = true;
};
@@ -374,13 +372,14 @@
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
  }).then(async () => {
    // è”调约定:删除接口使用 ids=1&ids=2
    await deleteFixedAsset([row.id]);
    if (dataList.value.length === 1 && pagination.currentPage > 1) {
      pagination.currentPage -= 1;
    }
    ElMessage.success("删除成功");
    getTableData();
    await getTableData();
  });
};
@@ -389,16 +388,10 @@
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    mockData.forEach(item => {
      if (item.status === "in_use") {
        const monthlyDepreciation = (item.originalValue * (1 - item.residualRate / 100)) / (item.usefulLife * 12);
        item.accumulatedDepreciation = Number((item.accumulatedDepreciation + monthlyDepreciation).toFixed(2));
        item.netValue = Number((item.originalValue - item.accumulatedDepreciation).toFixed(2));
      }
    });
  }).then(async () => {
    await depreciateFixedAsset({});
    ElMessage.success("折旧计提完成");
    getTableData();
    await getTableData();
  });
};
@@ -407,22 +400,24 @@
};
const submitForm = () => {
  formRef.value.validate((valid) => {
  formRef.value.validate(async valid => {
    if (valid) {
      try {
      calculateNetValue();
        const payload = { ...form };
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
        }
          payload.id = currentId.value;
          await updateFixedAsset(payload);
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
          await addFixedAsset(payload);
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
    }
  });
};
src/views/financialManagement/assets/intangibleAssets.vue
@@ -182,6 +182,13 @@
import { ref, reactive, onMounted, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
  listIntangibleAssetPage,
  addIntangibleAsset,
  updateIntangibleAsset,
  deleteIntangibleAsset,
  amortizeIntangibleAsset,
} from "@/api/financialManagement/intangibleAsset";
defineOptions({
  name: "无形资产",
@@ -203,13 +210,13 @@
const columns = [
  { label: "资产编号", prop: "assetCode", width: "130" },
  { label: "资产名称", prop: "assetName", width: "150" },
  { label: "资产类别", prop: "category", slot: "category" },
  { label: "资产类别", prop: "category", dataType: "slot", slot: "category" },
  { label: "证书编号", prop: "certificateNo", width: "150" },
  { label: "资产原值", prop: "originalValue", slot: "originalValue" },
  { label: "累计摊销", prop: "accumulatedAmortization", slot: "accumulatedAmortization" },
  { label: "资产净值", prop: "netValue", slot: "netValue" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "180", fixed: "right" },
  { label: "资产原值", prop: "originalValue", dataType: "slot", slot: "originalValue" },
  { label: "累计摊销", prop: "accumulatedAmortization", dataType: "slot", slot: "accumulatedAmortization" },
  { label: "资产净值", prop: "netValue", dataType: "slot", slot: "netValue" },
  { label: "状态", prop: "status", dataType: "slot", slot: "status" },
  { label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
@@ -219,7 +226,7 @@
const isEdit = ref(false);
const currentId = ref(null);
const form = reactive({
const createDefaultForm = () => ({
  assetCode: "",
  assetName: "",
  category: "",
@@ -236,6 +243,10 @@
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const rules = {
  assetName: [{ required: true, message: "请输入资产名称", trigger: "blur" }],
  category: [{ required: true, message: "请选择资产类别", trigger: "change" }],
@@ -243,13 +254,6 @@
  originalValue: [{ required: true, message: "请输入资产原值", trigger: "blur" }],
  amortizationPeriod: [{ required: true, message: "请输入摊销年限", trigger: "blur" }],
};
const mockData = [
  { id: 1, assetCode: "WX2024001", assetName: "ERP软件许可", category: "software", certificateNo: "SW-2023-001", acquisitionDate: "2023-01-01", originalValue: 50000, amortizationPeriod: 10, residualRate: 0, accumulatedAmortization: 5000, netValue: 45000, validityDate: "2033-01-01", status: "in_use", description: "企业资源计划管理系统", remark: "" },
  { id: 2, assetCode: "WX2024002", assetName: "发明专利", category: "patent", certificateNo: "ZL202210123456.7", acquisitionDate: "2022-06-15", originalValue: 100000, amortizationPeriod: 20, residualRate: 0, accumulatedAmortization: 3750, netValue: 96250, validityDate: "2042-06-15", status: "in_use", description: "一种新型生产工艺", remark: "" },
  { id: 3, assetCode: "WX2024003", assetName: "商标权", category: "trademark", certificateNo: "TM-2023-008", acquisitionDate: "2023-03-10", originalValue: 20000, amortizationPeriod: 10, residualRate: 0, accumulatedAmortization: 1500, netValue: 18500, validityDate: "2033-03-10", status: "in_use", description: "公司品牌商标", remark: "" },
  { id: 4, assetCode: "WX2024004", assetName: "土地使用权", category: "land", certificateNo: "土国用(2023)第001号", acquisitionDate: "2023-07-01", originalValue: 500000, amortizationPeriod: 50, residualRate: 0, accumulatedAmortization: 5000, netValue: 495000, validityDate: "2073-07-01", status: "in_use", description: "工业用地使用权", remark: "" },
];
const totalOriginalValue = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
@@ -281,35 +285,44 @@
};
const getStatusLabel = (status) => {
  const map = { in_use: "在用", idle: "闲置", amortized: "已摊销完毕" };
  return map[status] || status;
  const key = String(status || "").toLowerCase();
  const map = {
    in_use: "在用",
    idle: "闲置",
    expired: "已到期",
    amortized: "已摊销完毕",
  };
  return map[key] || status;
};
const getStatusType = (status) => {
  const map = { in_use: "success", idle: "warning", amortized: "info" };
  return map[status] || "";
  const key = String(status || "").toLowerCase();
  const map = { in_use: "success", idle: "warning", expired: "warning", amortized: "info" };
  return map[key] || "";
};
const calculateNetValue = () => {
  form.netValue = Number((form.originalValue - form.accumulatedAmortization).toFixed(2));
  const originalValue = Number(form.originalValue || 0);
  const accumulatedAmortization = Number(form.accumulatedAmortization || 0);
  form.netValue = Number((originalValue - accumulatedAmortization).toFixed(2));
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.assetCode) {
    result = result.filter(item => item.assetCode.includes(filters.assetCode));
// è”调约定:分页参数固定为 current/size,返回 data.records/data.total
const getTableData = async () => {
  try {
    const { data } = await listIntangibleAssetPage({
      current: pagination.currentPage,
      size: pagination.pageSize,
      assetCode: filters.assetCode,
      assetName: filters.assetName,
      category: filters.category,
      status: filters.status,
    });
    dataList.value = data?.records || [];
    pagination.total = Number(data?.total || 0);
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
  if (filters.assetName) {
    result = result.filter(item => item.assetName.includes(filters.assetName));
  }
  if (filters.category) {
    result = result.filter(item => item.category === filters.category);
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
@@ -327,24 +340,15 @@
  getTableData();
};
const buildAssetCode = () => `WX${Date.now().toString().slice(-10)}`;
const add = () => {
  isEdit.value = false;
  currentId.value = null;
  dialogTitle.value = "新增无形资产";
  Object.assign(form, {
    assetCode: "WX" + Date.now().toString().slice(-8),
    assetName: "",
    category: "",
    certificateNo: "",
  Object.assign(form, createDefaultForm(), {
    assetCode: buildAssetCode(),
    acquisitionDate: new Date().toISOString().split('T')[0],
    originalValue: 0,
    amortizationPeriod: 10,
    residualRate: 0,
    accumulatedAmortization: 0,
    netValue: 0,
    validityDate: "",
    status: "in_use",
    description: "",
    remark: "",
  });
  dialogVisible.value = true;
};
@@ -353,7 +357,7 @@
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑无形资产";
  Object.assign(form, row);
  Object.assign(form, createDefaultForm(), row);
  dialogVisible.value = true;
};
@@ -366,13 +370,14 @@
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
  }).then(async () => {
    // è”调约定:删除接口使用 ids=1&ids=2
    await deleteIntangibleAsset([row.id]);
    if (dataList.value.length === 1 && pagination.currentPage > 1) {
      pagination.currentPage -= 1;
    }
    ElMessage.success("删除成功");
    getTableData();
    await getTableData();
  });
};
@@ -381,20 +386,10 @@
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    mockData.forEach(item => {
      if (item.status === "in_use") {
        const monthlyAmortization = (item.originalValue * (1 - item.residualRate / 100)) / (item.amortizationPeriod * 12);
        item.accumulatedAmortization = Number((item.accumulatedAmortization + monthlyAmortization).toFixed(2));
        item.netValue = Number((item.originalValue - item.accumulatedAmortization).toFixed(2));
        if (item.netValue <= 0) {
          item.status = "amortized";
          item.netValue = 0;
        }
      }
    });
  }).then(async () => {
    await amortizeIntangibleAsset({});
    ElMessage.success("摊销计提完成");
    getTableData();
    await getTableData();
  });
};
@@ -403,22 +398,24 @@
};
const submitForm = () => {
  formRef.value.validate((valid) => {
  formRef.value.validate(async valid => {
    if (valid) {
      try {
      calculateNetValue();
        const payload = { ...form };
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
        }
          payload.id = currentId.value;
          await updateIntangibleAsset(payload);
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
          await addIntangibleAsset(payload);
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
    }
  });
};
src/views/financialManagement/generalLedger/index.vue
@@ -68,6 +68,10 @@
               :rules="rules"
               ref="formRef"
               label-width="100px">
        <el-form-item label="父级科目">
          <el-input :model-value="parentSubjectLabel"
                    disabled />
        </el-form-item>
        <el-form-item label="科目编码"
                      prop="subjectCode">
          <el-input v-model="form.subjectCode"
@@ -201,8 +205,15 @@
      label: "操作",
      align: "center",
      fixed: "right",
      width: "150",
      width: "220",
      operation: [
        {
          name: "新增",
          type: "primary",
          clickFun: row => {
            addChild(row);
          },
        },
        {
          name: "编辑",
          type: "primary",
@@ -224,11 +235,13 @@
  const dataList = ref([]);
  const dialogVisible = ref(false);
  const dialogTitle = ref("");
  const parentSubjectLabel = ref("顶级科目");
  const formRef = ref(null);
  const isEdit = ref(false);
  const form = reactive({
    id: undefined,
    parentId: null,
    subjectCode: "",
    subjectName: "",
    subjectType: "",
@@ -258,8 +271,8 @@
  const getTableData = () => {
    const query = {
      pageNum: pagination.currentPage,
      pageSize: pagination.pageSize,
      current: pagination.currentPage,
      size: pagination.pageSize,
      ...filters,
    };
    listAccountSubject(query).then(response => {
@@ -282,11 +295,19 @@
    getTableData();
  };
  const add = () => {
    isEdit.value = false;
    dialogTitle.value = "新增科目";
  const buildParentSubjectLabel = parentRow => {
    if (!parentRow) {
      return "顶级科目";
    }
    const code = parentRow.subjectCode || "";
    const name = parentRow.subjectName || "";
    return `${code} ${name}`.trim();
  };
  const resetForm = ({ parentId = null, parentRow = null } = {}) => {
    Object.assign(form, {
      id: undefined,
      parentId,
      subjectCode: "",
      subjectName: "",
      subjectType: "",
@@ -294,13 +315,54 @@
      status: 0,
      remark: "",
    });
    parentSubjectLabel.value = buildParentSubjectLabel(parentRow);
  };
  const add = () => {
    isEdit.value = false;
    dialogTitle.value = "新增科目";
    resetForm({ parentId: null, parentRow: null });
    dialogVisible.value = true;
  };
  const addChild = row => {
    isEdit.value = false;
    dialogTitle.value = "新增子科目";
    resetForm({ parentId: row.id, parentRow: row });
    form.subjectType = row.subjectType || "";
    form.balanceDirection = row.balanceDirection || "借方";
    dialogVisible.value = true;
  };
  const findSubjectById = (nodes, id) => {
    for (const item of nodes || []) {
      if (item.id === id) {
        return item;
      }
      if (item.children && item.children.length > 0) {
        const found = findSubjectById(item.children, id);
        if (found) {
          return found;
        }
      }
    }
    return null;
  };
  const edit = row => {
    isEdit.value = true;
    dialogTitle.value = "编辑科目";
    Object.assign(form, row);
    form.parentId = row.parentId ?? null;
    const parentRow =
      row.parentId === null || row.parentId === undefined
        ? null
        : findSubjectById(dataList.value, row.parentId);
    parentSubjectLabel.value = parentRow
      ? buildParentSubjectLabel(parentRow)
      : row.parentId
      ? `上级ID: ${row.parentId}`
      : buildParentSubjectLabel(null);
    dialogVisible.value = true;
  };
src/views/financialManagement/voucher/detailLedger.vue
@@ -43,13 +43,13 @@
        <el-table-column prop="date" label="日期" width="120" />
        <el-table-column prop="voucherNo" label="凭证字号" width="120" />
        <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
        <el-table-column label="借方" width="150">
        <el-table-column prop="debit" label="借方" width="150">
          <template #default="{ row }">
            <span v-if="row.debit > 0" class="text-danger">Â¥{{ formatMoney(row.debit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="贷方" width="150">
        <el-table-column prop="credit" label="贷方" width="150">
          <template #default="{ row }">
            <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
            <span v-else>-</span>
@@ -75,6 +75,8 @@
<script setup>
import { ref, reactive, onMounted, computed, watch } from "vue";
import { ElMessage } from "element-plus";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import { getDetailLedger } from "@/api/financialManagement/ledger";
defineOptions({
  name: "科目明细账",
@@ -89,36 +91,36 @@
});
const dataList = ref([]);
const subjectOptions = ref([]);
const subjectOptions = [
  {
    code: "1122",
    name: "应收账款",
    children: [
      { code: "112201", name: "北京科技有限公司" },
      { code: "112202", name: "上海贸易公司" },
      { code: "112203", name: "广州实业有限公司" },
    ],
  },
  {
    code: "2202",
    name: "应付账款",
    children: [
      { code: "220201", name: "北京原材料供应商" },
      { code: "220202", name: "上海电子元器件公司" },
      { code: "220203", name: "广州包装材料厂" },
    ],
  },
  {
    code: "6602",
    name: "管理费用",
    children: [
      { code: "660201", name: "办公费" },
      { code: "660202", name: "差旅费" },
      { code: "660203", name: "业务招待费" },
    ],
  },
const fallbackSubjects = [
  { code: "1122", name: "应收账款" },
  { code: "2202", name: "应付账款" },
  { code: "6602", name: "管理费用" },
];
const loadSubjectOptions = async () => {
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
    });
    const records = data?.records || [];
    if (records.length > 0) {
      subjectOptions.value = records
        .filter(item => item.subjectCode && item.subjectName)
        .map(item => ({
          code: item.subjectCode,
          name: item.subjectName,
          children: [],
        }));
      return;
    }
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  subjectOptions.value = fallbackSubjects.map(item => ({ ...item, children: [] }));
};
const auxiliaryItems = computed(() => {
  const map = {
@@ -158,7 +160,7 @@
const currentSubject = computed(() => {
  if (!filters.subject || filters.subject.length === 0) return null;
  const code = filters.subject[filters.subject.length - 1];
  return findSubject(subjectOptions, code);
  return findSubject(subjectOptions.value, code);
});
const findSubject = (options, code) => {
@@ -182,31 +184,24 @@
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const mockData = [
  { date: "2024-01-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 10000 },
  { date: "2024-01-05", voucherNo: "è®°-0001", summary: "销售出库", debit: 5000, credit: 0, direction: "借", balance: 15000 },
  { date: "2024-01-10", voucherNo: "è®°-0002", summary: "收到货款", debit: 0, credit: 3000, direction: "借", balance: 12000 },
  { date: "2024-01-15", voucherNo: "è®°-0003", summary: "销售出库", debit: 8000, credit: 0, direction: "借", balance: 20000 },
  { date: "2024-01-20", voucherNo: "è®°-0004", summary: "销售退货", debit: 0, credit: 2000, direction: "借", balance: 18000 },
  { date: "2024-01-25", voucherNo: "è®°-0005", summary: "收到货款", debit: 0, credit: 5000, direction: "借", balance: 13000 },
  { date: "2024-01-31", voucherNo: "-", summary: "本月合计", debit: 13000, credit: 10000, direction: "借", balance: 13000 },
  { date: "2024-02-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 13000 },
  { date: "2024-02-10", voucherNo: "è®°-0006", summary: "销售出库", debit: 6000, credit: 0, direction: "借", balance: 19000 },
  { date: "2024-02-15", voucherNo: "è®°-0007", summary: "收到货款", debit: 0, credit: 4000, direction: "借", balance: 15000 },
  { date: "2024-02-28", voucherNo: "-", summary: "本月合计", debit: 6000, credit: 4000, direction: "借", balance: 15000 },
  { date: "2024-03-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 15000 },
  { date: "2024-03-05", voucherNo: "è®°-0008", summary: "销售出库", debit: 7000, credit: 0, direction: "借", balance: 22000 },
  { date: "2024-03-10", voucherNo: "è®°-0009", summary: "收到货款", debit: 0, credit: 6000, direction: "借", balance: 16000 },
  { date: "2024-03-31", voucherNo: "-", summary: "本月合计", debit: 7000, credit: 6000, direction: "借", balance: 16000 },
  { date: "2024-03-31", voucherNo: "-", summary: "本年累计", debit: 26000, credit: 20000, direction: "借", balance: 16000 },
];
const getTableData = () => {
// è”调约定:明细账接口可按辅助核算过滤(auxiliaryType/auxiliaryId)
const getTableData = async () => {
  if (!currentSubject.value) {
    dataList.value = [];
    return;
  }
  dataList.value = [...mockData];
  try {
    const { data } = await getDetailLedger({
      subjectCode: currentSubject.value.code,
      auxiliaryType: filters.auxiliary,
      auxiliaryId: filters.auxiliaryItem,
      startMonth: filters.startMonth,
      endMonth: filters.endMonth,
    });
    dataList.value = Array.isArray(data) ? data : data?.records || [];
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const resetFilters = () => {
@@ -249,8 +244,8 @@
  ElMessage.success("导出成功");
};
onMounted(() => {
  // é»˜è®¤ä¸åŠ è½½æ•°æ®ï¼Œéœ€è¦é€‰æ‹©ç§‘ç›®
onMounted(async () => {
  await loadSubjectOptions();
});
</script>
src/views/financialManagement/voucher/generalLedger.vue
@@ -28,13 +28,13 @@
        <el-table-column prop="date" label="日期" width="120" />
        <el-table-column prop="voucherNo" label="凭证字号" width="120" />
        <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
        <el-table-column label="借方" width="150">
        <el-table-column prop="debit" label="借方" width="150">
          <template #default="{ row }">
            <span v-if="row.debit > 0" class="text-danger">Â¥{{ formatMoney(row.debit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="贷方" width="150">
        <el-table-column prop="credit" label="贷方" width="150">
          <template #default="{ row }">
            <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
            <span v-else>-</span>
@@ -60,6 +60,8 @@
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ElMessage } from "element-plus";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import { getGeneralLedger } from "@/api/financialManagement/ledger";
defineOptions({
  name: "科目总账",
@@ -72,42 +74,47 @@
});
const dataList = ref([]);
const subjectOptions = ref([]);
const subjectOptions = [
  {
    code: "1001",
    name: "库存现金",
    children: [],
  },
  {
    code: "1002",
    name: "银行存款",
    children: [
      { code: "100201", name: "工商银行" },
      { code: "100202", name: "建设银行" },
    ],
  },
  {
    code: "1122",
    name: "应收账款",
    children: [],
  },
  {
    code: "2202",
    name: "应付账款",
    children: [],
  },
  {
    code: "6001",
    name: "主营业务收入",
    children: [],
  },
const fallbackSubjects = [
  { code: "1001", name: "库存现金" },
  { code: "1002", name: "银行存款" },
  { code: "1122", name: "应收账款" },
  { code: "2202", name: "应付账款" },
  { code: "6001", name: "主营业务收入" },
];
const toCascaderTree = (nodes = []) =>
  nodes
    .filter(item => item.subjectCode && item.subjectName)
    .map(item => ({
      code: item.subjectCode,
      name: item.subjectName,
      children: toCascaderTree(item.children || []),
    }));
const loadSubjectOptions = async () => {
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
      status: 0,
    });
    const options = toCascaderTree(data?.records || []);
    if (options.length > 0) {
      subjectOptions.value = options;
      return;
    }
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  subjectOptions.value = fallbackSubjects.map(item => ({ ...item, children: [] }));
};
const currentSubject = computed(() => {
  if (!filters.subject || filters.subject.length === 0) return null;
  const code = filters.subject[filters.subject.length - 1];
  return findSubject(subjectOptions, code);
  return findSubject(subjectOptions.value, code);
});
const findSubject = (options, code) => {
@@ -126,30 +133,22 @@
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const mockData = [
  { date: "2024-01-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 100000 },
  { date: "2024-01-05", voucherNo: "è®°-0001", summary: "销售收入", debit: 5650, credit: 0, direction: "借", balance: 105650 },
  { date: "2024-01-10", voucherNo: "è®°-0002", summary: "采购支出", debit: 0, credit: 8000, direction: "借", balance: 97650 },
  { date: "2024-01-15", voucherNo: "è®°-0003", summary: "收到货款", debit: 10000, credit: 0, direction: "借", balance: 107650 },
  { date: "2024-01-20", voucherNo: "è®°-0004", summary: "支付费用", debit: 0, credit: 5000, direction: "借", balance: 102650 },
  { date: "2024-01-31", voucherNo: "-", summary: "本月合计", debit: 15650, credit: 13000, direction: "借", balance: 102650 },
  { date: "2024-02-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 102650 },
  { date: "2024-02-10", voucherNo: "è®°-0005", summary: "销售收入", debit: 8000, credit: 0, direction: "借", balance: 110650 },
  { date: "2024-02-15", voucherNo: "è®°-0006", summary: "采购支出", debit: 0, credit: 12000, direction: "借", balance: 98650 },
  { date: "2024-02-28", voucherNo: "-", summary: "本月合计", debit: 8000, credit: 12000, direction: "借", balance: 98650 },
  { date: "2024-03-01", voucherNo: "-", summary: "期初余额", debit: 0, credit: 0, direction: "借", balance: 98650 },
  { date: "2024-03-05", voucherNo: "è®°-0007", summary: "销售收入", debit: 12000, credit: 0, direction: "借", balance: 110650 },
  { date: "2024-03-10", voucherNo: "è®°-0008", summary: "支付工资", debit: 0, credit: 15000, direction: "借", balance: 95650 },
  { date: "2024-03-31", voucherNo: "-", summary: "本月合计", debit: 12000, credit: 15000, direction: "借", balance: 95650 },
  { date: "2024-03-31", voucherNo: "-", summary: "本年累计", debit: 35650, credit: 40000, direction: "借", balance: 95650 },
];
const getTableData = () => {
// è”调约定:总账接口返回行数组(rowType/date/voucherNo/summary/debit/credit/direction/balance)
const getTableData = async () => {
  if (!currentSubject.value) {
    dataList.value = [];
    return;
  }
  dataList.value = [...mockData];
  try {
    const { data } = await getGeneralLedger({
      subjectCode: currentSubject.value.code,
      startMonth: filters.startMonth,
      endMonth: filters.endMonth,
    });
    dataList.value = Array.isArray(data) ? data : data?.records || [];
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const resetFilters = () => {
@@ -190,8 +189,8 @@
  ElMessage.success("导出成功");
};
onMounted(() => {
  // é»˜è®¤ä¸åŠ è½½æ•°æ®ï¼Œéœ€è¦é€‰æ‹©ç§‘ç›®
onMounted(async () => {
  await loadSubjectOptions();
});
</script>
src/views/financialManagement/voucher/index.vue
@@ -62,9 +62,9 @@
        </template>
        <template #operation="{ row }">
          <el-button type="primary" link @click="view(row)">查看</el-button>
          <el-button type="primary" link @click="edit(row)" v-if="row.status === 'unposted'">编辑</el-button>
          <el-button type="success" link @click="handlePost(row)" v-if="row.status === 'unposted'">过账</el-button>
          <el-button type="danger" link @click="handleCancel(row)" v-if="row.status === 'unposted'">作废</el-button>
          <el-button type="primary" link @click="edit(row)" v-if="canEditVoucher(row.status)">编辑</el-button>
          <el-button type="success" link @click="handlePost(row)" v-if="canEditVoucher(row.status)">过账</el-button>
          <el-button type="danger" link @click="handleCancel(row)" v-if="canEditVoucher(row.status)">作废</el-button>
        </template>
      </PIMTable>
    </div>
@@ -137,9 +137,18 @@
                    <el-input v-model="entry.summary" placeholder="请输入摘要" @focus="selectRow(rowIndex)" />
                  </td>
                  <td class="col-subject">
                    <el-select v-model="entry.subjectCode" placeholder="选择科目" filterable @change="(val) => handleSubjectChange(val, rowIndex)" @focus="selectRow(rowIndex)">
                      <el-option v-for="item in subjectList" :key="item.code" :label="item.code + item.name" :value="item.code" />
                    </el-select>
                    <el-tree-select
                      v-model="entry.subjectCode"
                      :data="subjectTreeOptions"
                      :props="subjectTreeSelectProps"
                      placeholder="选择科目"
                      filterable
                      check-strictly
                      clearable
                      :render-after-expand="false"
                      @change="(val) => handleSubjectChange(val, rowIndex)"
                      @focus="selectRow(rowIndex)"
                    />
                    <div class="subject-name">{{ entry.subjectName }}</div>
                  </td>
                  <!-- å€Ÿæ–¹11列 -->
@@ -205,6 +214,15 @@
import { ref, reactive, onMounted, computed, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import {
  listVoucherPage,
  addVoucher,
  updateVoucher,
  postVoucher,
  cancelVoucher,
  getVoucherDetail,
} from "@/api/financialManagement/voucher";
defineOptions({
  name: "凭证管理",
@@ -227,11 +245,11 @@
  { label: "凭证字号", prop: "voucherNo", width: "120" },
  { label: "凭证日期", prop: "voucherDate", width: "120" },
  { label: "摘要", prop: "summary", showOverflowTooltip: true },
  { label: "借方金额", prop: "debit", slot: "debit" },
  { label: "贷方金额", prop: "credit", slot: "credit" },
  { label: "借方金额", prop: "debit", dataType: "slot", slot: "debit" },
  { label: "贷方金额", prop: "credit", dataType: "slot", slot: "credit" },
  { label: "制单人", prop: "creator", width: "100" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "220", fixed: "right" },
  { label: "状态", prop: "status", dataType: "slot", slot: "status" },
  { label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" },
];
const dataList = ref([]);
@@ -241,25 +259,64 @@
const isEdit = ref(false);
const currentId = ref(null);
const subjectList = [
  { code: "1001", name: "库存现金" },
  { code: "1002", name: "银行存款" },
  { code: "1122", name: "应收账款" },
  { code: "2202", name: "应付账款" },
  { code: "5001", name: "生产成本" },
  { code: "6001", name: "主营业务收入" },
  { code: "6401", name: "主营业务成本" },
const fallbackSubjectTree = [
  { subjectCode: "1001", subjectName: "库存现金", balanceDirection: "借方", children: [] },
  { subjectCode: "1002", subjectName: "银行存款", balanceDirection: "借方", children: [] },
  { subjectCode: "1122", subjectName: "应收账款", balanceDirection: "借方", children: [] },
  { subjectCode: "2202", subjectName: "应付账款", balanceDirection: "贷方", children: [] },
  { subjectCode: "5001", subjectName: "生产成本", balanceDirection: "借方", children: [] },
  { subjectCode: "6001", subjectName: "主营业务收入", balanceDirection: "贷方", children: [] },
  { subjectCode: "6401", subjectName: "主营业务成本", balanceDirection: "借方", children: [] },
];
const form = reactive({
const subjectTreeOptions = ref([]);
const subjectList = ref([]);
const subjectTreeSelectProps = {
  children: "children",
  label: "label",
  value: "value",
};
const buildSubjectTreeOptions = (nodes = [], flatList = []) =>
  (nodes || [])
    .filter(item => item.subjectCode && item.subjectName)
    .map(item => {
      const balanceDirection = item.balanceDirection || "";
      const flatItem = {
        code: item.subjectCode,
        name: item.subjectName,
        balanceDirection,
      };
      flatList.push(flatItem);
      return {
        value: flatItem.code,
        label: `${flatItem.code} ${flatItem.name}${balanceDirection ? ` [${balanceDirection}]` : ""}`,
        children: buildSubjectTreeOptions(item.children || [], flatList),
      };
    });
const createEmptyEntry = () => ({
  subjectCode: "",
  subjectName: "",
  balanceDirection: "",
  summary: "",
  debit: 0,
  credit: 0,
});
const createDefaultForm = () => ({
  voucherNo: "",
  voucherPrefix: "è®°",
  voucherNum: "",
  voucherDate: "",
  attachmentCount: 0,
  entries: [],
  entries: [createEmptyEntry(), createEmptyEntry()],
  creator: "张三",
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const selectedRowIndex = ref(-1);
@@ -276,12 +333,6 @@
const rules = {
  voucherDate: [{ required: true, message: "请选择凭证日期", trigger: "change" }],
};
const mockData = [
  { id: 1, voucherNo: "è®°-0001", voucherDate: "2024-01-15", summary: "销售收入", debit: 5650, credit: 5650, creator: "张三", status: "posted", entries: [{ subjectCode: "1002", subjectName: "银行存款", summary: "销售收入", debit: 5650, credit: 0 }, { subjectCode: "6001", subjectName: "主营业务收入", summary: "销售收入", debit: 0, credit: 5000 }, { subjectCode: "2221", subjectName: "应交税费", summary: "销项税额", debit: 0, credit: 650 }] },
  { id: 2, voucherNo: "è®°-0002", voucherDate: "2024-01-16", summary: "采购原材料", debit: 9040, credit: 9040, creator: "李四", status: "unposted", entries: [{ subjectCode: "5001", subjectName: "生产成本", summary: "采购原材料", debit: 8000, credit: 0 }, { subjectCode: "2221", subjectName: "应交税费", summary: "进项税额", debit: 1040, credit: 0 }, { subjectCode: "2202", subjectName: "应付账款", summary: "采购原材料", debit: 0, credit: 9040 }] },
  { id: 3, voucherNo: "è®°-0003", voucherDate: "2024-01-18", summary: "支付货款", debit: 5000, credit: 5000, creator: "张三", status: "posted", entries: [{ subjectCode: "2202", subjectName: "应付账款", summary: "支付货款", debit: 5000, credit: 0 }, { subjectCode: "1002", subjectName: "银行存款", summary: "支付货款", debit: 0, credit: 5000 }] },
];
const totalDebit = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.debit), 0);
@@ -304,32 +355,70 @@
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const normalizeVoucherStatus = status => String(status || "").toLowerCase();
const canEditVoucher = status => {
  const key = normalizeVoucherStatus(status);
  return key === "unposted" || status === "未过账";
};
const getStatusLabel = (status) => {
  const key = normalizeVoucherStatus(status);
  const map = { unposted: "未过账", posted: "已过账", cancelled: "已作废" };
  return map[status] || status;
  return map[key] || status;
};
const getStatusType = (status) => {
  const key = normalizeVoucherStatus(status);
  const map = { unposted: "warning", posted: "success", cancelled: "info" };
  return map[status] || "";
  return map[key] || "";
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.voucherNo) {
    result = result.filter(item => item.voucherNo.includes(filters.voucherNo));
// è”调约定:分页参数使用 current/size,日期范围拆分为 startDate/endDate
const getTableData = async () => {
  try {
    const [startDate, endDate] =
      filters.dateRange && filters.dateRange.length === 2 ? filters.dateRange : ["", ""];
    const { data } = await listVoucherPage({
      current: pagination.currentPage,
      size: pagination.pageSize,
      voucherNo: filters.voucherNo,
      creator: filters.creator,
      status: filters.status,
      startDate,
      endDate,
    });
    dataList.value = data?.records || [];
    pagination.total = Number(data?.total || 0);
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
  if (filters.dateRange && filters.dateRange.length === 2) {
    result = result.filter(item => item.voucherDate >= filters.dateRange[0] && item.voucherDate <= filters.dateRange[1]);
};
// å‡­è¯åˆ†å½•里的科目下拉与总账科目保持一致,避免提交不存在科目
const loadSubjectList = async () => {
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
      status: 0
    });
    const flatList = [];
    const treeOptions = buildSubjectTreeOptions(data?.records || [], flatList);
    if (treeOptions.length > 0) {
      subjectTreeOptions.value = treeOptions;
      subjectList.value = flatList;
      return;
  }
  if (filters.creator) {
    result = result.filter(item => item.creator === filters.creator);
    const fallbackFlatList = [];
    subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
    subjectList.value = fallbackFlatList;
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºé”™è¯¯ï¼Œè¿™é‡Œä¿ç•™é»˜è®¤ç§‘目作为兜底
    const fallbackFlatList = [];
    subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
    subjectList.value = fallbackFlatList;
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
@@ -348,7 +437,7 @@
};
const addEntry = () => {
  form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
  form.entries.push(createEmptyEntry());
};
const selectRow = (index) => {
@@ -402,61 +491,69 @@
};
const removeEntry = (index) => {
  if (form.entries.length <= 2) {
    return;
  }
  form.entries.splice(index, 1);
  calculateTotal();
};
const handleSubjectChange = (val, index) => {
  const subject = subjectList.find(item => item.code === val);
  const subject = subjectList.value.find(item => item.code === val);
  if (subject) {
    form.entries[index].subjectName = subject.name;
    form.entries[index].balanceDirection = subject.balanceDirection || "";
  } else {
    form.entries[index].subjectName = "";
    form.entries[index].balanceDirection = "";
  }
};
const calculateTotal = () => {
  // è‡ªåŠ¨è®¡ç®—ï¼Œç”±computed属性处理
};
const add = () => {
  isEdit.value = false;
  currentId.value = null;
  dialogTitle.value = "新增凭证";
  const nextNum = String(mockData.length + 1).padStart(2, "0");
  Object.assign(form, {
    voucherNo: "è®°-" + nextNum,
  const nextNum = String((pagination.total || 0) + 1).padStart(4, "0");
  Object.assign(form, createDefaultForm(), {
    voucherPrefix: "è®°",
    voucherNum: nextNum,
    voucherNo: `è®°-${nextNum}`,
    voucherDate: new Date().toISOString().split('T')[0],
    attachmentCount: 0,
    entries: [
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
      { subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 },
    ],
    creator: "张三",
    remark: "",
  });
  selectedRowIndex.value = 0;
  dialogVisible.value = true;
};
const edit = (row) => {
const edit = async row => {
  try {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑凭证";
  const parts = row.voucherNo.split('-');
  Object.assign(form, {
    ...row,
    voucherPrefix: parts[0] || 'è®°',
    voucherNum: parts[1] || '',
    const { data } = await getVoucherDetail(row.id);
    const detail = data || row;
    const parts = (detail.voucherNo || "").split("-");
    Object.assign(form, createDefaultForm(), detail, {
      voucherPrefix: parts[0] || "è®°",
      voucherNum: parts[1] || "",
      entries:
        detail.entries?.map(item => ({
          subjectCode: item.subjectCode || "",
          subjectName: item.subjectName || "",
          balanceDirection: item.balanceDirection || "",
          summary: item.summary || "",
          debit: Number(item.debit || 0),
          credit: Number(item.credit || 0),
        })) || [],
  });
  if (form.entries.length < 4) {
    while (form.entries.length < 4) {
      form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
    if (form.entries.length < 2) {
      while (form.entries.length < 2) {
        form.entries.push(createEmptyEntry());
    }
  }
  selectedRowIndex.value = 0;
  dialogVisible.value = true;
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const view = (row) => {
@@ -468,13 +565,10 @@
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "posted";
    }
  }).then(async () => {
    await postVoucher({ id: row.id });
    ElMessage.success("过账成功");
    getTableData();
    await getTableData();
  });
};
@@ -483,13 +577,10 @@
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "cancelled";
    }
  }).then(async () => {
    await cancelVoucher({ id: row.id });
    ElMessage.success("作废成功");
    getTableData();
    await getTableData();
  });
};
@@ -502,45 +593,74 @@
};
const submitForm = () => {
  formRef.value.validate((valid) => {
  formRef.value.validate(async valid => {
    if (valid) {
      // å‰ç½®æ ¡éªŒï¼šä¸ŽåŽç«¯è§„则对齐,减少无效请求
      if (!isBalanced.value) {
        ElMessage.error("借贷不平衡,请检查分录");
        return;
      }
      const validEntries = form.entries.filter(e => e.subjectCode && (e.debit > 0 || e.credit > 0));
      const validEntries = form.entries.filter(
        entry => entry.subjectCode && (Number(entry.debit) > 0 || Number(entry.credit) > 0)
      );
      if (validEntries.length === 0) {
        ElMessage.error("请至少填写一条有效分录");
        return;
      }
      const invalidEntry = validEntries.find(
        entry => Number(entry.debit) > 0 && Number(entry.credit) > 0
      );
      if (invalidEntry) {
        ElMessage.error("同一分录不能同时填写借方和贷方");
        return;
      }
      const summary = validEntries.find(e => e.debit > 0)?.summary || "";
      const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`;
      const dataToSave = {
        ...form,
        voucherNo,
        voucherDate: form.voucherDate,
        summary,
        creator: form.creator,
        attachmentCount: Number(form.attachmentCount || 0),
        remark: form.remark,
        debit: totalDebitEntry.value,
        credit: totalCreditEntry.value,
        entries: validEntries,
        entries: validEntries.map(entry => ({
          subjectCode: entry.subjectCode,
          subjectName: entry.subjectName,
          summary: entry.summary,
          debit: Number(entry.debit || 0),
          credit: Number(entry.credit || 0),
        })),
      };
      try {
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...dataToSave };
        }
          await updateVoucher({
            id: currentId.value,
            ...dataToSave,
          });
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...dataToSave, status: "unposted" });
          await addVoucher(dataToSave);
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
    }
  });
};
onMounted(() => {
  getTableData();
onMounted(async () => {
  await loadSubjectList();
  await getTableData();
});
</script>
@@ -780,7 +900,8 @@
    .col-subject {
      position: relative;
      .el-select {
      .el-select,
      .el-tree-select {
        .el-input input {
          font-size: 12px;
        }