huminmin
2026-05-14 5a520798abc9bef73c308edaa5d606ecdd7ce72a
Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventory-management into dev_西宁_青铝绿行
已添加16个文件
已修改29个文件
5596 ■■■■■ 文件已修改
FINANCIAL_MANAGEMENT_BACKEND_SPEC.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/DYKJfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYZCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/KYHGfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/WTXCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/DYKJLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYZCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/KYHGLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/WTXCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customer.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/accountPurchase.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/accountSales.js 19 ●●●●● 补丁 | 查看 | 原始文档 | 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/api/procurementManagement/purchase_return_order.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/logo/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/fixedAssets.vue 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/intangibleAssets.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/generalLedger/index.vue 172 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/purchaseIn.vue 326 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/purchaseReturn.vue 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/salesOut.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/salesReturn.vue 308 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/detailLedger.vue 434 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/generalLedger.vue 382 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/index.vue 439 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/New.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/index.vue 487 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 844 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/detailDia.vue 260 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/formDia.vue 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | 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. è¡¥æµ‹è¯•:
   - å€Ÿè´·å¹³è¡¡æ ¡éªŒ
   - æŠ˜æ—§/摊销公式
   - ç§‘目被引用禁止删除
multiple/assets/favicon/DYKJfavicon.ico
multiple/assets/favicon/HYZCfavicon.ico
multiple/assets/favicon/KYHGfavicon.ico
multiple/assets/favicon/WTXCfavicon.ico
multiple/assets/logo/DYKJLogo.png
multiple/assets/logo/HYZCLogo.png
multiple/assets/logo/KYHGLogo.png
multiple/assets/logo/WTXCLogo.png
multiple/config.json
@@ -45,8 +45,8 @@
  "BTYX": {
    "env": {
      "VITE_APP_TITLE": "河南帮太优选进出口有限公司",
      "VITE_BASE_API": "http://127.0.0.1:9001",
      "VITE_JAVA_API": "http://127.0.0.1:9000"
      "VITE_BASE_API": "http://1.15.17.182:9056",
      "VITE_JAVA_API": "http://1.15.17.182:9057"
    },
    "logo": "logo/BTYXLogo.png",
    "favicon": "favicon/BTYXfavicon.ico"
@@ -60,6 +60,42 @@
    "logo": "logo/ZXZNLogo.png",
    "favicon": "favicon/ZXZNfavicon.ico"
  },
  "HYZC": {
    "env": {
      "VITE_APP_TITLE": "山西华亿众成建材有限公司",
      "VITE_BASE_API": "http://36.137.13.103:9001",
      "VITE_JAVA_API": "http://36.137.13.103:9000"
    },
    "logo": "logo/HYZCLogo.png",
    "favicon": "favicon/HYZCfavicon.ico"
  },
  "WTXC": {
    "env": {
      "VITE_APP_TITLE": "宁夏万通新材",
      "VITE_BASE_API": "http://42.63.71.140:9001",
      "VITE_JAVA_API": "http://42.63.71.140:9000"
    },
    "logo": "logo/WTXCLogo.png",
    "favicon": "favicon/WTXCfavicon.ico"
  },
  "KYHG": {
    "env": {
      "VITE_APP_TITLE": "山西坤源化工有限公司",
      "VITE_BASE_API": "http://36.137.13.29:9001",
      "VITE_JAVA_API": "http://36.137.13.29:9000"
    },
    "logo": "logo/KYHGLogo.png",
    "favicon": "favicon/KYHGfavicon.ico"
  },
  "DYKJ": {
    "env": {
      "VITE_APP_TITLE": "山西德益科技有限公司",
      "VITE_BASE_API": "http://36.137.12.37:9001",
      "VITE_JAVA_API": "http://36.137.12.37:9000"
    },
    "logo": "logo/DYKJLogo.png",
    "favicon": "favicon/DYKJfavicon.ico"
  },
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
public/favicon.ico

src/api/basicData/customer.js
@@ -26,6 +26,14 @@
    })
}
// æµå…¥å…¬æµ·
export function backCustomer(id) {
    return request({
        url: '/basic/customer/back/' + id,
        method: 'post'
    })
}
export function shareCustomer(data) {
    return request({
        url: '/basic/customer/together',
src/api/financialManagement/accountPurchase.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import request from "@/utils/request";
/** é‡‡è´­å…¥åº“分页列表 */
export const listPageAccountPurchase = (params) => {
  return request({
    url: "/accountPurchase/listPageAccountPurchase",
    method: "get",
    params,
  });
};
/** é‡‡è´­é€€è´§åˆ†é¡µåˆ—表 */
export const listPageAccountPurchaseReturn = (params) => {
  return request({
    url: "/accountPurchase/listPageAccountPurchaseReturn",
    method: "get",
    params,
  });
};
src/api/financialManagement/accountSales.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import request from "@/utils/request";
/** é”€å”®å‡ºåº“分页列表 */
export const listPageAccountSales = (params) => {
  return request({
    url: "/accountSales/listPageAccountSales",
    method: "get",
    params,
  });
};
/** é”€å”®é€€è´§åˆ†é¡µåˆ—表 */
export const listPageAccountSalesReturn = (params) => {
  return request({
    url: "/accountSales/listPageAccountSalesReturn",
    method: "get",
    params,
  });
};
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/api/procurementManagement/purchase_return_order.js
@@ -19,6 +19,15 @@
    });
}
// æ ¹æ®é‡‡è´­å°è´¦ ID æŸ¥è¯¢å¯é€€äº§å“ç­‰ä¿¡æ¯
export function getPurchaseReturnOrderByPurchaseLedgerId(query) {
    return request({
        url: "/purchaseReturnOrders/getByPurchaseLedgerId",
        method: "get",
        params: query,
    });
}
// æŸ¥çœ‹è¯¦æƒ…
// purchaseReturnOrders/selectById/xxx
export function getPurchaseReturnOrderDetail(id) {
src/assets/logo/logo.png

src/router/index.js
@@ -183,7 +183,12 @@
        name: "PurchaseIn",
        meta: { title: "采购入库" },
      },
      {
        path: "purchase-return",
        component: () => import("@/views/financialManagement/payable/purchaseReturn.vue"),
        name: "PurchaseReturn",
        meta: { title: "采购退货" },
      },
      {
        path: "input-invoice",
        component: () => import("@/views/financialManagement/payable/input-invoice.vue"),
src/views/basicData/customerFile/index.vue
@@ -27,6 +27,9 @@
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增客户</el-button>
        <el-button type="primary"
                   plain
                   @click="back">流入公海</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="info"
                   plain
@@ -619,7 +622,7 @@
    addReturnVisit,
    getReturnVisit,
  } from "@/api/basicData/customerFile.js";
  import {listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer} from "@/api/basicData/customer.js";
  import {listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, backCustomer} from "@/api/basicData/customer.js";
  import { ElMessageBox } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
  import useUserStore from "@/store/modules/user";
@@ -1126,6 +1129,36 @@
      });
  };
  const back = () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    const ids = selectedRows.value.map(item => item.id);
    ElMessageBox.confirm("选中的客户将流入公海,是否确认?", "流入公海提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        tableLoading.value = true;
        return Promise.all(ids.map(id => backCustomer(id)))
          .then(() => {
            proxy.$modal.msgSuccess("流入公海成功");
            selectedRows.value = [];
            getList();
          })
          .finally(() => {
            tableLoading.value = false;
          });
      })
      .catch(error => {
        if (error === "cancel" || error === "close") {
          proxy.$modal.msg("已取消");
        }
      });
  };
  // æ‰“开回访提醒弹窗
  const openReminderDialog = row => {
    currentCustomerId.value = row.id;
src/views/basicData/product/index.vue
@@ -43,12 +43,12 @@
                <el-button type="primary"
                           link
                           :disabled="isTopLevelNode(data, node)"
                           @click="openProDia('edit', data)">
                           @click="openProDia('edit', data, node)">
                  ç¼–辑
                </el-button>
                <el-button type="primary"
                           link
                           @click="openProDia('add', data)">
                           @click="openProDia('add', data, node)">
                  æ·»åŠ äº§å“
                </el-button>
                <el-button v-if="!node.childNodes.length"
@@ -285,6 +285,8 @@
  const search = ref("");
  const currentId = ref("");
  const currentParentId = ref("");
  /** äº§å“å¼¹çª—:add å­˜çˆ¶èŠ‚ç‚¹ id;edit å­˜å½“前节点 id ä¸Ž parentId(不依赖树选中项) */
  const productDialogTarget = ref(null);
  const operationType = ref("");
  const treeLoad = ref(false);
  const list = ref([]);
@@ -388,17 +390,28 @@
    return [null, undefined, "", 0, "0"].includes(data?.parentId);
  };
  // æ‰“开产品弹框
  const openProDia = (type, data) => {
    if (data && type === "edit" && isTopLevelNode(data)) {
  const openProDia = (type, data, node) => {
    if (data && type === "edit" && isTopLevelNode(data, node)) {
      proxy.$modal.msgWarning("一级节点不能编辑或删除");
      return;
    }
    operationType.value = type;
    productDia.value = true;
    form.value.productName = "";
    if (type === "edit") {
      form.value.productName = data.productName;
    productDialogTarget.value = null;
    if (type === "add" && data) {
      productDialogTarget.value = { parentId: data.id };
    } else if (type === "edit" && data) {
      let parentId = data.parentId;
      if (
        [null, undefined, ""].includes(parentId) &&
        node?.parent?.data?.id != null
      ) {
        parentId = node.parent.data.id;
      }
      productDialogTarget.value = { id: data.id, parentId };
    }
    productDia.value = true;
    form.value.productName =
      type === "edit" && data ? data.productName : "";
  };
  // æ‰“开规格型号弹框
  const openModelDia = (type, data) => {
@@ -417,14 +430,16 @@
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        if (operationType.value === "add") {
          form.value.parentId = currentId.value;
          form.value.parentId =
            productDialogTarget.value?.parentId ?? currentId.value;
          form.value.id = "";
        } else if (operationType.value === "addOne") {
          form.value.id = "";
          form.value.parentId = "";
        } else {
          form.value.id = currentId.value;
          form.value.parentId = "";
          form.value.id =
            productDialogTarget.value?.id ?? currentId.value;
          form.value.parentId = productDialogTarget.value?.parentId ?? "";
        }
        addOrEditProduct(form.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
@@ -437,6 +452,7 @@
  // å…³é—­äº§å“å¼¹æ¡†
  const closeProDia = () => {
    proxy.$refs.formRef.resetFields();
    productDialogTarget.value = null;
    productDia.value = false;
  };
src/views/financialManagement/assets/fixedAssets.vue
@@ -43,6 +43,7 @@
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
@@ -50,6 +51,7 @@
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
        <template #originalValue="{ row }">
@@ -76,7 +78,7 @@
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
      <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="资产编号" prop="assetCode">
@@ -178,7 +180,7 @@
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitForm">确定</el-button>
        <el-button v-if="!isView" type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
@@ -189,6 +191,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,23 +219,30 @@
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([]);
const multipleList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const isView = ref(false);
const currentId = ref(null);
const selectedIds = computed(() =>
  multipleList.value
    .map(item => item?.id)
    .filter(id => id !== undefined && id !== null && id !== "")
);
const form = reactive({
const createDefaultForm = () => ({
  assetCode: "",
  assetName: "",
  category: "",
@@ -244,6 +260,10 @@
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const rules = {
  assetName: [{ required: true, message: "请输入资产名称", trigger: "blur" }],
  category: [{ required: true, message: "请选择资产类别", trigger: "change" }],
@@ -251,13 +271,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 +301,44 @@
};
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 || [];
    multipleList.value = [];
    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 handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
const resetFilters = () => {
@@ -334,39 +356,32 @@
  getTableData();
};
const buildAssetCode = () => `GD${Date.now().toString().slice(-10)}`;
const add = () => {
  isEdit.value = false;
  isView.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;
};
const edit = (row) => {
  isEdit.value = true;
  isView.value = false;
  currentId.value = row.id;
  dialogTitle.value = "编辑固定资产";
  Object.assign(form, row);
  Object.assign(form, createDefaultForm(), row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看资产: ${row.assetName}`);
  edit(row);
  isView.value = true;
};
const handleDelete = (row) => {
@@ -374,31 +389,30 @@
    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();
  });
};
const handleDepreciation = () => {
  ElMessageBox.confirm("确认进行本月折旧计提吗?", "提示", {
  const ids = selectedIds.value;
  const confirmText = ids.length
    ? `确认对选中的 ${ids.length} æ¡èµ„产进行本月折旧计提吗?`
    : "确认进行本月折旧计提吗?";
  ElMessageBox.confirm(confirmText, "提示", {
    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({ ids });
    ElMessage.success("折旧计提完成");
    getTableData();
    await getTableData();
  });
};
@@ -407,22 +421,28 @@
};
const submitForm = () => {
  formRef.value.validate((valid) => {
  if (isView.value) {
    dialogVisible.value = false;
    return;
  }
  formRef.value.validate(async valid => {
    if (valid) {
      calculateNetValue();
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
      try {
        calculateNetValue();
        const payload = { ...form };
        if (isEdit.value) {
          payload.id = currentId.value;
          await updateFixedAsset(payload);
          ElMessage.success("编辑成功");
        } else {
          await addFixedAsset(payload);
          ElMessage.success("新增成功");
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
        ElMessage.success("新增成功");
        dialogVisible.value = false;
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
src/views/financialManagement/assets/intangibleAssets.vue
@@ -44,6 +44,7 @@
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
@@ -51,6 +52,7 @@
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
        <template #originalValue="{ row }">
@@ -77,7 +79,7 @@
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
      <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="资产编号" prop="assetCode">
@@ -171,7 +173,7 @@
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitForm">确定</el-button>
        <el-button v-if="!isView" type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
@@ -182,6 +184,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,23 +212,30 @@
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([]);
const multipleList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const isView = ref(false);
const currentId = ref(null);
const selectedIds = computed(() =>
  multipleList.value
    .map(item => item?.id)
    .filter(id => id !== undefined && id !== null && id !== "")
);
const form = reactive({
const createDefaultForm = () => ({
  assetCode: "",
  assetName: "",
  category: "",
@@ -236,6 +252,10 @@
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const rules = {
  assetName: [{ required: true, message: "请输入资产名称", trigger: "blur" }],
  category: [{ required: true, message: "请选择资产类别", trigger: "change" }],
@@ -243,13 +263,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 +294,49 @@
};
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 || [];
    multipleList.value = [];
    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 handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
const resetFilters = () => {
@@ -327,38 +354,32 @@
  getTableData();
};
const buildAssetCode = () => `WX${Date.now().toString().slice(-10)}`;
const add = () => {
  isEdit.value = false;
  isView.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;
};
const edit = (row) => {
  isEdit.value = true;
  isView.value = false;
  currentId.value = row.id;
  dialogTitle.value = "编辑无形资产";
  Object.assign(form, row);
  Object.assign(form, createDefaultForm(), row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看资产: ${row.assetName}`);
  edit(row);
  isView.value = true;
};
const handleDelete = (row) => {
@@ -366,35 +387,30 @@
    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();
  });
};
const handleAmortization = () => {
  ElMessageBox.confirm("确认进行本月摊销计提吗?", "提示", {
  const ids = selectedIds.value;
  const confirmText = ids.length
    ? `确认对选中的 ${ids.length} æ¡èµ„产进行本月摊销计提吗?`
    : "确认进行本月摊销计提吗?";
  ElMessageBox.confirm(confirmText, "提示", {
    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({ ids });
    ElMessage.success("摊销计提完成");
    getTableData();
    await getTableData();
  });
};
@@ -403,22 +419,28 @@
};
const submitForm = () => {
  formRef.value.validate((valid) => {
  if (isView.value) {
    dialogVisible.value = false;
    return;
  }
  formRef.value.validate(async valid => {
    if (valid) {
      calculateNetValue();
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
      try {
        calculateNetValue();
        const payload = { ...form };
        if (isEdit.value) {
          payload.id = currentId.value;
          await updateIntangibleAsset(payload);
          ElMessage.success("编辑成功");
        } else {
          await addIntangibleAsset(payload);
          ElMessage.success("新增成功");
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
        ElMessage.success("新增成功");
        dialogVisible.value = false;
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
src/views/financialManagement/generalLedger/index.vue
@@ -48,16 +48,58 @@
                     icon="Download">导出</el-button>
        </div>
      </div>
      <PIMTable rowKey="id"
                :column="columns"
                :tableData="dataList"
                :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
                @pagination="changePage">
      </PIMTable>
      <el-table ref="tableRef"
                v-loading="loading"
                :data="dataList"
                row-key="id"
                :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
                height="calc(100vh - 280px)"
                border
                stripe
                highlight-current-row
                class="subject-table">
        <el-table-column label="科目编码" prop="subjectCode" width="140">
          <template #default="scope">
            <span class="subject-code">{{ scope.row.subjectCode }}</span>
          </template>
        </el-table-column>
        <el-table-column label="科目名称" prop="subjectName" min-width="180">
          <template #default="scope">
            <span class="subject-name" :class="{ 'is-parent': scope.row.children?.length > 0 }">
              {{ scope.row.subjectName }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="科目类型" prop="subjectType" width="100" align="center">
          <template #default="scope">
            <el-tag size="small" :type="getSubjectTypeType(scope.row.subjectType)">
              {{ scope.row.subjectType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="余额方向" prop="balanceDirection" width="100" align="center">
          <template #default="scope">
            <el-tag size="small" :type="scope.row.balanceDirection === '借方' ? 'primary' : 'danger'">
              {{ scope.row.balanceDirection }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="状态" prop="status" width="80" align="center">
          <template #default="scope">
            <el-tag size="small" :type="scope.row.status === 0 || scope.row.status === '0' ? 'success' : 'info'">
              {{ scope.row.status === 0 || scope.row.status === '0' ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="备注" prop="remark" show-overflow-tooltip min-width="150" />
        <el-table-column label="操作" align="center" fixed="right" width="240">
          <template #default="scope">
            <el-button link type="primary" icon="Plus" @click="addChild(scope.row)">新增</el-button>
            <el-button link type="primary" icon="Edit" @click="edit(scope.row)">编辑</el-button>
            <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <FormDialog :title="dialogTitle"
                v-model="dialogVisible"
@@ -68,6 +110,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"
@@ -127,7 +173,7 @@
</template>
<script setup>
  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
  import { ref, reactive, onMounted, getCurrentInstance, nextTick } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import {
@@ -201,8 +247,15 @@
      label: "操作",
      align: "center",
      fixed: "right",
      width: "150",
      width: "220",
      operation: [
        {
          name: "新增",
          type: "primary",
          clickFun: row => {
            addChild(row);
          },
        },
        {
          name: "编辑",
          type: "primary",
@@ -224,11 +277,15 @@
  const dataList = ref([]);
  const dialogVisible = ref(false);
  const dialogTitle = ref("");
  const parentSubjectLabel = ref("顶级科目");
  const formRef = ref(null);
  const tableRef = ref(null);
  const isEdit = ref(false);
  const loading = ref(false);
  const form = reactive({
    id: undefined,
    parentId: null,
    subjectCode: "",
    subjectName: "",
    subjectType: "",
@@ -257,14 +314,17 @@
  };
  const getTableData = () => {
    loading.value = true;
    const query = {
      pageNum: pagination.currentPage,
      pageSize: pagination.pageSize,
      current: pagination.currentPage,
      size: pagination.pageSize,
      ...filters,
    };
    listAccountSubject(query).then(response => {
      dataList.value = response.data.records;
      pagination.total = response.data.total;
      dataList.value = response.data.records || [];
      loading.value = false;
    }).catch(() => {
      loading.value = false;
    });
  };
@@ -282,11 +342,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 +362,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;
  };
@@ -361,4 +470,29 @@
    justify-content: space-between;
    margin-bottom: 15px;
  }
  .subject-table {
    border-radius: 8px;
    overflow: hidden;
    :deep(.el-table__row) {
      transition: background-color 0.3s;
    }
    :deep(.el-table__row:hover) {
      background-color: #f5f7fa;
    }
    .subject-code {
      color: #606266;
    }
    .subject-name {
      font-weight: 500;
      &.is-parent {
        color: #409eff;
      }
    }
  }
</style>
src/views/financialManagement/payable/purchaseIn.vue
@@ -1,19 +1,27 @@
<template>
  <!-- é‡‡è´­å…¥åº“ -->
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="入库单号:">
        <el-input v-model="filters.inCode" placeholder="请输入入库单号" clearable style="width: 200px;" />
        <el-input v-model="filters.inboundBatches" placeholder="请输入入库单号" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="供应商:">
        <el-select v-model="filters.supplierId" placeholder="请选择供应商" clearable style="width: 200px;">
          <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
        </el-select>
        <el-input v-model="filters.supplierName" placeholder="请输入供应商" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="入库日期:">
        <el-date-picker v-model="filters.dateRange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" clearable />
        <el-date-picker
          v-model="filters.dateRange"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          clearable
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button type="primary" @click="onSearch">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
@@ -28,6 +36,7 @@
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :tableLoading="tableLoading"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
@@ -35,107 +44,28 @@
        }"
        @pagination="changePage"
      >
        <template #amount="{ row }">
          <span class="text-primary">Â¥{{ formatMoney(row.amount) }}</span>
        </template>
        <template #status="{ row }">
          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
        </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 === 'pending'">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)" v-if="row.status === 'pending'">删除</el-button>
        <template #inboundDate="{ row }">
          {{ row.InboundDate || row.inboundDate || "" }}
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="入库单号" prop="inCode">
              <el-input v-model="form.inCode" placeholder="请输入入库单号" :disabled="isEdit" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="供应商" prop="supplierId">
              <el-select v-model="form.supplierId" placeholder="请选择供应商" style="width: 100%;" :disabled="isEdit">
                <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="入库日期" prop="inDate">
              <el-date-picker v-model="form.inDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="入库金额" prop="amount">
              <el-input-number v-model="form.amount" :min="0" :precision="2" style="width: 100%;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="入库明细" prop="details">
          <el-table :data="form.details" border style="width: 100%">
            <el-table-column prop="materialName" label="物料名称" width="150">
              <template #default="{ $index }">
                <el-input v-model="form.details[$index].materialName" placeholder="物料名称" />
              </template>
            </el-table-column>
            <el-table-column prop="spec" label="规格" width="120">
              <template #default="{ $index }">
                <el-input v-model="form.details[$index].spec" placeholder="规格" />
              </template>
            </el-table-column>
            <el-table-column prop="quantity" label="数量" width="100">
              <template #default="{ $index }">
                <el-input-number v-model="form.details[$index].quantity" :min="0" style="width: 100%;" />
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价" width="120">
              <template #default="{ $index }">
                <el-input-number v-model="form.details[$index].unitPrice" :min="0" :precision="2" style="width: 100%;" />
              </template>
            </el-table-column>
            <el-table-column prop="total" label="金额" width="120">
              <template #default="{ row }">
                <span>Â¥{{ formatMoney(row.quantity * row.unitPrice) }}</span>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80">
              <template #default="{ $index }">
                <el-button type="danger" link @click="removeDetail($index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <el-button type="primary" link @click="addDetail" style="margin-top: 10px;">+ æ·»åŠ æ˜Žç»†</el-button>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { ElMessage } from "element-plus";
import { listPageAccountPurchase } from "@/api/financialManagement/accountPurchase";
defineOptions({
  name: "采购入库",
});
const { proxy } = getCurrentInstance();
const filters = reactive({
  inCode: "",
  supplierId: "",
  inboundBatches: "",
  supplierName: "",
  dateRange: [],
});
@@ -146,170 +76,85 @@
});
const columns = [
  { label: "入库单号", prop: "inCode", width: "150" },
  { label: "供应商", prop: "supplierName", width: "180" },
  { label: "入库日期", prop: "inDate", width: "120" },
  { label: "入库金额", prop: "amount", slot: "amount" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "备注", prop: "remark", showOverflowTooltip: true },
  { label: "操作", prop: "operation", slot: "operation", width: "200", fixed: "right" },
  { label: "入库单号", prop: "inboundBatches", minWidth: "150" },
  { label: "供应商", prop: "supplierName", minWidth: "180" },
  {
    label: "入库日期",
    prop: "InboundDate",
    minWidth: "170",
    dataType: "slot",
    slot: "inboundDate",
  },
  { label: "产品名称", prop: "productName", minWidth: "140" },
  { label: "产品规格", prop: "specificationModel", minWidth: "140" },
  { label: "采购订单号", prop: "purchaseContractNumber", minWidth: "150" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const tableLoading = ref(false);
const supplierList = [
  { id: 1, name: "北京原材料供应商" },
  { id: 2, name: "上海电子元器件公司" },
  { id: 3, name: "广州包装材料厂" },
  { id: 4, name: "深圳五金配件公司" },
];
function buildFilterParams() {
  const params = {
    inboundBatches: filters.inboundBatches || undefined,
    supplierName: filters.supplierName || undefined,
  };
  if (filters.dateRange && filters.dateRange.length === 2) {
    params.startDate = filters.dateRange[0];
    params.endDate = filters.dateRange[1];
  }
  return params;
}
const form = reactive({
  inCode: "",
  supplierId: "",
  inDate: "",
  amount: 0,
  details: [],
  remark: "",
});
const rules = {
  inCode: [{ required: true, message: "请输入入库单号", trigger: "blur" }],
  supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }],
  inDate: [{ required: true, message: "请选择入库日期", trigger: "change" }],
  amount: [{ required: true, message: "请输入入库金额", trigger: "blur" }],
};
const mockData = [
  { id: 1, inCode: "RK2024001", supplierId: 1, supplierName: "北京原材料供应商", inDate: "2024-01-10", amount: 8000, status: "approved", details: [{ materialName: "钢材", spec: "Q235", quantity: 10, unitPrice: 500 }], remark: "" },
  { id: 2, inCode: "RK2024002", supplierId: 2, supplierName: "上海电子元器件公司", inDate: "2024-01-12", amount: 12000, status: "pending", details: [{ materialName: "芯片", spec: "STM32", quantity: 100, unitPrice: 80 }], remark: "" },
  { id: 3, inCode: "RK2024003", supplierId: 3, supplierName: "广州包装材料厂", inDate: "2024-01-15", amount: 3500, status: "approved", details: [{ materialName: "纸箱", spec: "50*40*30", quantity: 500, unitPrice: 5 }], remark: "" },
];
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const getStatusLabel = (status) => {
  const map = { pending: "待审核", approved: "已审核", rejected: "已驳回" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { pending: "warning", approved: "success", rejected: "danger" };
  return map[status] || "";
const onSearch = () => {
  pagination.currentPage = 1;
  getTableData();
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.inCode) {
    result = result.filter(item => item.inCode.includes(filters.inCode));
  }
  if (filters.supplierId) {
    result = result.filter(item => item.supplierId === filters.supplierId);
  }
  if (filters.dateRange && filters.dateRange.length === 2) {
    result = result.filter(item => item.inDate >= filters.dateRange[0] && item.inDate <= filters.dateRange[1]);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
  tableLoading.value = true;
  listPageAccountPurchase({
    ...buildFilterParams(),
    current: pagination.currentPage,
    size: pagination.pageSize,
  })
    .then((res) => {
      const ok = res.code === 200 || res.code === 0;
      if (ok && res.data) {
        pagination.total = res.data.total ?? 0;
        dataList.value = res.data.records ?? [];
      } else {
        ElMessage.error(res.msg || "查询失败");
        dataList.value = [];
      }
    })
    .catch(() => {
      dataList.value = [];
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const resetFilters = () => {
  filters.inCode = "";
  filters.supplierId = "";
  filters.inboundBatches = "";
  filters.supplierName = "";
  filters.dateRange = [];
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
  getTableData();
};
const addDetail = () => {
  form.details.push({ materialName: "", spec: "", quantity: 0, unitPrice: 0 });
};
const removeDetail = (index) => {
  form.details.splice(index, 1);
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增入库";
  Object.assign(form, {
    inCode: "RK" + Date.now().toString().slice(-8),
    supplierId: "",
    inDate: new Date().toISOString().split('T')[0],
    amount: 0,
    details: [{ materialName: "", spec: "", quantity: 0, unitPrice: 0 }],
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑入库";
  Object.assign(form, row);
  if (!form.details || form.details.length === 0) {
    form.details = [{ materialName: "", spec: "", quantity: 0, unitPrice: 0 }];
  }
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看入库单: ${row.inCode}`);
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该入库单吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const supplier = supplierList.find(item => item.id === form.supplierId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, supplierName: supplier?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, supplierName: supplier?.name, status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
  proxy.download(
    "/accountPurchase/exportAccountPurchaseInbound",
    buildFilterParams(),
    `采购入库_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
@@ -322,10 +167,5 @@
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
.text-primary {
  color: #409eff;
  font-weight: bold;
}
</style>
src/views/financialManagement/payable/purchaseReturn.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,239 @@
<template>
  <!-- é‡‡è´­é€€è´§ -->
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="退货单号:">
        <el-input
          v-model="filters.returnNo"
          placeholder="请输入退货单号"
          clearable
          style="width: 200px"
        />
      </el-form-item>
      <el-form-item label="供应商:">
        <el-input
          v-model="filters.supplierName"
          placeholder="请输入供应商"
          clearable
          style="width: 200px"
        />
      </el-form-item>
      <el-form-item label="退货日期:">
        <el-date-picker
          v-model="filters.dateRange"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          clearable
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSearch">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <el-button @click="handleOut" icon="Download">导出</el-button>
        </div>
      </div>
      <PIMTable
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :tableLoading="tableLoading"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { ElMessage } from "element-plus";
import { listPageAccountPurchaseReturn } from "@/api/financialManagement/accountPurchase";
defineOptions({
  name: "采购退货",
});
const { proxy } = getCurrentInstance();
const filters = reactive({
  returnNo: "",
  supplierName: "",
  dateRange: [],
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "退货单号", prop: "returnNo", minWidth: "150" },
  { label: "供应商", prop: "supplierName", minWidth: "180" },
  { label: "关联入库单号", prop: "inboundBatches", minWidth: "150" },
  { label: "退货日期", prop: "preparedAt", minWidth: "170" },
  {
    label: "退款总额",
    prop: "totalAmount",
    minWidth: "150",
    align: "right",
    formatData: (val) =>
      val === null || val === undefined || val === ""
        ? ""
        : Number(val).toLocaleString("zh-CN", {
            minimumFractionDigits: 2,
            maximumFractionDigits: 2,
          }),
  },
  { label: "退货方式", prop: "returnType", minWidth: "150" },
  { label: "采购订单号", prop: "purchaseContractNumber", minWidth: "150" },
];
const dataList = ref([]);
const tableLoading = ref(false);
function buildFilterParams() {
  const params = {
    returnNo: filters.returnNo || undefined,
    supplierName: filters.supplierName || undefined,
  };
  if (filters.dateRange && filters.dateRange.length === 2) {
    params.startDate = filters.dateRange[0];
    params.endDate = filters.dateRange[1];
  }
  return params;
}
const onSearch = () => {
  pagination.currentPage = 1;
  getTableData();
};
const getTableData = () => {
  tableLoading.value = true;
  listPageAccountPurchaseReturn({
    ...buildFilterParams(),
    current: pagination.currentPage,
    size: pagination.pageSize,
  })
    .then((res) => {
      const ok = res.code === 200 || res.code === 0;
      if (ok && res.data) {
        pagination.total = res.data.total ?? 0;
        dataList.value = res.data.records ?? [];
      } else {
        ElMessage.error(res.msg || "查询失败");
        dataList.value = [];
      }
    })
    .catch(() => {
      dataList.value = [];
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const resetFilters = () => {
  filters.returnNo = "";
  filters.supplierName = "";
  filters.dateRange = [];
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
  getTableData();
};
const handleOut = () => {
  proxy.download(
    "/accountPurchase/exportAccountPurchaseReturn",
    buildFilterParams(),
    `采购退货_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
</style>
src/views/financialManagement/receivable/salesOut.vue
@@ -1,19 +1,27 @@
<template>
<!-- é”€å”®å‡ºåº“ -->
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="出库单号:">
        <el-input v-model="filters.outCode" placeholder="请输入出库单号" clearable style="width: 200px;" />
        <el-input v-model="filters.outboundBatches" placeholder="请输入出库单号" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="客户:">
        <el-select v-model="filters.customerId" placeholder="请选择客户" clearable style="width: 200px;">
          <el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" />
        </el-select>
      <el-form-item label="客户名称:">
        <el-input v-model="filters.customerName" placeholder="请输入客户名称" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="出库日期:">
        <el-date-picker v-model="filters.dateRange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" clearable />
        <el-date-picker
          v-model="filters.dateRange"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          clearable
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button type="primary" @click="onSearch">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
@@ -28,76 +36,32 @@
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :tableLoading="tableLoading"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      >
        <template #status="{ row }">
          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
        </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 === 'pending'">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)" v-if="row.status === 'pending'">删除</el-button>
        </template>
      </PIMTable>
      />
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="出库单号" prop="outCode">
              <el-input v-model="form.outCode" placeholder="请输入出库单号" :disabled="isEdit" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户" prop="customerId">
              <el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%;" :disabled="isEdit">
                <el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="出库日期" prop="outDate">
              <el-date-picker v-model="form.outDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="金额" prop="amount">
              <el-input-number v-model="form.amount" :min="0" :precision="2" style="width: 100%;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { ElMessage } from "element-plus";
import { listPageAccountSales } from "@/api/financialManagement/accountSales";
defineOptions({
  name: "销售出库",
});
const { proxy } = getCurrentInstance();
const filters = reactive({
  outCode: "",
  customerId: "",
  outboundBatches: "",
  customerName: "",
  dateRange: [],
});
@@ -108,153 +72,87 @@
});
const columns = [
  { label: "出库单号", prop: "outCode", width: "150" },
  { label: "客户名称", prop: "customerName", width: "180" },
  { label: "出库日期", prop: "outDate", width: "120" },
  { label: "金额", prop: "amount", width: "120" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "备注", prop: "remark", showOverflowTooltip: true },
  { label: "操作", prop: "operation", slot: "operation", width: "200", fixed: "right" },
  { label: "出库单号", prop: "outboundBatches", minWidth: "150" },
  { label: "客户名称", prop: "customerName", minWidth: "180" },
  { label: "出库日期", prop: "shippingDate", width: "170" },
  { label: "产品名称", prop: "productName", minWidth: "140" },
  { label: "产品规格", prop: "specificationModel", minWidth: "140" },
  {
    label: "金额",
    prop: "outboundAmount",
    minWidth: "120",
    align: "right",
    formatData: (val) => (val === null || val === undefined || val === "" ? "" : Number(val).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })),
  },
  { label: "发货编号", prop: "shippingNo", minWidth: "140" },
  { label: "销售订单号", prop: "salesContractNo", minWidth: "150" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const tableLoading = ref(false);
const customerList = [
  { id: 1, name: "北京科技有限公司" },
  { id: 2, name: "上海贸易公司" },
  { id: 3, name: "广州实业有限公司" },
  { id: 4, name: "深圳电子公司" },
];
function buildFilterParams() {
  const params = {
    outboundBatches: filters.outboundBatches || undefined,
    customerName: filters.customerName || undefined,
  };
  if (filters.dateRange && filters.dateRange.length === 2) {
    params.startDate = filters.dateRange[0];
    params.endDate = filters.dateRange[1];
  }
  return params;
}
const form = reactive({
  outCode: "",
  customerId: "",
  outDate: "",
  amount: 0,
  remark: "",
});
const rules = {
  outCode: [{ required: true, message: "请输入出库单号", trigger: "blur" }],
  customerId: [{ required: true, message: "请选择客户", trigger: "change" }],
  outDate: [{ required: true, message: "请选择出库日期", trigger: "change" }],
  amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
};
const mockData = [
  { id: 1, outCode: "CK2024001", customerId: 1, customerName: "北京科技有限公司", outDate: "2024-01-15", amount: 5000, status: "approved", remark: "" },
  { id: 2, outCode: "CK2024002", customerId: 2, customerName: "上海贸易公司", outDate: "2024-01-16", amount: 8000, status: "pending", remark: "" },
  { id: 3, outCode: "CK2024003", customerId: 3, customerName: "广州实业有限公司", outDate: "2024-01-18", amount: 12000, status: "approved", remark: "" },
  { id: 4, outCode: "CK2024004", customerId: 4, customerName: "深圳电子公司", outDate: "2024-01-20", amount: 3500, status: "pending", remark: "" },
];
const getStatusLabel = (status) => {
  const map = { pending: "待审核", approved: "已审核", rejected: "已驳回" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { pending: "warning", approved: "success", rejected: "danger" };
  return map[status] || "";
const onSearch = () => {
  pagination.currentPage = 1;
  getTableData();
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.outCode) {
    result = result.filter(item => item.outCode.includes(filters.outCode));
  }
  if (filters.customerId) {
    result = result.filter(item => item.customerId === filters.customerId);
  }
  if (filters.dateRange && filters.dateRange.length === 2) {
    result = result.filter(item => item.outDate >= filters.dateRange[0] && item.outDate <= filters.dateRange[1]);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
  tableLoading.value = true;
  listPageAccountSales({
    ...buildFilterParams(),
    current: pagination.currentPage,
    size: pagination.pageSize,
  })
    .then((res) => {
      const ok = res.code === 200 || res.code === 0;
      if (ok && res.data) {
        pagination.total = res.data.total ?? 0;
        dataList.value = res.data.records ?? [];
      } else {
        ElMessage.error(res.msg || "查询失败");
        dataList.value = [];
      }
    })
    .catch(() => {
      dataList.value = [];
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const resetFilters = () => {
  filters.outCode = "";
  filters.customerId = "";
  filters.outboundBatches = "";
  filters.customerName = "";
  filters.dateRange = [];
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
  getTableData();
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增出库";
  Object.assign(form, {
    outCode: "CK" + Date.now(),
    customerId: "",
    outDate: "",
    amount: 0,
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑出库";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看出库单: ${row.outCode}`);
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const customer = customerList.find(item => item.id === form.customerId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, customerName: customer?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, customerName: customer?.name, status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该出库单吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
  proxy.download(
    "/accountSales/exportAccountSalesOutbound",
    buildFilterParams(),
    `销售出库_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
src/views/financialManagement/receivable/salesReturn.vue
@@ -1,19 +1,27 @@
<template>
  <!-- é”€å”®é€€è´§ -->
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="退货单号:">
        <el-input v-model="filters.returnCode" placeholder="请输入退货单号" clearable style="width: 200px;" />
        <el-input v-model="filters.returnNo" placeholder="请输入退货单号" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="客户:">
        <el-select v-model="filters.customerId" placeholder="请选择客户" clearable style="width: 200px;">
          <el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" />
        </el-select>
      <el-form-item label="客户名称:">
        <el-input v-model="filters.customerName" placeholder="请输入客户名称" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="退货日期:">
        <el-date-picker v-model="filters.dateRange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" clearable />
        <el-date-picker
          v-model="filters.dateRange"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          clearable
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button type="primary" @click="onSearch">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
@@ -28,90 +36,32 @@
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :tableLoading="tableLoading"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      >
        <template #status="{ row }">
          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
        </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 === 'pending'">编辑</el-button>
          <el-button type="success" link @click="handleAudit(row)" v-if="row.status === 'pending'">审核</el-button>
        </template>
      </PIMTable>
      />
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="退货单号" prop="returnCode">
              <el-input v-model="form.returnCode" placeholder="请输入退货单号" :disabled="isEdit" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="关联出库单" prop="outCode">
              <el-select v-model="form.outCode" placeholder="请选择出库单" style="width: 100%;" :disabled="isEdit">
                <el-option v-for="item in outList" :key="item.outCode" :label="item.outCode" :value="item.outCode" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="客户" prop="customerId">
              <el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%;" :disabled="isEdit">
                <el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="退货日期" prop="returnDate">
              <el-date-picker v-model="form.returnDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="退货金额" prop="amount">
              <el-input-number v-model="form.amount" :min="0" :precision="2" style="width: 100%;" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="退货原因" prop="reason">
              <el-input v-model="form.reason" placeholder="请输入退货原因" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { ElMessage } from "element-plus";
import { listPageAccountSalesReturn } from "@/api/financialManagement/accountSales";
defineOptions({
  name: "销售退货",
});
const { proxy } = getCurrentInstance();
const filters = reactive({
  returnCode: "",
  customerId: "",
  returnNo: "",
  customerName: "",
  dateRange: [],
});
@@ -122,173 +72,89 @@
});
const columns = [
  { label: "退货单号", prop: "returnCode", width: "150" },
  { label: "客户名称", prop: "customerName", width: "180" },
  { label: "关联出库单", prop: "outCode", width: "150" },
  { label: "退货日期", prop: "returnDate", width: "120" },
  { label: "退货金额", prop: "amount", width: "120" },
  { label: "退货原因", prop: "reason", width: "150", showOverflowTooltip: true },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "200", fixed: "right" },
  { label: "退货单号", prop: "returnNo", minWidth: "150" },
  { label: "客户名称", prop: "customerName", minWidth: "180" },
  { label: "关联发货单号", prop: "shippingNo", minWidth: "150" },
  { label: "退货日期", prop: "makeTime", minWidth: "170" },
  {
    label: "退款总额",
    prop: "refundAmount",
    minWidth: "120",
    align: "right",
    formatData: (val) =>
      val === null || val === undefined || val === ""
        ? ""
        : Number(val).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
  },
  { label: "退货原因", prop: "returnReason", minWidth: "150", showOverflowTooltip: true },
  { label: "销售订单号", prop: "salesContractNo", minWidth: "150" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const tableLoading = ref(false);
const customerList = [
  { id: 1, name: "北京科技有限公司" },
  { id: 2, name: "上海贸易公司" },
  { id: 3, name: "广州实业有限公司" },
  { id: 4, name: "深圳电子公司" },
];
function buildFilterParams() {
  const params = {
    returnNo: filters.returnNo || undefined,
    customerName: filters.customerName || undefined,
  };
  if (filters.dateRange && filters.dateRange.length === 2) {
    params.startDate = filters.dateRange[0];
    params.endDate = filters.dateRange[1];
  }
  return params;
}
const outList = [
  { outCode: "CK2024001", customerId: 1 },
  { outCode: "CK2024002", customerId: 2 },
  { outCode: "CK2024003", customerId: 3 },
];
const form = reactive({
  returnCode: "",
  outCode: "",
  customerId: "",
  returnDate: "",
  amount: 0,
  reason: "",
  remark: "",
});
const rules = {
  returnCode: [{ required: true, message: "请输入退货单号", trigger: "blur" }],
  outCode: [{ required: true, message: "请选择关联出库单", trigger: "change" }],
  customerId: [{ required: true, message: "请选择客户", trigger: "change" }],
  returnDate: [{ required: true, message: "请选择退货日期", trigger: "change" }],
  amount: [{ required: true, message: "请输入退货金额", trigger: "blur" }],
};
const mockData = [
  { id: 1, returnCode: "TH2024001", outCode: "CK2024001", customerId: 1, customerName: "北京科技有限公司", returnDate: "2024-01-20", amount: 1000, reason: "质量问题", status: "approved", remark: "" },
  { id: 2, returnCode: "TH2024002", outCode: "CK2024002", customerId: 2, customerName: "上海贸易公司", returnDate: "2024-01-22", amount: 500, reason: "规格不符", status: "pending", remark: "" },
];
const getStatusLabel = (status) => {
  const map = { pending: "待审核", approved: "已审核", rejected: "已驳回" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { pending: "warning", approved: "success", rejected: "danger" };
  return map[status] || "";
const onSearch = () => {
  pagination.currentPage = 1;
  getTableData();
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.returnCode) {
    result = result.filter(item => item.returnCode.includes(filters.returnCode));
  }
  if (filters.customerId) {
    result = result.filter(item => item.customerId === filters.customerId);
  }
  if (filters.dateRange && filters.dateRange.length === 2) {
    result = result.filter(item => item.returnDate >= filters.dateRange[0] && item.returnDate <= filters.dateRange[1]);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
  tableLoading.value = true;
  listPageAccountSalesReturn({
    ...buildFilterParams(),
    current: pagination.currentPage,
    size: pagination.pageSize,
  })
    .then((res) => {
      const ok = res.code === 200 || res.code === 0;
      if (ok && res.data) {
        pagination.total = res.data.total ?? 0;
        dataList.value = res.data.records ?? [];
      } else {
        ElMessage.error(res.msg || "查询失败");
        dataList.value = [];
      }
    })
    .catch(() => {
      dataList.value = [];
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const resetFilters = () => {
  filters.returnCode = "";
  filters.customerId = "";
  filters.returnNo = "";
  filters.customerName = "";
  filters.dateRange = [];
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
  getTableData();
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增退货";
  Object.assign(form, {
    returnCode: "TH" + Date.now(),
    outCode: "",
    customerId: "",
    returnDate: "",
    amount: 0,
    reason: "",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑退货";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看退货单: ${row.returnCode}`);
};
const handleAudit = (row) => {
  ElMessageBox.confirm("确认审核通过该退货单吗?", "提示", {
    confirmButtonText: "通过",
    cancelButtonText: "驳回",
    distinguishCancelAndClose: true,
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "approved";
    }
    ElMessage.success("审核通过");
    getTableData();
  }).catch((action) => {
    if (action === "cancel") {
      const index = mockData.findIndex(item => item.id === row.id);
      if (index !== -1) {
        mockData[index].status = "rejected";
      }
      ElMessage.warning("已驳回");
      getTableData();
    }
  });
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const customer = customerList.find(item => item.id === form.customerId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, customerName: customer?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, customerName: customer?.name, status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
  proxy.download(
    "/accountSales/exportAccountSalesReturn",
    buildFilterParams(),
    `销售退货_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
src/views/financialManagement/voucher/detailLedger.vue
@@ -1,165 +1,126 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="会计科目:">
        <el-cascader v-model="filters.subject" :options="subjectOptions" :props="{ label: 'name', value: 'code' }" placeholder="请选择会计科目" clearable style="width: 250px;" filterable />
      </el-form-item>
      <el-form-item label="辅助核算:">
        <el-select v-model="filters.auxiliary" placeholder="请选择辅助核算" clearable style="width: 180px;">
          <el-option label="客户" value="customer" />
          <el-option label="供应商" value="supplier" />
          <el-option label="部门" value="department" />
          <el-option label="员工" value="employee" />
          <el-option label="项目" value="project" />
        </el-select>
      </el-form-item>
      <el-form-item label="核算对象:">
        <el-select v-model="filters.auxiliaryItem" placeholder="请选择核算对象" clearable style="width: 200px;" :disabled="!filters.auxiliary">
          <el-option v-for="item in auxiliaryItems" :key="item.value" :label="item.label" :value="item.value" />
        </el-select>
      </el-form-item>
      <el-form-item label="期间:">
        <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
        <span style="margin: 0 10px;">至</span>
        <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">查询</el-button>
        <el-button @click="resetFilters">重置</el-button>
        <el-button @click="handlePrint" icon="Printer">打印</el-button>
        <el-button @click="handleOut" icon="Download">导出</el-button>
      </el-form-item>
    </el-form>
  <div class="app-container ledger-page">
    <div class="ledger-layout">
      <aside class="subject-panel">
        <el-input v-model="subjectKeyword" placeholder="请输入科目名称/编号" clearable prefix-icon="Search" />
        <el-scrollbar class="subject-tree-scroll">
          <el-tree
            ref="subjectTreeRef"
            :data="subjectOptions"
            node-key="code"
            :props="{ label: 'name', children: 'children' }"
            highlight-current
            default-expand-all
            :expand-on-click-node="false"
            :filter-node-method="filterSubjectNode"
            @node-click="handleSubjectClick"
          >
            <template #default="{ data }">
              <span class="subject-node">{{ data.code }} {{ data.name }}</span>
            </template>
          </el-tree>
        </el-scrollbar>
      </aside>
    <div class="ledger-header" v-if="currentSubject">
      <h2>科目明细账</h2>
      <p>科目: {{ currentSubject.code }} {{ currentSubject.name }}</p>
      <p v-if="filters.auxiliary && filters.auxiliaryItem">辅助核算: {{ getAuxiliaryLabel() }}</p>
      <p>期间: {{ filters.startMonth }} è‡³ {{ filters.endMonth }}</p>
      <section class="ledger-content">
        <el-form :model="filters" :inline="true" class="filter-form">
          <el-form-item label="期间:">
            <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
            <span style="margin: 0 10px;">至</span>
            <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="getTableData">查询</el-button>
            <el-button @click="resetFilters">重置</el-button>
            <el-button @click="handlePrint" icon="Printer">打印</el-button>
            <el-button @click="handleOut" icon="Download">导出</el-button>
          </el-form-item>
        </el-form>
        <div class="table_list">
          <el-table :data="dataList" border style="width: 100%">
            <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 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 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>
              </template>
            </el-table-column>
            <el-table-column label="方向" width="80">
              <template #default="{ row }">
                <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="余额" width="150">
              <template #default="{ row }">
                <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
      </section>
    </div>
    <div class="table_list">
      <el-table :data="dataList" border style="width: 100%" show-summary :summary-method="getSummaries">
        <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">
          <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">
          <template #default="{ row }">
            <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="方向" width="80">
          <template #default="{ row }">
            <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="余额" width="150">
          <template #default="{ row }">
            <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch } from "vue";
import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import { getDetailLedger } from "@/api/financialManagement/ledger";
defineOptions({
  name: "科目明细账",
});
const filters = reactive({
  subject: [],
  auxiliary: "",
  auxiliaryItem: "",
  startMonth: "2024-01",
  endMonth: "2024-03",
  subject: "",
  startMonth: "",
  endMonth: "",
});
const dataList = ref([]);
const subjectOptions = ref([]);
const subjectKeyword = ref("");
const subjectTreeRef = 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 getPreviousMonth = () => {
  const date = new Date();
  date.setDate(1);
  date.setMonth(date.getMonth() - 1);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  return `${year}-${month}`;
};
const defaultMonth = getPreviousMonth();
filters.startMonth = defaultMonth;
filters.endMonth = defaultMonth;
const fallbackSubjects = [
  { code: "1122", name: "应收账款" },
  { code: "2202", name: "应付账款" },
  { code: "6602", name: "管理费用" },
];
const auxiliaryItems = computed(() => {
  const map = {
    customer: [
      { value: "1", label: "北京科技有限公司" },
      { value: "2", label: "上海贸易公司" },
      { value: "3", label: "广州实业有限公司" },
    ],
    supplier: [
      { value: "1", label: "北京原材料供应商" },
      { value: "2", label: "上海电子元器件公司" },
      { value: "3", label: "广州包装材料厂" },
    ],
    department: [
      { value: "1", label: "财务部" },
      { value: "2", label: "销售部" },
      { value: "3", label: "采购部" },
    ],
    employee: [
      { value: "1", label: "张三" },
      { value: "2", label: "李四" },
      { value: "3", label: "王五" },
    ],
    project: [
      { value: "1", label: "项目A" },
      { value: "2", label: "项目B" },
      { value: "3", label: "项目C" },
    ],
  };
  return map[filters.auxiliary] || [];
});
watch(() => filters.auxiliary, () => {
  filters.auxiliaryItem = "";
});
const currentSubject = computed(() => {
  if (!filters.subject || filters.subject.length === 0) return null;
  const code = filters.subject[filters.subject.length - 1];
  return findSubject(subjectOptions, code);
});
const toTree = (nodes = []) =>
  nodes
    .filter(item => item.subjectCode && item.subjectName)
    .map(item => ({
      code: item.subjectCode,
      name: item.subjectName,
      children: toTree(item.children || []),
    }));
const findSubject = (options, code) => {
  for (const item of options) {
@@ -172,9 +133,68 @@
  return null;
};
const getAuxiliaryLabel = () => {
  const item = auxiliaryItems.value.find(i => i.value === filters.auxiliaryItem);
  return item ? item.label : "";
const currentSubject = computed(() => {
  if (!filters.subject) return null;
  return findSubject(subjectOptions.value, filters.subject);
});
const getFirstSubjectCode = (nodes = []) => {
  for (const item of nodes) {
    if (item.code) return item.code;
    if (item.children && item.children.length > 0) {
      const childCode = getFirstSubjectCode(item.children);
      if (childCode) return childCode;
    }
  }
  return "";
};
const setDefaultSubjectSelection = async () => {
  const firstCode = getFirstSubjectCode(subjectOptions.value);
  if (!firstCode) {
    filters.subject = "";
    subjectTreeRef.value?.setCurrentKey(null);
    return;
  }
  filters.subject = firstCode;
  await nextTick();
  subjectTreeRef.value?.setCurrentKey(firstCode);
};
const filterSubjectNode = (value, data) => {
  const keyword = value?.trim();
  if (!keyword) return true;
  return `${data.code}${data.name}`.includes(keyword);
};
watch(subjectKeyword, (value) => {
  subjectTreeRef.value?.filter(value || "");
});
const handleSubjectClick = async (data) => {
  filters.subject = data.code;
  await getTableData();
};
const loadSubjectOptions = async () => {
  let options = [];
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
    });
    options = toTree(data?.records || []);
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  if (options.length === 0) {
    options = fallbackSubjects.map(item => ({ ...item, children: [] }));
  }
  subjectOptions.value = options;
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const formatMoney = (value) => {
@@ -182,63 +202,34 @@
  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 = () => {
// è”调约定:明细账按科目与期间过滤
const getTableData = async () => {
  if (!currentSubject.value) {
    dataList.value = [];
    return;
  }
  dataList.value = [...mockData];
  try {
    const { data } = await getDetailLedger({
      subjectCode: currentSubject.value.code,
      startMonth: filters.startMonth,
      endMonth: filters.endMonth,
    });
    dataList.value = Array.isArray(data) ? data : data?.records || [];
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const resetFilters = () => {
  filters.subject = [];
  filters.auxiliary = "";
  filters.auxiliaryItem = "";
  filters.startMonth = "2024-01";
  filters.endMonth = "2024-03";
const resetFilters = async () => {
  filters.startMonth = defaultMonth;
  filters.endMonth = defaultMonth;
  dataList.value = [];
};
const getSummaries = (param) => {
  const { columns, data } = param;
  const sums = [];
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "合计";
      return;
    }
    if (column.property === "debit") {
      const values = data.map(item => Number(item.debit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else if (column.property === "credit") {
      const values = data.map(item => Number(item.credit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else {
      sums[index] = "";
    }
  });
  return sums;
  subjectKeyword.value = "";
  subjectTreeRef.value?.filter("");
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const handlePrint = () => {
@@ -249,22 +240,43 @@
  ElMessage.success("导出成功");
};
onMounted(() => {
  // é»˜è®¤ä¸åŠ è½½æ•°æ®ï¼Œéœ€è¦é€‰æ‹©ç§‘ç›®
onMounted(async () => {
  await loadSubjectOptions();
});
</script>
<style lang="scss" scoped>
.ledger-header {
  text-align: center;
  margin-bottom: 20px;
  h2 {
    margin: 0 0 10px 0;
  }
  p {
    color: #606266;
    margin: 5px 0;
  }
.ledger-layout {
  display: flex;
  gap: 16px;
}
.subject-panel {
  width: 260px;
  flex-shrink: 0;
  padding: 12px;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  background-color: #fff;
}
.subject-tree-scroll {
  height: 600px;
  margin-top: 12px;
}
.subject-node {
  display: inline-flex;
  align-items: center;
}
.ledger-content {
  flex: 1;
  min-width: 0;
}
.filter-form {
  margin-bottom: 12px;
}
.text-primary {
@@ -286,4 +298,12 @@
  color: #e6a23c;
  font-weight: bold;
}
.subject-panel :deep(.el-tree-node__content) {
  height: 34px;
}
.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
  background-color: #f0f7ff;
}
</style>
src/views/financialManagement/voucher/generalLedger.vue
@@ -1,114 +1,128 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="会计科目:">
        <el-cascader v-model="filters.subject" :options="subjectOptions" :props="{ label: 'name', value: 'code' }" placeholder="请选择会计科目" clearable style="width: 250px;" filterable />
      </el-form-item>
      <el-form-item label="期间:">
        <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
        <span style="margin: 0 10px;">至</span>
        <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">查询</el-button>
        <el-button @click="resetFilters">重置</el-button>
        <el-button @click="handlePrint" icon="Printer">打印</el-button>
        <el-button @click="handleOut" icon="Download">导出</el-button>
      </el-form-item>
    </el-form>
  <div class="app-container ledger-page">
    <div class="ledger-layout">
      <aside class="subject-panel">
        <el-input v-model="subjectKeyword" placeholder="请输入科目名称/编号" clearable prefix-icon="Search" />
        <el-scrollbar class="subject-tree-scroll">
          <el-tree
            ref="subjectTreeRef"
            :data="subjectOptions"
            node-key="code"
            :props="{ label: 'name', children: 'children' }"
            highlight-current
            default-expand-all
            :expand-on-click-node="false"
            :filter-node-method="filterSubjectNode"
            @node-click="handleSubjectClick"
          >
            <template #default="{ data }">
              <span class="subject-node">{{ data.code }} {{ data.name }}</span>
            </template>
          </el-tree>
        </el-scrollbar>
      </aside>
    <div class="ledger-header" v-if="currentSubject">
      <h2>科目总账</h2>
      <p>科目: {{ currentSubject.code }} {{ currentSubject.name }}</p>
      <p>期间: {{ filters.startMonth }} è‡³ {{ filters.endMonth }}</p>
      <section class="ledger-content">
        <el-form :model="filters" :inline="true" class="filter-form">
          <el-form-item label="期间:">
            <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
            <span style="margin: 0 10px;">至</span>
            <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="getTableData">查询</el-button>
            <el-button @click="resetFilters">重置</el-button>
            <el-button @click="handlePrint" icon="Printer">打印</el-button>
            <el-button @click="handleOut" icon="Download">导出</el-button>
          </el-form-item>
        </el-form>
        <div class="table_list">
          <el-table :data="dataList" border style="width: 100%">
            <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 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 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>
              </template>
            </el-table-column>
            <el-table-column label="方向" width="80">
              <template #default="{ row }">
                <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="余额" width="150">
              <template #default="{ row }">
                <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
      </section>
    </div>
    <div class="table_list">
      <el-table :data="dataList" border style="width: 100%" show-summary :summary-method="getSummaries">
        <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">
          <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">
          <template #default="{ row }">
            <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="方向" width="80">
          <template #default="{ row }">
            <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="余额" width="150">
          <template #default="{ row }">
            <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import { getGeneralLedger } from "@/api/financialManagement/ledger";
defineOptions({
  name: "科目总账",
});
const filters = reactive({
  subject: [],
  startMonth: "2024-01",
  endMonth: "2024-03",
  subject: "",
  startMonth: "",
  endMonth: "",
});
const dataList = ref([]);
const subjectOptions = ref([]);
const subjectKeyword = ref("");
const subjectTreeRef = 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 getPreviousMonth = () => {
  const date = new Date();
  date.setDate(1);
  date.setMonth(date.getMonth() - 1);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  return `${year}-${month}`;
};
const defaultMonth = getPreviousMonth();
filters.startMonth = defaultMonth;
filters.endMonth = defaultMonth;
const fallbackSubjects = [
  { code: "1001", name: "库存现金" },
  { code: "1002", name: "银行存款" },
  { code: "1122", name: "应收账款" },
  { code: "2202", name: "应付账款" },
  { code: "6001", name: "主营业务收入" },
];
const currentSubject = computed(() => {
  if (!filters.subject || filters.subject.length === 0) return null;
  const code = filters.subject[filters.subject.length - 1];
  return findSubject(subjectOptions, code);
});
const toTree = (nodes = []) =>
  nodes
    .filter(item => item.subjectCode && item.subjectName)
    .map(item => ({
      code: item.subjectCode,
      name: item.subjectName,
      children: toTree(item.children || []),
    }));
const findSubject = (options, code) => {
  for (const item of options) {
@@ -121,65 +135,104 @@
  return null;
};
const currentSubject = computed(() => {
  if (!filters.subject) return null;
  return findSubject(subjectOptions.value, filters.subject);
});
const getFirstSubjectCode = (nodes = []) => {
  for (const item of nodes) {
    if (item.code) return item.code;
    if (item.children && item.children.length > 0) {
      const childCode = getFirstSubjectCode(item.children);
      if (childCode) return childCode;
    }
  }
  return "";
};
const setDefaultSubjectSelection = async () => {
  const firstCode = getFirstSubjectCode(subjectOptions.value);
  if (!firstCode) {
    filters.subject = "";
    subjectTreeRef.value?.setCurrentKey(null);
    return;
  }
  filters.subject = firstCode;
  await nextTick();
  subjectTreeRef.value?.setCurrentKey(firstCode);
};
const filterSubjectNode = (value, data) => {
  const keyword = value?.trim();
  if (!keyword) return true;
  return `${data.code}${data.name}`.includes(keyword);
};
watch(subjectKeyword, (value) => {
  subjectTreeRef.value?.filter(value || "");
});
const handleSubjectClick = async (data) => {
  filters.subject = data.code;
  await getTableData();
};
const loadSubjectOptions = async () => {
  let options = [];
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
      status: 0,
    });
    options = toTree(data?.records || []);
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  if (options.length === 0) {
    options = fallbackSubjects.map(item => ({ ...item, children: [] }));
  }
  subjectOptions.value = options;
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  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 = () => {
  filters.subject = [];
  filters.startMonth = "2024-01";
  filters.endMonth = "2024-03";
const resetFilters = async () => {
  filters.startMonth = defaultMonth;
  filters.endMonth = defaultMonth;
  dataList.value = [];
};
const getSummaries = (param) => {
  const { columns, data } = param;
  const sums = [];
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "合计";
      return;
    }
    if (column.property === "debit") {
      const values = data.map(item => Number(item.debit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else if (column.property === "credit") {
      const values = data.map(item => Number(item.credit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else {
      sums[index] = "";
    }
  });
  return sums;
  subjectKeyword.value = "";
  subjectTreeRef.value?.filter("");
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const handlePrint = () => {
@@ -190,22 +243,43 @@
  ElMessage.success("导出成功");
};
onMounted(() => {
  // é»˜è®¤ä¸åŠ è½½æ•°æ®ï¼Œéœ€è¦é€‰æ‹©ç§‘ç›®
onMounted(async () => {
  await loadSubjectOptions();
});
</script>
<style lang="scss" scoped>
.ledger-header {
  text-align: center;
  margin-bottom: 20px;
  h2 {
    margin: 0 0 10px 0;
  }
  p {
    color: #606266;
    margin: 5px 0;
  }
.ledger-layout {
  display: flex;
  gap: 16px;
}
.subject-panel {
  width: 260px;
  flex-shrink: 0;
  padding: 12px;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  background-color: #fff;
}
.subject-tree-scroll {
  height: 600px;
  margin-top: 12px;
}
.subject-node {
  display: inline-flex;
  align-items: center;
}
.ledger-content {
  flex: 1;
  min-width: 0;
}
.filter-form {
  margin-bottom: 12px;
}
.text-primary {
@@ -227,4 +301,12 @@
  color: #e6a23c;
  font-weight: bold;
}
.subject-panel :deep(.el-tree-node__content) {
  height: 34px;
}
.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
  background-color: #f0f7ff;
}
</style>
src/views/financialManagement/voucher/index.vue
@@ -9,9 +9,12 @@
      </el-form-item>
      <el-form-item label="制单人:">
        <el-select v-model="filters.creator" placeholder="请选择制单人" clearable style="width: 150px;">
          <el-option label="张三" value="张三" />
          <el-option label="李四" value="李四" />
          <el-option label="王五" value="王五" />
          <el-option
            v-for="item in creatorOptions"
            :key="item"
            :label="item"
            :value="item"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="状态:">
@@ -62,9 +65,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>
@@ -75,25 +78,25 @@
          <h2 class="voucher-title">记账凭证</h2>
          <div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '期' : '' }}</div>
        </div>
        <el-form :model="form" :rules="rules" ref="formRef" label-width="0">
        <el-form :model="form" :rules="rules" :disabled="isViewMode" ref="formRef" label-width="0">
          <div class="voucher-info">
            <div class="voucher-no-section">
              <span class="label">凭证字:</span>
              <el-select v-model="form.voucherPrefix" style="width: 70px;">
              <el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;">
                <el-option label="è®°" value="è®°" />
              </el-select>
              <el-input v-model="form.voucherNum" style="width: 60px;" />
              <el-input v-model="form.voucherNum" :disabled="isViewMode" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">号</span>
            </div>
            <div class="voucher-date-section">
              <span class="label">日期:</span>
              <el-date-picker v-model="form.voucherDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 140px;" />
              <el-date-picker v-model="form.voucherDate" :disabled="isViewMode" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 140px;" />
            </div>
            <div class="voucher-attachment-section">
              <span class="label">附件:</span>
              <el-input-number v-model="form.attachmentCount" :min="0" :controls="false" style="width: 60px;" />
              <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">å¼ </span>
              <el-button type="primary" link style="margin-left: 10px;">上传文件</el-button>
              <el-button type="primary" link :disabled="isViewMode" style="margin-left: 10px;">上传文件</el-button>
            </div>
          </div>
          <div class="voucher-table">
@@ -134,18 +137,28 @@
              <tbody>
                <tr v-for="(entry, rowIndex) in form.entries" :key="rowIndex" @click="selectRow(rowIndex)" :class="{ 'selected-row': selectedRowIndex === rowIndex }">
                  <td class="col-summary">
                    <el-input v-model="entry.summary" placeholder="请输入摘要" @focus="selectRow(rowIndex)" />
                    <el-input v-model="entry.summary" :disabled="isViewMode" 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"
                      :disabled="isViewMode"
                      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列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'">
                    <td colspan="11" class="debit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.debit" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                      <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
@@ -156,7 +169,7 @@
                  <!-- è´·æ–¹11列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'">
                    <td colspan="11" class="credit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.credit" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                      <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
@@ -165,7 +178,7 @@
                    </td>
                  </template>
                  <td class="col-action">
                    <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="form.entries.length <= 2">删除</el-button>
                    <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="isViewMode || form.entries.length <= 2">删除</el-button>
                  </td>
                </tr>
                <tr class="total-row">
@@ -182,19 +195,34 @@
            </table>
          </div>
          <div class="voucher-toolbar">
            <el-button type="primary" link @click="addEntry" icon="Plus">新增行</el-button>
            <el-button type="primary" link @click="addEntry" icon="Plus" :disabled="isViewMode">新增行</el-button>
          </div>
          <div class="voucher-footer">
            <div class="creator-section">
              <span class="label">制单人:{{ form.creator }}</span>
              <span class="label">制单人:</span>
              <el-select
                v-model="form.creator"
                :disabled="isViewMode"
                placeholder="请选择制单人"
                filterable
                clearable
                style="width: 200px;"
              >
                <el-option
                  v-for="item in creatorOptions"
                  :key="item"
                  :label="item"
                  :value="item"
                />
              </el-select>
            </div>
          </div>
        </el-form>
      </div>
      <template #footer>
        <div>
          <el-button type="primary" @click="submitForm" :disabled="!isBalanced">保存</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button v-if="!isViewMode" type="primary" @click="submitForm" :disabled="!isBalanced">保存</el-button>
          <el-button @click="dialogVisible = false">{{ isViewMode ? '关闭' : '取消' }}</el-button>
        </div>
      </template>
    </FormDialog>
@@ -205,10 +233,24 @@
import { ref, reactive, onMounted, computed, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import useUserStore from "@/store/modules/user";
import { userListNoPageByTenantId } from "@/api/system/user";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import {
  listVoucherPage,
  addVoucher,
  updateVoucher,
  postVoucher,
  cancelVoucher,
  getVoucherDetail,
} from "@/api/financialManagement/voucher";
defineOptions({
  name: "凭证管理",
});
const userStore = useUserStore();
const getDefaultCreator = () => userStore.nickName || userStore.name || "张三";
const filters = reactive({
  voucherNo: "",
@@ -227,39 +269,92 @@
  { 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([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const dialogMode = ref("add");
const isEdit = ref(false);
const currentId = ref(null);
const isViewMode = computed(() => dialogMode.value === "view");
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: [],
  creator: "张三",
  entries: [createEmptyEntry(), createEmptyEntry()],
  creator: getDefaultCreator(),
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const userOptions = ref([]);
const creatorOptions = computed(() => {
  const source = [
    ...userOptions.value.map(item => item.nickName || item.userName || item.name),
    getDefaultCreator(),
    form.creator,
    filters.creator,
  ];
  return [...new Set(source.filter(Boolean))];
});
const selectedRowIndex = ref(-1);
@@ -276,12 +371,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 +393,79 @@
  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;
    }
    const fallbackFlatList = [];
    subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
    subjectList.value = fallbackFlatList;
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºé”™è¯¯ï¼Œè¿™é‡Œä¿ç•™é»˜è®¤ç§‘目作为兜底
    const fallbackFlatList = [];
    subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
    subjectList.value = fallbackFlatList;
  }
  if (filters.creator) {
    result = result.filter(item => item.creator === filters.creator);
};
const loadUserOptions = async () => {
  try {
    const { data } = await userListNoPageByTenantId();
    userOptions.value = Array.isArray(data) ? data : [];
  } catch (error) {
    userOptions.value = [];
  }
  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 +484,10 @@
};
const addEntry = () => {
  form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
  if (isViewMode.value) {
    return;
  }
  form.entries.push(createEmptyEntry());
};
const selectRow = (index) => {
@@ -356,6 +495,9 @@
};
const openAmountInput = (index, type) => {
  if (isViewMode.value) {
    return;
  }
  editingCell.row = index;
  editingCell.type = type;
  nextTick(() => {
@@ -402,65 +544,83 @@
};
const removeEntry = (index) => {
  if (isViewMode.value) {
    return;
  }
  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 = () => {
  dialogMode.value = "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) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑凭证";
  const parts = row.voucherNo.split('-');
  Object.assign(form, {
    ...row,
    voucherPrefix: parts[0] || 'è®°',
    voucherNum: parts[1] || '',
  });
  if (form.entries.length < 4) {
    while (form.entries.length < 4) {
      form.entries.push({ subjectCode: "", subjectName: "", summary: "", debit: 0, credit: 0 });
const openVoucherDialog = async (row, mode = "edit") => {
  try {
    dialogMode.value = mode;
    isEdit.value = mode === "edit";
    currentId.value = row.id;
    dialogTitle.value = mode === "view" ? "查看凭证" : "编辑凭证";
    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] || "",
      creator: detail.creator || getDefaultCreator(),
      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 < 2) {
      while (form.entries.length < 2) {
        form.entries.push(createEmptyEntry());
      }
    }
    selectedRowIndex.value = 0;
    dialogVisible.value = true;
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
  selectedRowIndex.value = 0;
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看凭证: ${row.voucherNo}`);
const edit = async row => {
  await openVoucherDialog(row, "edit");
};
const view = async row => {
  await openVoucherDialog(row, "view");
};
const handlePost = (row) => {
@@ -468,13 +628,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 +640,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 +656,79 @@
};
const submitForm = () => {
  formRef.value.validate((valid) => {
  if (isViewMode.value) {
    dialogVisible.value = false;
    return;
  }
  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),
        })),
      };
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...dataToSave };
      try {
        if (isEdit.value) {
          await updateVoucher({
            id: currentId.value,
            ...dataToSave,
          });
          ElMessage.success("编辑成功");
        } else {
          await addVoucher(dataToSave);
          ElMessage.success("新增成功");
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...dataToSave, status: "unposted" });
        ElMessage.success("新增成功");
        dialogVisible.value = false;
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  getTableData();
onMounted(async () => {
  await loadUserOptions();
  await loadSubjectList();
  await getTableData();
});
</script>
@@ -780,7 +968,8 @@
    .col-subject {
      position: relative;
      .el-select {
      .el-select,
      .el-tree-select {
        .el-input input {
          font-size: 12px;
        }
src/views/index.vue
@@ -479,7 +479,7 @@
// èŽ·å–å·¥åºåˆ—è¡¨
const getProcessList = () => {
  list().then(res => {
    processOptions.value = res.data
    processOptions.value = res.data.records
  })
}
src/views/inventoryManagement/receiptManagement/Record.vue
@@ -124,7 +124,8 @@
  batchApproveStockInRecords,
} from "@/api/inventoryManagement/stockInRecord.js";
import {
  findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions,
  findAllQualifiedStockInRecordTypeOptions,
  // findAllUnQualifiedStockInRecordTypeOptions,
} from "@/api/basicData/enum.js";
const {proxy} = getCurrentInstance();
@@ -236,10 +237,10 @@
        })
    return
  }
  findAllUnQualifiedStockInRecordTypeOptions()
      .then(res => {
        stockRecordTypeOptions.value = res.data;
      })
  // findAllUnQualifiedStockInRecordTypeOptions()
  //     .then(res => {
  //       stockRecordTypeOptions.value = res.data;
  //     })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
src/views/inventoryManagement/stockReport/index.vue
@@ -223,7 +223,7 @@
  } from "@/api/inventoryManagement/stockInventory.js";
  import {
    findAllQualifiedStockInRecordTypeOptions,
    findAllUnQualifiedStockInRecordTypeOptions,
    // findAllUnQualifiedStockInRecordTypeOptions,
  } from "@/api/basicData/enum.js";
  const { proxy } = getCurrentInstance();
@@ -265,12 +265,12 @@
  const fetchStockRecordTypeOptions = () => {
    findAllQualifiedStockInRecordTypeOptions().then(res => {
      stockRecordTypeOptions.value = res.data;
      findAllUnQualifiedStockInRecordTypeOptions().then(res => {
        stockRecordTypeOptions.value = [
          ...stockRecordTypeOptions.value,
          ...res.data,
        ];
      });
      // findAllUnQualifiedStockInRecordTypeOptions().then(res => {
      //   stockRecordTypeOptions.value = [
      //     ...stockRecordTypeOptions.value,
      //     ...res.data,
      //   ];
      // });
    });
  };
src/views/procurementManagement/purchaseReturnOrder/New.vue
@@ -227,10 +227,11 @@
              <span class="title-text">产品列表</span>
            </div>
            <el-button type="primary" size="small" style="margin-bottom: 20px" @click="isShowProductsModal = true" :disabled="!formState.purchaseLedgerId">添加产品</el-button>
            <el-table :data="formState.purchaseReturnOrderProductsDtos"
            <div class="product-table-scroll">
            <el-table class="product-table-inner"
                      :data="formState.purchaseReturnOrderProductsDtos"
                      border
                      max-height="400"
                      :scroll-y="true"
                      show-summary
                      :summary-method="summarizeChildrenTable">
              <el-table-column align="center"
@@ -240,6 +241,12 @@
                               label="序号"
                               type="index"
                               width="60" />
              <el-table-column label="入库单号"
                               prop="inboundBatches"
                               width="150" />
              <el-table-column label="批次号"
                               prop="batchNo"
                               width="150" />
              <el-table-column label="产品大类"
                               prop="productCategory" />
              <el-table-column label="规格型号"
@@ -248,11 +255,17 @@
                               prop="unit"
                               width="70" />
              <el-table-column label="数量"
                               prop="quantity"
                               prop="stockInNum"
                               width="100" />
                               <el-table-column label="可退货数量"
                               prop="availableQuality"
                               prop="unQuantity"
                               width="130" />
              <el-table-column label="已退货数量"
                               width="130">
                <template #default="scope">
                  {{ calcAlreadyReturned(scope.row) }}
                </template>
              </el-table-column>
              <el-table-column label="退货数量"
                               prop="returnQuantity"
                               width="180">
@@ -268,27 +281,27 @@
                            placeholder="请输入退货数量" />
                </template>
              </el-table-column>
              <el-table-column label="库存预警数量"
              <!-- <el-table-column label="库存预警数量"
                               prop="warnNum"
                               width="120"
                               show-overflow-tooltip />
              <el-table-column label="税率(%)"
                               prop="taxRate"
                               width="80" />
                               width="80" /> -->
              <el-table-column label="含税单价(元)"
                               prop="taxInclusiveUnitPrice"
                               :formatter="formattedNumber"
                               width="150" />
                               width="120" />
              <el-table-column label="退货总价(元)"
                               prop="taxInclusiveTotalPrice"
                               width="180">
                               width="120">
                <template #default="scope">
                  {{ formatAmount(getReturnTotal(scope.row)) || '--' }}
                </template>
              </el-table-column>
              <el-table-column label="是否质检"
                               prop="isChecked"
                               width="150">
                               width="100">
                <template #default="scope">
                  <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
                    {{ scope.row.isChecked ? '是' : '否' }}
@@ -311,6 +324,7 @@
                </template>
              </el-table-column>
            </el-table>
            </div>
          </div>
        <div class="section-title">
@@ -408,9 +422,6 @@
import {getOptions, purchaseList} from "@/api/procurementManagement/procurementLedger.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
const ProductList = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/ProductList.vue"));
  import {
    productList,
  } from "@/api/procurementManagement/procurementLedger.js";
const props = defineProps({
  visible: {
    type: Boolean,
@@ -518,6 +529,14 @@
  return Number.isNaN(num) ? 0 : num
}
/** å·²é€€è´§æ•°é‡ = å…¥åº“行总数量 âˆ’ å½“前可退货数量(剩余) */
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0)
  const un = Number(row?.unQuantity ?? 0)
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0
  return Math.max(total - un, 0)
}
const getReturnTotal = (row) => {
  const qty = toNumber(row?.returnQuantity)
  const unitPrice = toNumber(row?.taxInclusiveUnitPrice)
@@ -553,7 +572,7 @@
}
const getReturnQtyMax = (row) => {
  const max = Number(row?.availableQuality)
  const max = Number(row?.unQuantity)
  if (Number.isNaN(max) || max < 0) {
    return 0
  }
@@ -568,17 +587,17 @@
  return proxy.summarizeTable(
      param,
      [
        "quantity",
        "availableQuality",
        "stockInNum",
        "unQuantity",
        "returnQuantity",
        "taxInclusiveUnitPrice",
        "taxInclusiveTotalPrice",
        "taxExclusiveTotalPrice",
      ],
      {
        quantity: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        stockInNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        returnQuantity: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        availableQuality: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        unQuantity: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      }
  );
};
@@ -660,20 +679,10 @@
  }
}
// å¤„理改变采购台账数据
const handleChangePurchaseLedgerId = async () => {
// å¤„理改变采购台账数据(不请求接口回显产品,产品仅在「添加产品」弹窗勾选后写入)
const handleChangePurchaseLedgerId = () => {
  resetFeeInfo()
  if (!formState.value.purchaseLedgerId) {
    formState.value.purchaseReturnOrderProductsDtos = []
    return
  }
  const res = await productList({ salesLedgerId: formState.value.purchaseLedgerId, type: 2 });
  formState.value.purchaseReturnOrderProductsDtos = res.data.map(item => ({
    ...item,
    returnQuantity: undefined,
    taxInclusiveTotalPrice: 0,
    salesLedgerProductId: item.id,
  }))
  formState.value.purchaseReturnOrderProductsDtos = []
  syncTotalAmount()
}
@@ -691,7 +700,7 @@
    ...item,
    returnQuantity: undefined,
    taxInclusiveTotalPrice: 0,
    salesLedgerProductId: item.id,
    // salesLedgerProductId: item.salesLedgerProductId,
  }));
  formState.value.purchaseReturnOrderProductsDtos.push(...newProducts);
  syncTotalAmount()
@@ -717,7 +726,7 @@
  // é€è¡Œæ ¡éªŒé€€è´§æ•°é‡ï¼šä»»æ„ä¸€è¡Œæœªå¡«/非法/超限都不允许提交
  const invalidRowIndex = productList.findIndex((item) => {
    const qty = Number(item.returnQuantity)
    const maxQty = Number(item.availableQuality)
    const maxQty = Number(item.unQuantity)
    if (item.returnQuantity === null || item.returnQuantity === undefined || item.returnQuantity === "") {
      return true
@@ -738,7 +747,15 @@
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      createPurchaseReturnOrder(formState.value).then(res => {
      console.log(productList)
      const submitPayload = {
        ...formState.value,
        purchaseReturnOrderProductsDtos: productList.map((row) => ({
          ...row,
          stockInRecordId: row.id,
        })),
      }
      createPurchaseReturnOrder(submitPayload).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
@@ -785,4 +802,13 @@
  border-radius: 50%;
  margin-right: 8px;
}
.product-table-scroll {
  width: 100%;
  overflow-x: auto;
}
.product-table-inner {
  min-width: 1280px;
}
</style>
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue
@@ -6,9 +6,10 @@
        width="1200"
        @close="closeModal"
    >
      <div class="table_list">
      <div class="table_list" v-loading="tableLoading">
        <el-table :data="tableData"
                  border
                  row-key="id"
                  @selection-change="handleChangeSelection">
          <el-table-column align="center"
                           type="selection"
@@ -17,6 +18,12 @@
                           label="序号"
                           type="index"
                           width="60" />
                           <el-table-column label="入库单号"
                               prop="inboundBatches"
                               width="150" />
              <el-table-column label="批次号"
                               prop="batchNo"
                               width="150" />
          <el-table-column label="产品大类"
                           prop="productCategory" />
          <el-table-column label="规格型号"
@@ -25,27 +32,36 @@
                           prop="unit"
                           width="70" />
          <el-table-column label="数量"
                           prop="quantity"
                           prop="stockInNum"
                           width="70" />
          <el-table-column label="库存预警数量"
          <el-table-column label="可退货数量"
                           prop="unQuantity"
                           width="130" />
          <el-table-column label="已退货数量"
                           width="130">
            <template #default="scope">
              {{ calcAlreadyReturned(scope.row) }}
            </template>
          </el-table-column>
          <!-- <el-table-column label="库存预警数量"
                           prop="warnNum"
                           width="120"
                           show-overflow-tooltip />
          <el-table-column label="税率(%)"
                           prop="taxRate"
                           width="80" />
                           width="80" /> -->
          <el-table-column label="含税单价(元)"
                           prop="taxInclusiveUnitPrice"
                           :formatter="formattedNumber"
                           width="150" />
          <el-table-column label="含税总价(元)"
          <!-- <el-table-column label="含税总价(元)"
                           prop="taxInclusiveTotalPrice"
                           :formatter="formattedNumber"
                           width="150" />
          <el-table-column label="不含税总价(元)"
                           prop="taxExclusiveTotalPrice"
                           :formatter="formattedNumber"
                           width="150" />
                           width="150" /> -->
          <el-table-column label="是否质检"
                           prop="isChecked"
                           width="150">
@@ -56,8 +72,6 @@
            </template>
          </el-table-column>
        </el-table>
        <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
                    :page="page.current" :limit="page.size" @pagination="paginationChange" />
      </div>
      <template #footer>
@@ -71,8 +85,8 @@
</template>
<script setup>
import {computed, reactive, ref, onMounted} from "vue";
import {productList} from "@/api/procurementManagement/procurementLedger.js";
import {computed, ref, onMounted} from "vue";
import {getPurchaseReturnOrderByPurchaseLedgerId} from "@/api/procurementManagement/purchase_return_order.js";
import {ElMessage} from "element-plus";
const props = defineProps({
@@ -82,7 +96,7 @@
  },
  purchaseLedgerId: {
    type: Number,
    type: [Number, String],
    required: true,
  }
});
@@ -101,32 +115,59 @@
const tableData = ref([])
const selectedRows = ref([])
const tableLoading = ref(false)
const page = reactive({
  current: 1,
  size: 100,
})
const total = ref(0)
const formattedNumber = (row, column, cellValue) => {
  return parseFloat(cellValue).toFixed(2);
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList()
/** å·²é€€è´§æ•°é‡ = å…¥åº“行总数量 âˆ’ å½“前可退货数量(剩余) */
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0)
  const un = Number(row?.unQuantity ?? 0)
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0
  return Math.max(total - un, 0)
}
const handleChangeSelection = (val) => {
  selectedRows.value = val;
}
/** ä¸Ž New.vue ä¸­é‡‡è´­å°è´¦å˜æ›´æ—¶è§£æž getByPurchaseLedgerId çš„规则一致 */
const parseProductRowsFromLedgerResponse = (res) => {
  const payload = res?.data
  let list = []
  if (Array.isArray(payload)) {
    list = payload
  } else if (payload && typeof payload === 'object') {
    const nested =
      payload.purchaseReturnOrderProductsDtos ||
      payload.purchaseReturnOrderProductsDetailVoList
    list = Array.isArray(nested) ? nested : []
    if (list.length && list[0]?.salesLedgerProduct) {
      list = list.map((item) => ({ ...item, ...item.salesLedgerProduct }))
    }
  }
  return list
}
const fetchData = () => {
  tableLoading.value = true;
  productList({salesLedgerId: props.purchaseLedgerId, type: 2}).then((res) => {
    tableData.value = res.data;
  }).finally(() => {
    tableLoading.value = false;
  if (props.purchaseLedgerId === undefined || props.purchaseLedgerId === null || props.purchaseLedgerId === '') {
    tableData.value = []
    return
  }
  tableLoading.value = true
  getPurchaseReturnOrderByPurchaseLedgerId({
    purchaseLedgerId: props.purchaseLedgerId,
  })
    .then((res) => {
      const list = parseProductRowsFromLedgerResponse(res)
      tableData.value = list
    })
    .catch(() => {
      tableData.value = []
    })
    .finally(() => {
      tableLoading.value = false
    })
}
const handleSubmit = () => {
src/views/procurementManagement/purchaseReturnOrder/index.vue
@@ -1,24 +1,26 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
      <el-form :model="searchForm" :inline="true">
        <el-form-item label="退料单号:">
          <el-input v-model="searchForm.no"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    @change="handleQuery" />
          <el-input
            v-model="searchForm.no"
            placeholder="请输入"
            clearable
            prefix-icon="Search"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery"> æœç´¢ </el-button>
          <el-button type="primary" @click="handleQuery"> æœç´¢ </el-button>
        </el-form-item>
      </el-form>
      <div>
        <el-button type="primary" @click="isShowNewModal = true">新增</el-button>
        <el-button type="primary" @click="isShowNewModal = true"
          >新增</el-button
        >
      </div>
    </div>
@@ -35,14 +37,25 @@
        @pagination="paginationChange"
      >
        <template #operation="{ row }">
          <el-button link type="primary" size="small" style="color: #67C23A" @click="handleDetail(row)">详情</el-button>
          <el-button link size="small" @click="handleDelete(row)">删除</el-button>
          <el-button
            link
            type="primary"
            size="small"
            style="color: #67c23a"
            @click="handleDetail(row)"
            >详情</el-button
          >
          <el-button link size="small" @click="handleDelete(row)"
            >删除</el-button
          >
        </template>
      </PIMTable>
    </div>
    <new v-if="isShowNewModal"
         v-model:visible="isShowNewModal"
         @completed="handleQuery" />
    <new
      v-if="isShowNewModal"
      v-model:visible="isShowNewModal"
      @completed="handleQuery"
    />
    <el-dialog
      v-model="detailVisible"
@@ -52,21 +65,51 @@
    >
      <div v-loading="detailLoading">
        <el-descriptions :column="3" border>
          <el-descriptions-item label="退料单号">{{ detailData.no || '--' }}</el-descriptions-item>
          <el-descriptions-item label="退货方式">{{ getReturnTypeLabel(detailData.returnType) }}</el-descriptions-item>
          <el-descriptions-item label="供应商名称">{{ detailData.supplierName || '--' }}</el-descriptions-item>
          <el-descriptions-item label="项目阶段">{{ getProjectPhaseLabel(detailData.projectPhase) }}</el-descriptions-item>
          <el-descriptions-item label="关联单号">{{ detailData.purchaseContractNumber || '--' }}</el-descriptions-item>
          <el-descriptions-item label="制作日期">{{ detailData.preparedAt || '--' }}</el-descriptions-item>
          <el-descriptions-item label="制单人">{{ detailData.preparedUserName || '--' }}</el-descriptions-item>
          <el-descriptions-item label="退料人">{{ detailData.returnUserName || '--' }}</el-descriptions-item>
          <el-descriptions-item label="整单折扣额">{{ formatAmount(detailData.totalDiscountAmount) }}</el-descriptions-item>
          <el-descriptions-item label="整单折扣率">{{ detailData.totalDiscountRate ?? '--' }}</el-descriptions-item>
          <el-descriptions-item label="成交金额">{{ formatAmount(detailData.totalAmount) }}</el-descriptions-item>
          <el-descriptions-item label="创建人">{{ detailData.createUserName || '--' }}</el-descriptions-item>
          <el-descriptions-item label="创建时间">{{ detailData.createTime || '--' }}</el-descriptions-item>
          <el-descriptions-item label="最近更新时间">{{ detailData.updateTime || '--' }}</el-descriptions-item>
          <el-descriptions-item label="备注" :span="3">{{ detailData.remark || '--' }}</el-descriptions-item>
          <el-descriptions-item label="退料单号">{{
            detailData.no || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="退货方式">{{
            getReturnTypeLabel(detailData.returnType)
          }}</el-descriptions-item>
          <el-descriptions-item label="供应商名称">{{
            detailData.supplierName || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="项目阶段">{{
            getProjectPhaseLabel(detailData.projectPhase)
          }}</el-descriptions-item>
          <el-descriptions-item label="关联的采购订单号">{{
            detailData.purchaseContractNumber || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="制作日期">{{
            detailData.preparedAt || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="制单人">{{
            detailData.preparedUserName || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="退料人">{{
            detailData.returnUserName || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="整单折扣额">{{
            formatAmount(detailData.totalDiscountAmount)
          }}</el-descriptions-item>
          <el-descriptions-item label="整单折扣率">{{
            detailData.totalDiscountRate ?? "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="成交金额">{{
            formatAmount(detailData.totalAmount)
          }}</el-descriptions-item>
          <el-descriptions-item label="创建人">{{
            detailData.createUserName || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="创建时间">{{
            detailData.createTime || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="最近更新时间">{{
            detailData.updateTime || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="备注" :span="3">{{
            detailData.remark || "--"
          }}</el-descriptions-item>
        </el-descriptions>
        <el-divider content-position="left">产品列表</el-divider>
@@ -77,27 +120,75 @@
          max-height="420"
          style="width: 100%"
        >
          <el-table-column align="center" label="序号" type="index" width="60" />
          <el-table-column label="产品大类" prop="productCategory" min-width="120" show-overflow-tooltip />
          <el-table-column label="规格型号" prop="specificationModel" min-width="140" show-overflow-tooltip />
          <el-table-column
            align="center"
            label="序号"
            type="index"
            width="60"
          />
          <el-table-column label="入库单号" prop="inboundBatches" width="150" />
          <el-table-column label="批次号" prop="batchNo" width="150" />
          <el-table-column
            label="产品大类"
            prop="productCategory"
            min-width="120"
            show-overflow-tooltip
          />
          <el-table-column
            label="规格型号"
            prop="specificationModel"
            min-width="140"
            show-overflow-tooltip
          />
          <el-table-column label="单位" prop="unit" width="80" />
          <el-table-column label="数量" prop="quantity" width="80" />
          <el-table-column label="退货数量" prop="returnQuantity" width="100" />
          <el-table-column label="库存预警数量" prop="warnNum" width="120" />
          <el-table-column label="税率(%)" prop="taxRate" width="90" />
          <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" width="130">
            <template #default="scope">{{ formatAmount(scope.row.taxInclusiveUnitPrice) }}</template>
          <el-table-column label="数量" prop="stockInNum" width="80" />
          <el-table-column label="可退货数量"
                           prop="unQuantity"
                           width="100" />
          <el-table-column label="已退货数量"
                           width="100">
            <template #default="scope">
              {{ calcAlreadyReturned(scope.row) }}
            </template>
          </el-table-column>
          <el-table-column label="退货总价(元)" prop="taxInclusiveTotalPrice" width="130">
            <template #default="scope">{{ formatAmount(scope.row.taxInclusiveTotalPrice) }}</template>
          <!-- <el-table-column label="库存预警数量" prop="warnNum" width="120" /> -->
          <!-- <el-table-column label="税率(%)" prop="taxRate" width="90" /> -->
          <el-table-column
            label="含税单价(元)"
            prop="taxInclusiveUnitPrice"
            width="130"
          >
            <template #default="scope">{{
              formatAmount(scope.row.taxInclusiveUnitPrice)
            }}</template>
          </el-table-column>
          <el-table-column label="不退货总价(元)" prop="taxExclusiveTotalPrice" width="140">
            <template #default="scope">{{ formatAmount(scope.row.taxExclusiveTotalPrice) }}</template>
          <!-- <el-table-column
            label="退货总价(元)"
            prop="taxInclusiveTotalPrice"
            width="130"
          >
            <template #default="scope">{{
              formatAmount(scope.row.taxInclusiveTotalPrice)
            }}</template>
          </el-table-column>
          <el-table-column label="是否质检" prop="isChecked" width="100" align="center">
          <el-table-column
            label="不退货总价(元)"
            prop="taxExclusiveTotalPrice"
            width="140"
          >
            <template #default="scope">{{
              formatAmount(scope.row.taxExclusiveTotalPrice)
            }}</template>
          </el-table-column> -->
          <el-table-column
            label="是否质检"
            prop="isChecked"
            width="100"
            align="center"
          >
            <template #default="scope">
              <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
                {{ scope.row.isChecked ? '是' : '否' }}
                {{ scope.row.isChecked ? "是" : "否" }}
              </el-tag>
            </template>
          </el-table-column>
@@ -111,238 +202,280 @@
</template>
<script setup>
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import { ref, reactive, toRefs, onMounted, defineAsyncComponent, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
import {findPurchaseReturnOrderListPage, getPurchaseReturnOrderDetail, deletePurchaseReturnOrder} from "@/api/procurementManagement/purchase_return_order.js";
const New = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/New.vue"));
const tableData = ref([])
const selectedRows = ref([])
const tableLoading = ref(false)
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import {
  ref,
  reactive,
  toRefs,
  onMounted,
  defineAsyncComponent,
  getCurrentInstance,
} from "vue";
const { proxy } = getCurrentInstance();
import {
  findPurchaseReturnOrderListPage,
  getPurchaseReturnOrderDetail,
  deletePurchaseReturnOrder,
} from "@/api/procurementManagement/purchase_return_order.js";
const New = defineAsyncComponent(() =>
  import("@/views/procurementManagement/purchaseReturnOrder/New.vue")
);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
})
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref({})
const detailProducts = ref([])
});
const detailVisible = ref(false);
const detailLoading = ref(false);
const detailData = ref({});
const detailProducts = ref([]);
// æ˜¯å¦æ˜¾ç¤ºæ–°å¢žå¼¹æ¡†
const isShowNewModal = ref(false)
const isShowNewModal = ref(false);
const returnTypeOptions = [
  { label: '退货退款', value: 0 },
  { label: '拒收', value: 1 },
]
  { label: "退货退款", value: 0 },
  { label: "拒收", value: 1 },
];
const projectPhaseOptions = [
  { label: '立项', value: 0 },
  { label: '设计', value: 1 },
  { label: '采购', value: 2 },
  { label: '生产', value: 3 },
  { label: '出货', value: 4 },
]
  { label: "立项", value: 0 },
  { label: "设计", value: 1 },
  { label: "采购", value: 2 },
  { label: "生产", value: 3 },
  { label: "出货", value: 4 },
];
const tableColumn = ref([
  {
    label: '退料单号',
    prop: 'no',
    label: "退料单号",
    prop: "no",
  },
  {
    label: '退货方式',
    prop: 'returnType',
    formatData: (val) => returnTypeOptions.find(item => item.value === val)?.label || '--',
    label: "退货方式",
    prop: "returnType",
    formatData: (val) =>
      returnTypeOptions.find((item) => item.value === val)?.label || "--",
  },
  {
    label: '供应商名称',
    prop: 'supplierName',
    label: "供应商名称",
    prop: "supplierName",
    width: 180,
  },
  {
    label: '项目阶段',
    prop: 'projectPhase',
    label: "项目阶段",
    prop: "projectPhase",
    width: 100,
    formatData: (val) => projectPhaseOptions.find(item => String(item.value) === String(val))?.label || '--',
    formatData: (val) =>
      projectPhaseOptions.find((item) => String(item.value) === String(val))
        ?.label || "--",
  },
  {
    label: '关联单号',
    prop: 'purchaseContractNumber',
    label: "关联的采购订单号",
    prop: "purchaseContractNumber",
    width: 160,
  },
  {
    label: '制作日期',
    prop: 'preparedAt',
    label: "制作日期",
    prop: "preparedAt",
    width: 130,
  },
  {
    label: '制单人',
    prop: 'preparedUserName',
    label: "制单人",
    prop: "preparedUserName",
    width: 110,
  },
  {
    label: '退料人',
    prop: 'returnUserName',
    label: "退料人",
    prop: "returnUserName",
    width: 110,
  },
  {
    label: '整单折扣额',
    prop: 'totalDiscountAmount',
    label: "整单折扣额",
    prop: "totalDiscountAmount",
    width: 120,
  },
  {
    label: '整单折扣率',
    prop: 'totalDiscountRate',
    label: "整单折扣率",
    prop: "totalDiscountRate",
    width: 120,
  },
  {
    label: '成交金额',
    prop: 'totalAmount',
    label: "成交金额",
    prop: "totalAmount",
    width: 120,
  },
  {
    label: '创建人',
    prop: 'createUserName',
    label: "创建人",
    prop: "createUserName",
    width: 110,
  },
  {
    label: '创建时间',
    prop: 'createTime',
    label: "创建时间",
    prop: "createTime",
    width: 170,
  },
  {
    label: '最近更新时间',
    prop: 'updateTime',
    label: "最近更新时间",
    prop: "updateTime",
    width: 170,
  },
  {
    label: '备注',
    prop: 'remark',
    label: "备注",
    prop: "remark",
    width: 180,
  },
  {
    dataType: "action",
    width: 120,
      label: "操作",
      align: "center",
      fixed: "right",
    label: "操作",
    align: "center",
    fixed: "right",
    operation: [
      {
                name: "详情",
                type: "text",
                clickFun: row => {handleDetail(row);},
            },
        name: "详情",
        type: "text",
        clickFun: (row) => {
          handleDetail(row);
        },
      },
      {
        name: "删除",
        clickFun: row => {handleDelete(row)},
        clickFun: (row) => {
          handleDelete(row);
        },
      },
  ],
    ],
  },
])
]);
const data = reactive({
  searchForm: {
    no: '',
  }
})
const { searchForm } = toRefs(data)
    no: "",
  },
});
const { searchForm } = toRefs(data);
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1
  getList()
}
  page.current = 1;
  getList();
};
// åˆ é™¤æ“ä½œ
const handleDelete = (row) => {
  console.log('删除行数据:', row)
  proxy?.$modal?.confirm('确定要删除吗?删除将无法恢复').then(() => {
    // è¿™é‡Œè°ƒç”¨åˆ é™¤æŽ¥å£ï¼Œä¼ å…¥ row.id
    deletePurchaseReturnOrder(row.id).then(() => {
      proxy?.$modal?.msgSuccess?.("删除成功");
      getList()
    }).catch(() => {
      proxy?.$modal?.msgError?.('删除失败')
  console.log("删除行数据:", row);
  proxy?.$modal
    ?.confirm("确定要删除吗?删除将无法恢复")
    .then(() => {
      // è¿™é‡Œè°ƒç”¨åˆ é™¤æŽ¥å£ï¼Œä¼ å…¥ row.id
      deletePurchaseReturnOrder(row.id)
        .then(() => {
          proxy?.$modal?.msgSuccess?.("删除成功");
          getList();
        })
        .catch(() => {
          proxy?.$modal?.msgError?.("删除失败");
        });
    })
  }).catch(() => {
    // å–消删除
    proxy?.$modal?.msgInfo?.('已取消删除')
  })
}
    .catch(() => {
      // å–消删除
      proxy?.$modal?.msgInfo?.("已取消删除");
    });
};
// æŸ¥çœ‹è¯¦æƒ…
const handleDetail = (row) => {
  if (!row?.id) {
    proxy?.$modal?.msgWarning?.('未获取到单据ID')
    return
    proxy?.$modal?.msgWarning?.("未获取到单据ID");
    return;
  }
  detailVisible.value = true
  detailLoading.value = true
  getPurchaseReturnOrderDetail(row.id).then(res => {
    const payload = res?.data || {}
    detailData.value = payload
    // æ‹¼æŽ¥è¿žä¸ªå¯¹è±¡æˆä¸€ä¸ªå¯¹è±¡ï¼Œæ–¹ä¾¿å±•示 item å’Œ item.salesLedgerProduct é‡Œçš„字段
  detailVisible.value = true;
  detailLoading.value = true;
  getPurchaseReturnOrderDetail(row.id)
    .then((res) => {
      const payload = res?.data || {};
      detailData.value = payload;
      // æ‹¼æŽ¥è¿žä¸ªå¯¹è±¡æˆä¸€ä¸ªå¯¹è±¡ï¼Œæ–¹ä¾¿å±•示 item å’Œ item.salesLedgerProduct é‡Œçš„字段
    detailProducts.value =
      payload.purchaseReturnOrderProductsDetailVoList.map(item => ({ ...item, ...item.salesLedgerProduct })) ||
      []
  }).catch(() => {
    proxy?.$modal?.msgError?.('获取详情失败')
  }).finally(() => {
    detailLoading.value = false
  })
}
      detailProducts.value =
        payload.purchaseReturnOrderProductsDetailVoList.map((item) => ({
          ...item,
          ...item.salesLedgerProduct,
        })) || [];
    })
    .catch(() => {
      proxy?.$modal?.msgError?.("获取详情失败");
    })
    .finally(() => {
      detailLoading.value = false;
    });
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList()
}
  getList();
};
const getList = () => {
  tableLoading.value = true
  findPurchaseReturnOrderListPage({ ...searchForm.value, ...page }).then(res => {
    tableLoading.value = false
    tableData.value = res.data.records
    page.total = res.data.total
  }).catch(() => {
    tableLoading.value = false
  })
}
  tableLoading.value = true;
  findPurchaseReturnOrderListPage({ ...searchForm.value, ...page })
    .then((res) => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      page.total = res.data.total;
    })
    .catch(() => {
      tableLoading.value = false;
    });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  // è¿‡æ»¤æŽ‰å­æ•°æ®
  selectedRows.value = selection.filter(item => item.id);
}
  selectedRows.value = selection.filter((item) => item.id);
};
const getReturnTypeLabel = (value) => {
  return returnTypeOptions.find(item => String(item.value) === String(value))?.label || '--'
}
  return (
    returnTypeOptions.find((item) => String(item.value) === String(value))
      ?.label || "--"
  );
};
const getProjectPhaseLabel = (value) => {
  return projectPhaseOptions.find(item => String(item.value) === String(value))?.label || '--'
}
  return (
    projectPhaseOptions.find((item) => String(item.value) === String(value))
      ?.label || "--"
  );
};
const formatAmount = (value) => {
  if (value === null || value === undefined || value === '') {
    return '--'
  if (value === null || value === undefined || value === "") {
    return "--";
  }
  const num = Number(value)
  const num = Number(value);
  if (Number.isNaN(num)) {
    return value
    return value;
  }
  return num.toFixed(2)
}
  return num.toFixed(2);
};
/** å·²é€€è´§æ•°é‡ = å…¥åº“行总数量 âˆ’ å½“前可退货数量(剩余) */
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0);
  const un = Number(row?.unQuantity ?? 0);
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
  return Math.max(total - un, 0);
};
onMounted(() => {
  getList()
})
  getList();
});
</script>
<style scoped>
.table_list {
    margin-top: unset;
  margin-top: unset;
}
</style>
src/views/qualityManagement/processInspection/components/formDia.vue
@@ -1,129 +1,165 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增过程检验' : '编辑过程检验'"
        width="70%"
        @close="closeDia"
    >
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'add' ? '新增过程检验' : '编辑过程检验'"
               width="70%"
               @close="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="工序:" prop="process">
              <el-select v-model="form.process" placeholder="请选择工序" clearable :disabled="processQuantityDisabled" style="width: 100%">
                <el-option v-for="item in processList" :key="item.name" :label="item.name" :value="item.name"/>
            <el-form-item label="工序:"
                          prop="process">
              <el-select v-model="form.process"
                         placeholder="请选择工序"
                         clearable
                         :disabled="processQuantityDisabled"
                         style="width: 100%">
                <el-option v-for="item in processList"
                           :key="item.name"
                           :label="item.name"
                           :value="item.name" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品名称:" prop="productId">
              <el-tree-select
                  v-model="form.productId"
                  placeholder="请选择"
                  clearable
                  check-strictly
                  @change="getModels"
                  :data="productOptions"
                  :render-after-expand="false"
                  :disabled="operationType === 'edit'"
                  style="width: 100%"
              />
            <el-form-item label="产品名称:"
                          prop="productId">
              <el-tree-select v-model="form.productId"
                              placeholder="请选择"
                              clearable
                              check-strictly
                              @change="getModels"
                              :data="productOptions"
                              :render-after-expand="false"
                              :disabled="operationType === 'edit'"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="productModelId">
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
            <el-form-item label="规格型号:"
                          prop="productModelId">
              <el-select v-model="form.productModelId"
                         placeholder="请选择"
                         clearable
                         :disabled="operationType === 'edit'"
                         filterable
                         readonly
                         @change="handleChangeModel">
                <el-option v-for="item in modelOptions"
                           :key="item.id"
                           :label="item.model"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="指标选择:" prop="testStandardId">
              <el-select
                v-model="form.testStandardId"
                placeholder="请选择指标"
                clearable
                @change="handleTestStandardChange"
                style="width: 100%"
              >
                <el-option
                  v-for="item in testStandardOptions"
                  :key="item.id"
                  :label="item.standardName || item.standardNo"
                  :value="item.id"
                />
            <el-form-item label="指标选择:"
                          prop="testStandardId">
              <el-select v-model="form.testStandardId"
                         placeholder="请选择指标"
                         clearable
                         @change="handleTestStandardChange"
                         style="width: 100%">
                <el-option v-for="item in testStandardOptions"
                           :key="item.id"
                           :label="item.standardName || item.standardNo"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="单位:" prop="unit">
              <el-input v-model="form.unit" placeholder="请输入" disabled/>
            <el-form-item label="单位:"
                          prop="unit">
              <el-input v-model="form.unit"
                        placeholder="请输入"
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入" clearable :precision="2" :disabled="processQuantityDisabled"/>
            <el-form-item label="数量:"
                          prop="quantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.quantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               :disabled="processQuantityDisabled" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:" prop="checkCompany">
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable/>
            <el-form-item label="检测单位:"
                          prop="checkCompany">
              <el-input v-model="form.checkCompany"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:" prop="checkResult">
            <el-form-item label="检测结果:"
                          prop="checkResult">
              <el-select v-model="form.checkResult">
                <el-option label="合格" value="合格" />
                <el-option label="不合格" value="不合格" />
                <el-option label="合格"
                           value="合格" />
                <el-option label="不合格"
                           value="不合格" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
                            <el-select v-model="form.checkName" placeholder="请选择" clearable>
                                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                                     :value="item.nickName"/>
                            </el-select>
            <el-form-item label="检验员:"
                          prop="checkName">
              <el-select v-model="form.checkName"
                         placeholder="请选择"
                         clearable>
                <el-option v-for="item in userList"
                           :key="item.nickName"
                           :label="item.nickName"
                           :value="item.nickName" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测日期:" prop="checkTime">
              <el-date-picker
                  v-model="form.checkTime"
                  type="date"
                  placeholder="请选择日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
              />
            <el-form-item label="检测日期:"
                          prop="checkTime">
              <el-date-picker v-model="form.checkTime"
                              type="date"
                              placeholder="请选择日期"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              clearable
                              style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
            <PIMTable
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400"
            >
                <template #slot="{ row }">
                    <el-input v-model="row.testValue" clearable/>
                </template>
            </PIMTable>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400">
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
                    clearable />
        </template>
      </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
@@ -132,332 +168,356 @@
</template>
<script setup>
import {ref, reactive, toRefs, computed, getCurrentInstance, nextTick} from "vue";
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js";
import {userListNoPage} from "@/api/system/user.js";
import {qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js";
import { list } from "@/api/productionManagement/productionProcess";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
  import {
    ref,
    reactive,
    toRefs,
    computed,
    getCurrentInstance,
    nextTick,
  } from "vue";
  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import {
    qualityInspectAdd,
    qualityInspectUpdate,
  } from "@/api/qualityManagement/rawMaterialInspection.js";
  import {
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
  } from "@/api/qualityManagement/metricMaintenance.js";
  import { userListNoPage } from "@/api/system/user.js";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/qualityInspectParam.js";
  import { list } from "@/api/productionManagement/productionProcess";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
const dialogFormVisible = ref(false);
const operationType = ref('')
const data = reactive({
  form: {
    checkTime: "",
    process: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
  },
  rules: {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" },],
    process: [{ required: true, message: "请选择工序", trigger: "change" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [{ required: true, message: "请选择", trigger: "change" }],
    testStandardId: [{required: false, message: "请选择指标", trigger: "change"}],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [{ required: true, message: "请输入", trigger: "change" }],
  },
});
const userList = ref([]);
const { form, rules } = toRefs(data);
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
const processQuantityDisabled = computed(() => {
  const v = form.value || {};
  return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
const processList = ref([]); // å·¥åºä¸‹æ‹‰åˆ—表(工序名称 name)
const supplierList = ref([]);
const productOptions = ref([]);
const tableColumn = ref([
    {
        label: "指标",
        prop: "parameterItem",
    },
    {
        label: "单位",
        prop: "unit",
    },
    {
        label: "标准值",
        prop: "standardValue",
    },
    {
        label: "内控值",
        prop: "controlValue",
    },
    {
        label: "检验值",
        prop: "testValue",
        dataType: 'slot',
        slot: 'slot',
    },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const currentProductId = ref(0);
const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
const modelOptions = ref([]);
// æ‰“开弹框
const openDialog = async (type, row) => {
    operationType.value = type;
    getOptions().then((res) => {
        supplierList.value = res.data;
    });
    // åŠ è½½å·¥åºä¸‹æ‹‰åˆ—è¡¨
    try {
        const res = await list();
        processList.value = res.data || [];
    } catch (e) {
        console.error("加载工序列表失败", e);
        processList.value = [];
    }
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    // å…ˆé‡ç½®è¡¨å•数据(保持字段完整,避免弹窗首次渲染时触发必填红框“闪一下”)
    form.value = {
        checkTime: "",
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
    }
    testStandardOptions.value = [];
    tableData.value = [];
    // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
    await getProductOptions();
    if (operationType.value === 'edit') {
        // å…ˆä¿å­˜ testStandardId,避免被清空
        const savedTestStandardId = row.testStandardId;
        // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
        form.value = {...row, testStandardId: ''}
        currentProductId.value = row.productId || 0
        // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
        if (currentProductId.value) {
            try {
                const res = await modelList({ id: currentProductId.value });
                modelOptions.value = res || [];
                // åŒæ­¥å›žå¡« model / unit(有些接口返回的 row é‡Œå¯èƒ½æ²¡å¸¦å…¨ï¼‰
                if (form.value.productModelId) {
                    handleChangeModel(form.value.productModelId);
                }
            } catch (e) {
                console.error("加载规格型号失败", e);
                modelOptions.value = [];
            }
        }
        // ç¼–辑模式下,先加载指标选项,然后加载参数列表
        if (currentProductId.value) {
            // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
            let params = {
                productId: currentProductId.value,
                inspectType: 1,
                process: form.value.process || ''
            }
            qualityInspectDetailByProductId(params).then(res => {
                testStandardOptions.value = res.data || [];
                // ä½¿ç”¨ nextTick å’Œ setTimeout ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“到 DOM
                nextTick(() => {
                    setTimeout(() => {
                        // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
                        if (savedTestStandardId) {
                            // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
                            const matchedOption = testStandardOptions.value.find(item =>
                                item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
                            );
                            if (matchedOption) {
                                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id(保持类型一致)
                                form.value.testStandardId = matchedOption.id;
                                // ç¼–辑保留原检验值,直接拉取原参数数据
                                getQualityInspectParamList(row.id);
                            } else {
                                // å¦‚果找不到匹配项,尝试直接使用原值
                                console.warn('未找到匹配的指标选项,testStandardId:', savedTestStandardId, '可用选项:', testStandardOptions.value);
                                form.value.testStandardId = savedTestStandardId;
                                getQualityInspectParamList(row.id);
                            }
                        } else {
                            // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
                            getQualityInspectParamList(row.id);
                        }
                    }, 100);
                });
            });
        } else {
            getQualityInspectParamList(row.id);
        }
    }
    // æœ€åŽå†æ‰“开弹窗,并清理校验态,避免必填提示闪烁
    dialogFormVisible.value = true;
    nextTick(() => {
        proxy.$refs?.formRef?.clearValidate?.();
    });
}
const getProductOptions = () => {
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
        return productOptions.value;
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const data = reactive({
    form: {
      checkTime: "",
      process: "",
      checkName: "",
      productName: "",
      productId: "",
      productModelId: "",
      model: "",
      testStandardId: "",
      unit: "",
      quantity: "",
      checkCompany: "",
      checkResult: "",
    },
    rules: {
      checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
      process: [{ required: true, message: "请选择工序", trigger: "change" }],
      checkName: [{ required: false, message: "请输入", trigger: "blur" }],
      productId: [{ required: true, message: "请输入", trigger: "blur" }],
      productModelId: [{ required: true, message: "请选择", trigger: "change" }],
      testStandardId: [
        { required: false, message: "请选择指标", trigger: "change" },
      ],
      unit: [{ required: false, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
      checkResult: [{ required: true, message: "请输入", trigger: "change" }],
    },
  });
};
const getModels = (value) => {
  form.value.productModelId = undefined;
  form.value.unit = undefined;
  modelOptions.value = [];
  currentProductId.value = value
  form.value.productName = findNodeById(productOptions.value, value);
  modelList({ id: value }).then((res) => {
    modelOptions.value = res;
  })
  if (currentProductId.value) {
    getList();
  }
};
  const userList = ref([]);
  const { form, rules } = toRefs(data);
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  const processList = ref([]); // å·¥åºä¸‹æ‹‰åˆ—表(工序名称 name)
  const supplierList = ref([]);
  const productOptions = ref([]);
  const tableColumn = ref([
    {
      label: "指标",
      prop: "parameterItem",
    },
    {
      label: "单位",
      prop: "unit",
    },
    {
      label: "标准值",
      prop: "standardValue",
    },
    {
      label: "内控值",
      prop: "controlValue",
    },
    {
      label: "检验值",
      prop: "testValue",
      dataType: "slot",
      slot: "slot",
    },
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const currentProductId = ref(0);
  const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
  const modelOptions = ref([]);
const handleChangeModel = (value) => {
  form.value.model = modelOptions.value.find(item => item.id == value)?.model || '';
  form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || '';
}
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
      return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
  // æ‰“开弹框
  const openDialog = async (type, row) => {
    operationType.value = type;
    getOptions().then(res => {
      supplierList.value = res.data;
    });
    // åŠ è½½å·¥åºä¸‹æ‹‰åˆ—è¡¨
    try {
      const res = await list({ size: -1, current: -1 });
      processList.value = res.data.records || [];
    } catch (e) {
      console.error("加载工序列表失败", e);
      processList.value = [];
    }
    if (nodes[i].children && nodes[i].children.length > 0) {
      const foundNode = findNodeById(nodes[i].children, productId);
      if (foundNode) {
        return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
    }
  }
  return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
};
function convertIdToValue(data) {
  return data.map((item) => {
    const { id, children, ...rest } = item;
    const newItem = {
      ...rest,
      value: id, // å°† id æ”¹ä¸º value
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    // å…ˆé‡ç½®è¡¨å•数据(保持字段完整,避免弹窗首次渲染时触发必填红框“闪一下”)
    form.value = {
      checkTime: "",
      process: "",
      checkName: "",
      productName: "",
      productId: "",
      productModelId: "",
      model: "",
      testStandardId: "",
      unit: "",
      quantity: "",
      checkCompany: "",
      checkResult: "",
    };
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
// å·¥åºå˜åŒ–处理
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      form.value.inspectType = 1
            const processName = form.value.process || '';
            if (operationType.value === "add") {
                tableData.value.forEach((item) => {
                    delete item.id
                })
            }
            const data = {
                ...form.value,
                process: processName, // ä¿ç•™ process å­—段以兼容后端
                qualityInspectParams: tableData.value
            }
      if (operationType.value === "add") {
        qualityInspectAdd(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
    testStandardOptions.value = [];
    tableData.value = [];
    // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
    await getProductOptions();
    if (operationType.value === "edit") {
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
      form.value = { ...row, testStandardId: "" };
      currentProductId.value = row.productId || 0;
      // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
      if (currentProductId.value) {
        try {
          const res = await modelList({ id: currentProductId.value });
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit(有些接口返回的 row é‡Œå¯èƒ½æ²¡å¸¦å…¨ï¼‰
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
          modelOptions.value = [];
        }
      }
      // ç¼–辑模式下,先加载指标选项,然后加载参数列表
      if (currentProductId.value) {
        // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
        let params = {
          productId: currentProductId.value,
          inspectType: 1,
          process: form.value.process || "",
        };
        qualityInspectDetailByProductId(params).then(res => {
          testStandardOptions.value = res.data || [];
          // ä½¿ç”¨ nextTick å’Œ setTimeout ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“到 DOM
          nextTick(() => {
            setTimeout(() => {
              // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
              if (savedTestStandardId) {
                // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
                const matchedOption = testStandardOptions.value.find(
                  item =>
                    item.id == savedTestStandardId ||
                    String(item.id) === String(savedTestStandardId)
                );
                if (matchedOption) {
                  // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id(保持类型一致)
                  form.value.testStandardId = matchedOption.id;
                  // ç¼–辑保留原检验值,直接拉取原参数数据
                  getQualityInspectParamList(row.id);
                } else {
                  // å¦‚果找不到匹配项,尝试直接使用原值
                  console.warn(
                    "未找到匹配的指标选项,testStandardId:",
                    savedTestStandardId,
                    "可用选项:",
                    testStandardOptions.value
                  );
                  form.value.testStandardId = savedTestStandardId;
                  getQualityInspectParamList(row.id);
                }
              } else {
                // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
                getQualityInspectParamList(row.id);
              }
            }, 100);
          });
        });
      } else {
        qualityInspectUpdate(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
        getQualityInspectParamList(row.id);
      }
    }
  })
}
const getList = () => {
    if (!currentProductId.value) {
        testStandardOptions.value = [];
        tableData.value = [];
        return;
    }
    const processName = form.value.process || '';
    let params = {
        productId: currentProductId.value,
        inspectType: 1,
        process: processName
    }
    qualityInspectDetailByProductId(params).then(res => {
        // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
        testStandardOptions.value = res.data || [];
        // æ¸…空表格数据,等待用户选择指标
        tableData.value = [];
        // æ¸…空指标选择
        form.value.testStandardId = '';
    })
}
    // æœ€åŽå†æ‰“开弹窗,并清理校验态,避免必填提示闪烁
    dialogFormVisible.value = true;
    nextTick(() => {
      proxy.$refs?.formRef?.clearValidate?.();
    });
  };
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
    });
  };
  const getModels = value => {
    form.value.productModelId = undefined;
    form.value.unit = undefined;
    modelOptions.value = [];
    currentProductId.value = value;
    form.value.productName = findNodeById(productOptions.value, value);
    modelList({ id: value }).then(res => {
      modelOptions.value = res;
    });
    if (currentProductId.value) {
      getList();
    }
  };
// æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
const handleTestStandardChange = (testStandardId) => {
    if (!testStandardId) {
        tableData.value = [];
        return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId).then(res => {
        tableData.value = res.data || [];
    }).catch(error => {
        console.error('获取标准参数失败:', error);
        tableData.value = [];
    }).finally(() => {
        tableLoading.value = false;
    })
}
const getQualityInspectParamList = (id) => {
    qualityInspectParamInfo(id).then(res => {
        tableData.value = res.data;
    })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  tableData.value = [];
  testStandardOptions.value = [];
  form.value.testStandardId = '';
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
  const handleChangeModel = value => {
    form.value.model =
      modelOptions.value.find(item => item.id == value)?.model || "";
    form.value.unit =
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // å·¥åºå˜åŒ–处理
  // æäº¤äº§å“è¡¨å•
  const submitForm = () => {
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        form.value.inspectType = 1;
        const processName = form.value.process || "";
        if (operationType.value === "add") {
          tableData.value.forEach(item => {
            delete item.id;
          });
        }
        const data = {
          ...form.value,
          process: processName, // ä¿ç•™ process å­—段以兼容后端
          qualityInspectParams: tableData.value,
        };
        if (operationType.value === "add") {
          qualityInspectAdd(data).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
          });
        } else {
          qualityInspectUpdate(data).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
          });
        }
      }
    });
  };
  const getList = () => {
    if (!currentProductId.value) {
      testStandardOptions.value = [];
      tableData.value = [];
      return;
    }
    const processName = form.value.process || "";
    let params = {
      productId: currentProductId.value,
      inspectType: 1,
      process: processName,
    };
    qualityInspectDetailByProductId(params).then(res => {
      // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
      testStandardOptions.value = res.data || [];
      // æ¸…空表格数据,等待用户选择指标
      tableData.value = [];
      // æ¸…空指标选择
      form.value.testStandardId = "";
    });
  };
  // æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
  const handleTestStandardChange = testStandardId => {
    if (!testStandardId) {
      tableData.value = [];
      return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const getQualityInspectParamList = id => {
    qualityInspectParamInfo(id).then(res => {
      tableData.value = res.data;
    });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    tableData.value = [];
    testStandardOptions.value = [];
    form.value.testStandardId = "";
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
</style>
src/views/salesManagement/deliveryLedger/index.vue
@@ -87,6 +87,11 @@
          show-overflow-tooltip
        />
        <el-table-column
          label="发货数量"
          prop="totalQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="发货车牌号"
          prop="shippingCarNumber"
          show-overflow-tooltip
@@ -105,7 +110,7 @@
          label="审核状态"
          prop="status"
          align="center"
          width="120"
          width="100"
        >
          <template #default="scope">
            <el-tag :type="getApprovalStatusType(scope.row.status)">
@@ -113,6 +118,12 @@
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="出库单号"
          prop="outboundBatches"
          show-overflow-tooltip
          width="130"
        />
        <el-table-column fixed="right" label="操作" width="220" align="center">
          <template #default="scope">
            <!--            <el-button-->
@@ -279,6 +290,9 @@
          <el-descriptions-item label="快递单号" :span="2">{{
            detailRow.expressNumber || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="出库单号" :span="2">{{
            detailRow.outboundBatches || "--"
          }}</el-descriptions-item>
        </el-descriptions>
        <el-table
          :data="getDeliveryProductInfoList()"
src/views/salesManagement/returnOrder/components/detailDia.vue
@@ -10,8 +10,8 @@
        <el-descriptions-item label="客户名称">{{ detail.customerName }}</el-descriptions-item>
        <el-descriptions-item label="销售单号">{{ detail.salesContractNo }}</el-descriptions-item>
        <el-descriptions-item label="业务员">{{ detail.salesman }}</el-descriptions-item>
        <el-descriptions-item label="关联出库单号">{{ detail.shippingNo }}</el-descriptions-item>
        <el-descriptions-item label="项目名称">{{ detail.projectName }}</el-descriptions-item>
        <el-descriptions-item label="关联发货单号">{{ detail.shippingNo }}</el-descriptions-item>
        <!-- <el-descriptions-item label="项目名称">{{ detail.projectName }}</el-descriptions-item> -->
        <el-descriptions-item label="制单人">{{ detail.maker }}</el-descriptions-item>
        <el-descriptions-item label="制单时间">{{ detail.makeTime }}</el-descriptions-item>
        <el-descriptions-item label="退货原因">{{ detail.returnReason }}</el-descriptions-item>
@@ -20,7 +20,11 @@
      <div style="padding-top: 20px">
        <span class="descriptions">产品列表</span>
        <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData" />
        <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
          <template #totalReturnNum="{ row }">
            {{ calcAlreadyReturned(row) }}
          </template>
        </PIMTable>
      </div>
    </div>
    <template #footer>
@@ -41,12 +45,214 @@
const tableData = ref([]);
const availableProducts = ref([]);
const sameKey = (a, b) => a != null && b != null && String(a) === String(b);
/** ä¸Ž formDia ä¸€è‡´ï¼šä¸¤ä»½åˆ—表按 id åˆå¹¶ï¼Œé¿å…åªå– productDtoData æ—¶ç¼ºå‡ºåº“单号/批次/数量 */
const mergeShippingProductLists = (data) => {
  const lists = [data?.shippingProductVoList, data?.productDtoData].filter(Array.isArray);
  if (!lists.length) return [];
  const map = new Map();
  for (const list of lists) {
    for (const p of list) {
      if (p == null) continue;
      const key = p.id != null ? String(p.id) : null;
      if (!key) continue;
      const prev = map.get(key);
      map.set(key, prev ? { ...prev, ...p } : { ...p });
    }
  }
  return Array.from(map.values());
};
const pickShippingLine = (normalized) => {
  const pid = normalized?.returnSaleLedgerProductId ?? normalized?.id;
  const sid = normalized?.stockOutRecordId ?? normalized?.shippingProductId;
  const direct = availableProducts.value.find(
    (p) =>
      sameKey(p?.id, pid) ||
      sameKey(p?.stockOutRecordId, pid) ||
      sameKey(p?.id, sid) ||
      sameKey(p?.stockOutRecordId, sid)
  );
  if (direct) return direct;
  const pmid = normalized?.productModelId;
  if (pmid == null || pmid === "") return undefined;
  const candidates = availableProducts.value.filter((p) => sameKey(p?.productModelId, pmid));
  if (!candidates.length) return undefined;
  if (candidates.length === 1) return candidates[0];
  const spec = String(normalized?.specificationModel ?? normalized?.model ?? "");
  if (spec) {
    const hit = candidates.find((p) => {
      const ps = String(p?.specificationModel ?? p?.model ?? "");
      return ps && ps === spec;
    });
    if (hit) return hit;
  }
  return candidates[0];
};
const isEmptyText = (v) => v === "" || v == null || v === undefined;
const firstFiniteNumber = (...vals) => {
  for (const v of vals) {
    if (v === "" || v == null || v === undefined) continue;
    const n = Number(v);
    if (Number.isFinite(n)) return n;
  }
  return undefined;
};
const firstNonEmptyText = (...vals) => {
  const hit = vals.find((v) => !isEmptyText(v));
  return hit === undefined ? "" : hit;
};
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockOutNum ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
  const un = Number(row?.unQuantity ?? 0);
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
  return Math.max(total - un, 0);
};
/** è¯¦æƒ…表用 productName / model;合并时勿让空串盖掉出库行字段 */
const mergeDetailProductRow = (product, normalized) => {
  const row = { ...product, ...normalized };
  row.outboundBatches = firstNonEmptyText(
    row.outboundBatches,
    product?.outboundBatches,
    product?.shippingNo,
    product?.outboundNo,
    normalized?.outboundBatches,
    normalized?.outboundNo,
    normalized?.shippingNo
  );
  row.batchNo = firstNonEmptyText(
    row.batchNo,
    product?.batchNo,
    product?.batchNumber,
    product?.lotNo,
    product?.batchCode,
    product?.shippingBatchNo,
    normalized?.batchNo,
    normalized?.batchNumber,
    normalized?.lotNo,
    normalized?.shippingBatchNo
  );
  const stock = firstFiniteNumber(
    row.stockOutNum,
    product?.stockOutNum,
    product?.totalQuantity,
    product?.shippingQuantity,
    product?.deliveryQuantity,
    product?.quantity,
    product?.outQuantity,
    normalized?.stockOutNum,
    normalized?.totalQuantity,
    normalized?.shippingQuantity,
    normalized?.deliveryQuantity
  );
  if (stock !== undefined) row.stockOutNum = stock;
  const un = firstFiniteNumber(
    row.unQuantity,
    product?.unQuantity,
    product?.remainingQuantity,
    product?.noReturnQuantity,
    product?.canReturnQuantity,
    product?.availableReturnNum,
    normalized?.unQuantity,
    normalized?.remainingQuantity,
    normalized?.noReturnQuantity,
    normalized?.canReturnQuantity
  );
  if (un !== undefined) row.unQuantity = un;
  else {
    const s = Number(row.stockOutNum);
    const ret = Number(row.totalReturnNum ?? 0);
    if (Number.isFinite(s) && s >= 0 && Number.isFinite(ret) && ret >= 0) {
      row.unQuantity = Math.max(0, s - ret);
    }
  }
  const returned = firstFiniteNumber(
    row.totalReturnNum,
    product?.totalReturnNum,
    product?.totalReturnedNum,
    normalized?.totalReturnNum,
    normalized?.totalReturnedNum
  );
  if (returned !== undefined) row.totalReturnNum = returned;
  else if (isEmptyText(row.totalReturnNum)) row.totalReturnNum = 0;
  if (isEmptyText(row.unit)) {
    row.unit = firstNonEmptyText(product?.unit, normalized?.unit);
  }
  row.productName = firstNonEmptyText(
    row.productName,
    normalized?.productName,
    normalized?.productCategory,
    product?.productName,
    product?.productCategory
  );
  row.model = firstNonEmptyText(
    row.model,
    normalized?.model,
    normalized?.specificationModel,
    product?.model,
    product?.specificationModel
  );
  return row;
};
const normalizeDetailRow = (raw) => {
  const ledgerId =
    raw?.returnSaleLedgerProductId ??
    raw?.saleLedgerProductId ??
    raw?.stockOutRecordId ??
    raw?.shippingProductId;
  const productId = ledgerId ?? raw?.id;
  const num = Number(raw?.num ?? raw?.returnQuantity ?? 0);
  return {
    ...raw,
    id: productId,
    returnSaleLedgerProductId: productId,
    productModelId: raw?.productModelId,
    stockOutRecordId: raw?.stockOutRecordId,
    shippingProductId: raw?.shippingProductId,
    productName: raw?.productName ?? raw?.productCategory ?? raw?.productTypeName ?? "",
    model: raw?.model ?? raw?.specificationModel ?? raw?.specModel ?? "",
    outboundBatches: raw?.outboundBatches ?? raw?.outboundNo ?? raw?.shippingNo,
    batchNo:
      raw?.batchNo ??
      raw?.batchNumber ??
      raw?.lotNo ??
      raw?.batchCode ??
      raw?.shippingBatchNo,
    stockOutNum:
      raw?.stockOutNum ??
      raw?.totalQuantity ??
      raw?.shippingQuantity ??
      raw?.deliveryQuantity ??
      raw?.quantity,
    totalReturnNum: raw?.totalReturnNum ?? raw?.totalReturnedNum,
    unQuantity:
      raw?.unQuantity ??
      raw?.remainingQuantity ??
      raw?.noReturnQuantity ??
      raw?.canReturnQuantity,
    returnQuantity: Number.isFinite(num) ? num : 0,
    price: Number(raw?.taxInclusiveUnitPrice ?? raw?.price ?? 0),
    amount: Number(raw?.amount ?? 0).toFixed(2),
    isQuality: raw?.isQuality ?? 2,
    remark: raw?.remark ?? "",
  };
};
const tableColumn = [
  {align: "center", label: "产品大类", prop: "productCategory"},
  {align: "center", label: "规格型号", prop: "specificationModel"},
  {align: "center", label: "出库单号", prop: "outboundBatches"},
  {align: "center", label: "批次号", prop: "batchNo"},
  {align: "center", label: "产品大类", prop: "productName"},
  {align: "center", label: "规格型号", prop: "model"},
  {align: "center", label: "单位", prop: "unit", width: 80},
  {align: "center", label: "总数量", prop: "quantity", width: 120},
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120},
  {align: "center", label: "总数量", prop: "stockOutNum", width: 120},
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum"},
  {align: "center", label: "未退货数量", prop: "unQuantity", width: 120},
  {align: "center", label: "退货数量", prop: "returnQuantity", width: 120},
  {align: "center", label: "退货产品单价", prop: "price", width: 120},
@@ -82,30 +288,30 @@
    if (detail.value.shippingId) {
      const productRes = await returnManagementGetByShippingId({ shippingId: detail.value.shippingId });
      if (productRes.code === 200) {
        availableProducts.value = productRes.data.productDtoData || [];
        availableProducts.value = mergeShippingProductLists(productRes.data);
      }
    }
    const list =
      detail.value?.returnSaleProducts ||
        detail.value?.returnSaleProductList ||
        detail.value?.returnSaleProductDtoData ||
        [];
    tableData.value = Array.isArray(list) ? list.map(raw => {
      const productId = raw?.returnSaleLedgerProductId ?? raw?.saleLedgerProductId ?? raw?.id;
      const product = availableProducts.value.find((p) => p.id === productId);
      const normalized = {
        ...raw,
        id: productId,
        returnQuantity: Number(raw?.num ?? raw?.returnQuantity ?? 0),
        price: Number(raw?.taxInclusiveUnitPrice ?? raw?.price ?? 0),
        amount: Number(raw?.amount ?? 0).toFixed(2),
        isQuality: raw?.isQuality ?? 2,
        remark: raw?.remark ?? "",
      };
      return product ? { ...product, ...normalized } : normalized;
    }) : [];
      detail.value?.returnSaleProductList ||
      detail.value?.returnSaleProductDtoData ||
      [];
    tableData.value = Array.isArray(list)
      ? list.map((raw) => {
          const normalized = normalizeDetailRow(raw);
          const product = pickShippingLine(normalized);
          return product ? mergeDetailProductRow(product, normalized) : normalized;
        })
      : [];
    const headerShipNo = detail.value?.shippingNo;
    if (headerShipNo && Array.isArray(tableData.value) && tableData.value.length) {
      tableData.value = tableData.value.map((r) =>
        isEmptyText(r.outboundBatches) ? { ...r, outboundBatches: headerShipNo } : r
      );
    }
  } catch (e) {
    console.error("Failed to load detail", e);
  } finally {
src/views/salesManagement/returnOrder/components/formDia.vue
@@ -32,7 +32,7 @@
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="关联出库单号:" prop="shippingId">
              <el-form-item label="关联发货单号:" prop="shippingId">
                <el-select v-model="form.shippingId" filterable placeholder="请选择出库单号" @change="outboundNoChange">
                  <el-option
                    v-for="item in outboundOptions"
@@ -82,6 +82,9 @@
            <el-button type="primary" @click="openProductSelection" :disabled="!form.shippingId">添加产品</el-button>
          </div>
          <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
            <template #totalReturnNum="{ row }">
              {{ calcAlreadyReturned(row) }}
            </template>
            <template #returnQuantity="{ row }">
              <el-input 
                v-model="row.returnQuantity" 
@@ -122,7 +125,7 @@
                placeholder="请输入" 
              />
            </template>
            <template #action="{ row, index }">
            <template #action="{ index }">
              <el-button type="danger" link @click="deleteRow(index)">删除</el-button>
            </template>
          </PIMTable>
@@ -145,10 +148,12 @@
        row-key="id"
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" prop="outboundBatches" label="出库单号" />
        <el-table-column align="center" prop="batchNo" label="批次号" />
        <el-table-column align="center" prop="productCategory" label="产品大类" />
        <el-table-column align="center" prop="specificationModel" label="规格型号" />
        <el-table-column align="center" prop="unit" label="单位" />
        <el-table-column align="center" prop="quantity" label="总数量" />
        <el-table-column align="center" prop="stockOutNum" label="总数量" />
        <el-table-column align="center" prop="unQuantity" label="未退货数量" />
        <el-table-column align="center" label="已退货数量">
          <template #default="{ row }">{{ calcAlreadyReturned(row) }}</template>
@@ -208,18 +213,20 @@
const { form, rules } = toRefs(data);
const calcAlreadyReturned = (row) => {
  const total = Number(row?.quantity ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
  const total = Number(row?.stockOutNum ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
  const un = Number(row?.unQuantity ?? 0);
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
  return Math.max(total - un, 0);
};
const tableColumn = ref([
  {align: "center", label: "出库单号", prop: "outboundBatches" },
  {align: "center", label: "批次号", prop: "batchNo" },
  {align: "center", label: "产品大类", prop: "productCategory" },
  {align: "center", label: "规格型号", prop: "specificationModel" },
  {align: "center", label: "单位", prop: "unit", width: 80 },
  {align: "center", label: "总数量", prop: "quantity", width: 120 },
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120 },
  {align: "center", label: "总数量", prop: "stockOutNum", width: 120 },
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum" },
  {align: "center", label: "未退货数量", prop: "unQuantity", width: 120 },
  {align: "center", label: "退货数量", prop: "returnQuantity", dataType: "slot", slot: "returnQuantity", width: 120 },
  {align: "center", label: "退货产品单价", prop: "price", dataType: "slot", slot: "price", width: 120 },
@@ -238,8 +245,164 @@
  tableData.value.splice(index, 1);
};
const sameKey = (a, b) => a != null && b != null && String(a) === String(b);
/** æŽ¥å£å¯èƒ½æ‹†æˆ shippingProductVoList / productDtoData ä¸¤ä»½ï¼Œåªå–其一会缺批次、数量等字段 */
const mergeShippingProductLists = (data) => {
  const lists = [data?.shippingProductVoList, data?.productDtoData].filter(Array.isArray);
  if (!lists.length) return [];
  const map = new Map();
  for (const list of lists) {
    for (const p of list) {
      if (p == null) continue;
      const key = p.id != null ? String(p.id) : null;
      if (!key) continue;
      const prev = map.get(key);
      map.set(key, prev ? { ...prev, ...p } : { ...p });
    }
  }
  return Array.from(map.values());
};
const pickShippingLine = (normalized) => {
  const pid = normalized?.returnSaleLedgerProductId ?? normalized?.id;
  const sid = normalized?.stockOutRecordId ?? normalized?.shippingProductId;
  const direct = availableProducts.value.find(
    (p) =>
      sameKey(p?.id, pid) ||
      sameKey(p?.stockOutRecordId, pid) ||
      sameKey(p?.id, sid) ||
      sameKey(p?.stockOutRecordId, sid)
  );
  if (direct) return direct;
  const pmid = normalized?.productModelId;
  if (pmid == null || pmid === "") return undefined;
  const candidates = availableProducts.value.filter((p) => sameKey(p?.productModelId, pmid));
  if (!candidates.length) return undefined;
  if (candidates.length === 1) return candidates[0];
  const spec = String(normalized?.specificationModel ?? normalized?.model ?? "");
  if (spec) {
    const hit = candidates.find((p) => {
      const ps = String(p?.specificationModel ?? p?.model ?? "");
      return ps && ps === spec;
    });
    if (hit) return hit;
  }
  return candidates[0];
};
const isEmptyText = (v) => v === "" || v == null || v === undefined;
const firstFiniteNumber = (...vals) => {
  for (const v of vals) {
    if (v === "" || v == null || v === undefined) continue;
    const n = Number(v);
    if (Number.isFinite(n)) return n;
  }
  return undefined;
};
const firstNonEmptyText = (...vals) => {
  const hit = vals.find((v) => !isEmptyText(v));
  return hit === undefined ? "" : hit;
};
/** è¯¦æƒ…接口字段常不全;{...product,...normalized} ä¼šè¢« normalized é‡Œçš„空串盖掉出库行上的展示字段 */
const mergeShippingLineWithDetail = (product, normalized) => {
  const row = { ...product, ...normalized };
  row.outboundBatches = firstNonEmptyText(
    row.outboundBatches,
    product?.outboundBatches,
    product?.shippingNo,
    product?.outboundNo,
    normalized?.outboundBatches,
    normalized?.outboundNo,
    normalized?.shippingNo
  );
  row.batchNo = firstNonEmptyText(
    row.batchNo,
    product?.batchNo,
    product?.batchNumber,
    product?.lotNo,
    product?.batchCode,
    product?.shippingBatchNo,
    normalized?.batchNo,
    normalized?.batchNumber,
    normalized?.lotNo,
    normalized?.shippingBatchNo
  );
  const stock = firstFiniteNumber(
    row.stockOutNum,
    product?.stockOutNum,
    product?.totalQuantity,
    product?.shippingQuantity,
    product?.deliveryQuantity,
    product?.quantity,
    product?.outQuantity,
    normalized?.stockOutNum,
    normalized?.totalQuantity,
    normalized?.shippingQuantity,
    normalized?.deliveryQuantity
  );
  if (stock !== undefined) row.stockOutNum = stock;
  const un = firstFiniteNumber(
    row.unQuantity,
    product?.unQuantity,
    product?.remainingQuantity,
    product?.noReturnQuantity,
    product?.canReturnQuantity,
    product?.availableReturnNum,
    normalized?.unQuantity,
    normalized?.remainingQuantity,
    normalized?.noReturnQuantity,
    normalized?.canReturnQuantity
  );
  if (un !== undefined) row.unQuantity = un;
  else {
    const s = Number(row.stockOutNum);
    const ret = Number(row.totalReturnNum ?? 0);
    if (Number.isFinite(s) && s >= 0 && Number.isFinite(ret) && ret >= 0) {
      row.unQuantity = Math.max(0, s - ret);
    }
  }
  const returned = firstFiniteNumber(
    row.totalReturnNum,
    product?.totalReturnNum,
    product?.totalReturnedNum,
    normalized?.totalReturnNum,
    normalized?.totalReturnedNum
  );
  if (returned !== undefined) row.totalReturnNum = returned;
  else if (isEmptyText(row.totalReturnNum)) row.totalReturnNum = 0;
  if (isEmptyText(row.unit)) {
    row.unit = firstNonEmptyText(product?.unit, normalized?.unit);
  }
  if (isEmptyText(row.productCategory)) {
    row.productCategory = firstNonEmptyText(
      normalized?.productCategory,
      normalized?.productName,
      product?.productCategory,
      product?.productName
    );
  }
  if (isEmptyText(row.specificationModel)) {
    row.specificationModel = firstNonEmptyText(
      normalized?.specificationModel,
      normalized?.model,
      product?.specificationModel,
      product?.model
    );
  }
  return row;
};
const normalizeDetailRow = (raw) => {
  const productId = raw?.returnSaleLedgerProductId ?? raw?.saleLedgerProductId ?? raw?.id;
  const ledgerId =
    raw?.returnSaleLedgerProductId ??
    raw?.saleLedgerProductId ??
    raw?.stockOutRecordId ??
    raw?.shippingProductId;
  const productId = ledgerId ?? raw?.id;
  const returnSaleProductId = raw?.returnSaleProductId ?? raw?.id;
  const num = Number(raw?.num ?? raw?.returnQuantity ?? 0);
  return {
@@ -248,6 +411,29 @@
    returnSaleProductId,
    returnSaleLedgerProductId: productId,
    productModelId: raw?.productModelId,
    stockOutRecordId: raw?.stockOutRecordId,
    shippingProductId: raw?.shippingProductId,
    productCategory: raw?.productCategory ?? raw?.productName ?? raw?.productTypeName ?? "",
    specificationModel: raw?.specificationModel ?? raw?.model ?? raw?.specModel ?? "",
    outboundBatches: raw?.outboundBatches ?? raw?.outboundNo ?? raw?.shippingNo,
    batchNo:
      raw?.batchNo ??
      raw?.batchNumber ??
      raw?.lotNo ??
      raw?.batchCode ??
      raw?.shippingBatchNo,
    stockOutNum:
      raw?.stockOutNum ??
      raw?.totalQuantity ??
      raw?.shippingQuantity ??
      raw?.deliveryQuantity ??
      raw?.quantity,
    totalReturnNum: raw?.totalReturnNum ?? raw?.totalReturnedNum,
    unQuantity:
      raw?.unQuantity ??
      raw?.remainingQuantity ??
      raw?.noReturnQuantity ??
      raw?.canReturnQuantity,
    num,
    returnQuantity: Number.isFinite(num) ? num : 0,
    price: Number(raw?.taxInclusiveUnitPrice ?? raw?.price ?? 0),
@@ -259,7 +445,6 @@
const setFormForEdit = async (row) => {
  const res = await returnManagementGetById({ returnManagementId: row?.id });
  console.log("res", res);
  const detail = res?.data ?? res ?? {};
  Object.assign(form.value, detail);
@@ -281,11 +466,18 @@
  tableData.value = Array.isArray(list)
    ? list.map((raw) => {
        const normalized = normalizeDetailRow(raw);
        const product = availableProducts.value.find((p) => p.id === normalized.id);
        return product ? { ...product, ...normalized } : normalized;
        const product = pickShippingLine(normalized);
        return product ? mergeShippingLineWithDetail(product, normalized) : normalized;
      })
    : [];
  const headerShipNo = detail?.shippingNo ?? form.value?.shippingNo;
  if (headerShipNo && Array.isArray(tableData.value) && tableData.value.length) {
    tableData.value = tableData.value.map((r) =>
      isEmptyText(r.outboundBatches) ? { ...r, outboundBatches: headerShipNo } : r
    );
  }
  calculateTotalRefund();
};
@@ -320,7 +512,7 @@
  proxy.$refs["formRef"].validate(valid => {
    if (!valid) return;
    const returnSaleProducts = (tableData.value || []).map(el => ({
      returnSaleLedgerProductId: el.returnSaleLedgerProductId ?? el.id,
      stockOutRecordId: el.returnSaleLedgerProductId ?? el.id,
      productModelId: el.productModelId,
      unit: el.unit,
      num: Number(el.num ?? el.returnQuantity ?? 0),
@@ -419,8 +611,7 @@
    // If backend returns project info, set it
    if (res.data.projectId) form.value.projectId = res.data.projectId;
    
    // Store available products for selection
    availableProducts.value = res.data.productDtoData || [];
    availableProducts.value = mergeShippingProductLists(res.data);
    if (clearTable) tableData.value = [];
  }
};
@@ -457,9 +648,9 @@
};
const calculateRowAmount = (row) => {
  const quantity = Number(row.returnQuantity || 0);
  const stockOutNum = Number(row.returnQuantity || 0);
  const price = Number(row.price || 0);
  row.amount = (quantity * price).toFixed(2);
  row.amount = (stockOutNum * price).toFixed(2);
};
const calculateTotalRefund = () => {
@@ -511,10 +702,11 @@
        amount: "0.00",
        isQuality: 2,
        remark: "",
        productCategory: product.productCategory ?? product.productName ?? "",
        productName: product.productName,
        specificationModel: product.specificationModel,
        specificationModel: product.specificationModel ?? product.model ?? "",
        unit: product.unit,
        quantity: product.quantity,
        stockOutNum: product.stockOutNum,
        totalReturnNum: product.totalReturnNum,
        unQuantity: product.unQuantity
      });
src/views/salesManagement/returnOrder/index.vue
@@ -11,8 +11,8 @@
                <el-form-item label="销售单号">
                    <el-input v-model="searchForm.salesContractNo" placeholder="销售单号" clearable />
                </el-form-item>
                <el-form-item label="关联出库单号">
                    <el-input v-model="searchForm.shippingNo" placeholder="关联出库单号" clearable />
                <el-form-item label="关联发货单号">
                    <el-input v-model="searchForm.shippingNo" placeholder="关联发货单号" clearable />
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="handleQuery">搜索</el-button>
@@ -112,22 +112,22 @@
]);
const defaultColumns = [
  { label: "退货单号", prop: "returnNo", width: 160 },
  { label: "单据状态", prop: "status", width: 90, dataType: "slot", slot: "status" },
  { label: "制单时间", prop: "makeTime", width: 170 },
  { label: "客户名称", prop: "customerName", width: 220 },
  { label: "销售单号", prop: "salesContractNo", width: 160 },
  { label: "业务员", prop: "salesman", width: 120 },
  { label: "关联出库单号", prop: "shippingNo", width: 170 },
  { label: "项目名称", prop: "projectName", width: 180 },
  { label: "制单人", prop: "maker", width: 120 },
  { label: "退货单号", prop: "returnNo", minWidth: 160 },
  { label: "单据状态", prop: "status", minWidth: 90, dataType: "slot", slot: "status" },
  { label: "制单时间", prop: "makeTime", minWidth: 170 },
  { label: "客户名称", prop: "customerName", minWidth: 220 },
  { label: "销售单号", prop: "salesContractNo", minWidth: 160 },
  { label: "业务员", prop: "salesman", minWidth: 120 },
  { label: "关联发货单号", prop: "shippingNo", minWidth: 170 },
  { label: "项目名称", prop: "projectName", minWidth: 180 },
  { label: "制单人", prop: "maker", minWidth: 120 },
  {
    label: "操作",
    prop: "operation",
    dataType: "action",
    align: "center",
    fixed: "right",
    width: 240,
    minWidth: 240,
    operation: [
      { name: "编辑", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => openForm("edit", row) },
      { name: "退款处理", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => handleRowHandle(row) },
src/views/salesManagement/salesLedger/index.vue
@@ -2594,6 +2594,7 @@
      å®¡æ ¸æ‹’绝: "审核拒绝",
      å®¡æ ¸é€šè¿‡: "审核通过",
      å·²å‘è´§: "已发货",
      éƒ¨åˆ†å‘è´§: "部分发货",
    };
    return statusTextMap[statusStr] || "待发货";
  };
@@ -2625,6 +2626,7 @@
      å®¡æ ¸æ‹’绝: "danger",
      å®¡æ ¸é€šè¿‡: "success",
      å·²å‘è´§: "success",
      éƒ¨åˆ†å‘è´§: "warning",
    };
    return typeTextMap[statusStr] || "info";
  };