1.客户,销售,供应商,采购增加总合同号
2.采购的产品增加库存状态回显
3.出厂改成创建时间倒序
4.开发总合同号管理页面
已添加5个文件
已修改6个文件
2174 ■■■■ 文件已修改
doc/customer_contract_record.md 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/purchase_master_contract_linkage.md 345 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sales_master_contract_linkage.md 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerContract.js 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/supplierContract.js 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 411 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/HomeTab.vue 529 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/formDia.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 132 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/customer_contract_record.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
# å®¢æˆ·æ¡£æ¡ˆç§æµ·-合同记录
## æ¶‰åŠé¡µé¢
- å®¢æˆ·æ¡£æ¡ˆï¼ˆç§æµ·ï¼‰è¯¦æƒ…页
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /basic/customer-contract/list/{customerId} | æŸ¥è¯¢åˆåŒè®°å½•列表 |
| GET | /basic/customer-contract/{id} | èŽ·å–åˆåŒè®°å½•è¯¦æƒ… |
| POST | /basic/customer-contract | æ–°å¢žåˆåŒè®°å½• |
| PUT | /basic/customer-contract | ä¿®æ”¹åˆåŒè®°å½• |
| DELETE | /basic/customer-contract/{id} | åˆ é™¤åˆåŒè®°å½• |
**注意:附件使用系统通用接口 `/basic/storage-attachment` ç®¡ç†**
### è¯·æ±‚参数
**新增/修改合同记录:**
```json
{
    "id": null,  // ä¿®æ”¹æ—¶å¿…ä¼ 
    "customerId": 1,  // å®¢æˆ·æ¡£æ¡ˆID(必填)
    "masterContractNo": "HT202606240001",  // æ€»åˆåŒå·ï¼ˆå¿…填)
    "entryDate": "2026-06-24 10:30:00",  // å½•入日期(必填)
    "entryPerson": "张三",  // å½•入人姓名
    "entryPersonId": 1,  // å½•入人ID
    "remark": "备注信息",  // å¤‡æ³¨
    "storageBlobDTOs": [  // é™„件列表
        { "id": 1 },
        { "id": 2 }
    ]
}
```
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | ä¿®æ”¹æ—¶å¿…ä¼  |
| customerId | Long | æ˜¯ | å®¢æˆ·æ¡£æ¡ˆID |
| masterContractNo | String | æ˜¯ | æ€»åˆåŒå· |
| entryDate | String | æ˜¯ | å½•入日期(yyyy-MM-dd HH:mm:ss) |
| entryPerson | String | å¦ | å½•入人姓名 |
| entryPersonId | Long | å¦ | å½•入人ID |
| remark | String | å¦ | å¤‡æ³¨ |
| storageBlobDTOs | List | å¦ | é™„ä»¶ID列表 |
**响应:**
```json
{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "id": 1,
        "customerId": 1,
        "masterContractNo": "HT202606240001",
        "entryDate": "2026-06-24 10:30:00",
        "entryPerson": "张三",
        "entryPersonId": 1,
        "remark": "备注信息",
        "fileList": [
            {
                "id": 1,
                "name": "合同扫描件.pdf",
                "previewURL": "xxx",
                "downloadURL": "xxx",
                "byteSize": 102400,
                "contentType": "application/pdf"
            }
        ]
    }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. åœ¨å®¢æˆ·æ¡£æ¡ˆï¼ˆç§æµ·ï¼‰è¯¦æƒ…页添加合同记录Tab
```html
<el-tabs v-model="activeTab">
  <!-- å…¶ä»–Tab -->
  <el-tab-pane label="合同记录" name="contract">
    <el-button type="primary" size="small" @click="handleAddContract">新增合同记录</el-button>
    <el-table :data="contractList" style="width: 100%; margin-top: 15px">
      <el-table-column prop="masterContractNo" label="总合同号" />
      <el-table-column prop="entryDate" label="录入日期" width="180" />
      <el-table-column prop="entryPerson" label="录入人" width="120" />
      <el-table-column prop="remark" label="备注" show-overflow-tooltip />
      <el-table-column label="附件" width="100">
        <template #default="{ row }">
          <el-link type="primary" v-if="row.fileList && row.fileList.length" @click="handleViewFiles(row)">
            æŸ¥çœ‹({{ row.fileList.length }})
          </el-link>
          <span v-else>-</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150">
        <template #default="{ row }">
          <el-button type="text" size="small" @click="handleEditContract(row)">编辑</el-button>
          <el-button type="text" size="small" style="color: #f56c6c" @click="handleDeleteContract(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-tab-pane>
</el-tabs>
```
### 2. æ–°å¢ž/编辑合同记录弹窗(使用系统通用上传组件)
```html
<el-dialog :title="contractDialogTitle" v-model="contractDialogVisible" width="600px">
  <el-form :model="contractForm" :rules="contractRules" ref="contractFormRef" label-width="100px">
    <el-form-item label="总合同号" prop="masterContractNo">
      <el-input v-model="contractForm.masterContractNo" placeholder="请输入总合同号" />
    </el-form-item>
    <el-form-item label="录入日期" prop="entryDate">
      <el-date-picker
        v-model="contractForm.entryDate"
        type="datetime"
        placeholder="选择日期时间"
        value-format="yyyy-MM-dd HH:mm:ss"
        style="width: 100%"
      />
    </el-form-item>
    <el-form-item label="录入人" prop="entryPerson">
      <el-input v-model="contractForm.entryPerson" placeholder="请输入录入人" />
    </el-form-item>
    <el-form-item label="附件">
      <!-- ä½¿ç”¨ç³»ç»Ÿå°è£…的文件上传组件 -->
      <file-upload
        v-model="contractForm.storageBlobDTOs"
        :record-type="'customer_contract_record'"
        :record-id="contractForm.id"
      />
    </el-form-item>
    <el-form-item label="备注">
      <el-input v-model="contractForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="contractDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="submitContractForm">确定</el-button>
  </template>
</el-dialog>
```
### 3. data æ•°æ®
```js
data() {
  return {
    // åˆåŒè®°å½•相关
    contractList: [],
    contractDialogVisible: false,
    contractDialogTitle: '',
    contractForm: {
      id: null,
      customerId: null,
      masterContractNo: '',
      entryDate: '',
      entryPerson: '',
      entryPersonId: null,
      remark: '',
      storageBlobDTOs: []  // é™„件列表,使用系统通用格式
    },
    contractRules: {
      masterContractNo: [{ required: true, message: '请输入总合同号', trigger: 'blur' }],
      entryDate: [{ required: true, message: '请选择录入日期', trigger: 'change' }]
    }
  }
}
```
### 4. æ–¹æ³•
```js
methods: {
  // èŽ·å–åˆåŒè®°å½•åˆ—è¡¨ï¼ˆå«é™„ä»¶ï¼‰
  async getContractList(customerId) {
    const res = await this.$http.get(`/basic/customer-contract/list/${customerId}`)
    if (res.code === 200) {
      this.contractList = res.data
    }
  },
  // æ–°å¢žåˆåŒè®°å½•
  handleAddContract() {
    this.contractDialogTitle = '新增合同记录'
    this.contractForm = {
      id: null,
      customerId: this.customerId,
      masterContractNo: '',
      entryDate: '',
      entryPerson: '',
      entryPersonId: null,
      remark: '',
      storageBlobDTOs: []
    }
    this.contractDialogVisible = true
  },
  // ç¼–辑合同记录
  async handleEditContract(row) {
    this.contractDialogTitle = '编辑合同记录'
    const res = await this.$http.get(`/basic/customer-contract/${row.id}`)
    if (res.code === 200) {
      this.contractForm = {
        ...res.data,
        storageBlobDTOs: res.data.fileList ? res.data.fileList.map(f => ({ id: f.id })) : []
      }
      this.contractDialogVisible = true
    }
  },
  // åˆ é™¤åˆåŒè®°å½•
  async handleDeleteContract(row) {
    await this.$confirm('确定删除该合同记录吗?', '提示', { type: 'warning' })
    const res = await this.$http.delete(`/basic/customer-contract/${row.id}`)
    if (res.code === 200) {
      this.$message.success('删除成功')
      this.getContractList(this.customerId)
    }
  },
  // æäº¤åˆåŒè®°å½•表单
  async submitContractForm() {
    await this.$refs.contractFormRef.validate()
    const res = await this.$http.post('/basic/customer-contract', this.contractForm)
    if (res.code === 200) {
      this.$message.success('操作成功')
      this.contractDialogVisible = false
      this.getContractList(this.customerId)
    }
  },
  // æŸ¥çœ‹é™„件列表
  handleViewFiles(row) {
    // å¼¹çª—展示附件列表,使用 row.fileList
  }
}
```
## åŽç«¯æ–‡ä»¶æ¸…单
| æ–‡ä»¶è·¯å¾„ | è¯´æ˜Ž |
|---------|------|
| `src/main/java/com/ruoyi/basic/pojo/CustomerContractRecord.java` | å®žä½“ç±» |
| `src/main/java/com/ruoyi/basic/dto/CustomerContractRecordDto.java` | DTO(含附件列表) |
| `src/main/java/com/ruoyi/basic/mapper/CustomerContractRecordMapper.java` | Mapper接口 |
| `src/main/resources/mapper/basic/CustomerContractRecordMapper.xml` | Mapper XML |
| `src/main/java/com/ruoyi/basic/service/CustomerContractRecordService.java` | Service接口 |
| `src/main/java/com/ruoyi/basic/service/impl/CustomerContractRecordServiceImpl.java` | Service实现 |
| `src/main/java/com/ruoyi/basic/controller/CustomerContractRecordController.java` | Controller |
| `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java` | æ·»åŠ æžšä¸¾å€¼ `CUSTOMER_CONTRACT_RECORD` |
## æ³¨æ„äº‹é¡¹
- é™„件上传使用系统封装的 `StorageAttachment` æœåŠ¡ï¼Œé€šè¿‡ `record_type='customer_contract_record'` å’Œ `record_id` å…³è”
- å‰ç«¯å¯ä½¿ç”¨é¡¹ç›®å°è£…çš„ `file-upload` ç»„件,组件内部调用 `/basic/storage-attachment` æŽ¥å£
- åˆ é™¤åˆåŒè®°å½•时,后端会自动同步删除关联的附件
- æ–°å¢žåˆåŒè®°å½•成功后,返回的 `data.id` ä¸ºæ–°å¢žè®°å½•çš„ID
doc/purchase_master_contract_linkage.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,345 @@
# é‡‡è´­å°è´¦-总合同号联动选择及附件查看
## æ¶‰åŠé¡µé¢
- ä¾›åº”商管理详情页(新增合同记录Tab)
- é‡‡è´­å°è´¦åˆ—表页/新增编辑页
## æ–°å¢žAPI
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /basic/supplier-contract/list/{supplierId} | æŸ¥è¯¢ä¾›åº”商合同记录列表(供下拉选择) |
| GET | /basic/supplier-contract/{id} | èŽ·å–ä¾›åº”å•†åˆåŒè®°å½•è¯¦æƒ… |
| POST | /basic/supplier-contract | æ–°å¢žä¾›åº”商合同记录 |
| PUT | /basic/supplier-contract | ä¿®æ”¹ä¾›åº”商合同记录 |
| DELETE | /basic/supplier-contract/{id} | åˆ é™¤ä¾›åº”商合同记录 |
| GET | /basic/supplier-contract/files/byContractNo?masterContractNo=xxx | æ ¹æ®æ€»åˆåŒå·æŸ¥è¯¢é™„ä»¶ |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ä¾›åº”商管理详情页 - æ·»åŠ åˆåŒè®°å½•Tab
```html
<el-tabs v-model="activeTab">
  <!-- åŸºæœ¬ä¿¡æ¯Tab -->
  <el-tab-pane label="基本信息" name="basic">...</el-tab-pane>
  <!-- åˆåŒè®°å½•Tab -->
  <el-tab-pane label="合同记录" name="contract">
    <el-button type="primary" size="small" @click="handleAddContract">新增合同记录</el-button>
    <el-table :data="contractList" style="width: 100%; margin-top: 15px">
      <el-table-column prop="masterContractNo" label="总合同号" />
      <el-table-column prop="entryDate" label="录入日期" width="180" />
      <el-table-column prop="entryPerson" label="录入人" width="120" />
      <el-table-column prop="remark" label="备注" show-overflow-tooltip />
      <el-table-column label="附件" width="100">
        <template #default="{ row }">
          <el-link type="primary" v-if="row.fileList && row.fileList.length" @click="handleViewFiles(row)">
            æŸ¥çœ‹({{ row.fileList.length }})
          </el-link>
          <span v-else>-</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150">
        <template #default="{ row }">
          <el-button type="text" size="small" @click="handleEditContract(row)">编辑</el-button>
          <el-button type="text" size="small" style="color: #f56c6c" @click="handleDeleteContract(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-tab-pane>
</el-tabs>
```
### 2. æ–°å¢ž/编辑合同记录弹窗
```html
<el-dialog :title="contractDialogTitle" v-model="contractDialogVisible" width="600px">
  <el-form :model="contractForm" :rules="contractRules" ref="contractFormRef" label-width="100px">
    <el-form-item label="总合同号" prop="masterContractNo">
      <el-input v-model="contractForm.masterContractNo" placeholder="请输入总合同号" />
    </el-form-item>
    <el-form-item label="录入日期" prop="entryDate">
      <el-date-picker
        v-model="contractForm.entryDate"
        type="datetime"
        placeholder="选择日期时间"
        value-format="yyyy-MM-dd HH:mm:ss"
        style="width: 100%"
      />
    </el-form-item>
    <el-form-item label="录入人" prop="entryPerson">
      <el-input v-model="contractForm.entryPerson" placeholder="请输入录入人" />
    </el-form-item>
    <el-form-item label="附件">
      <file-upload
        v-model="contractForm.storageBlobDTOs"
        :record-type="'supplier_contract_record'"
        :record-id="contractForm.id"
      />
    </el-form-item>
    <el-form-item label="备注">
      <el-input v-model="contractForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="contractDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="submitContractForm">确定</el-button>
  </template>
</el-dialog>
```
### 3. é‡‡è´­å°è´¦æ–°å¢ž/编辑页 - ä¾›åº”商选择后联动总合同号
```html
<el-form-item label="供应商" prop="supplierId">
  <el-select
    v-model="form.supplierId"
    placeholder="请选择供应商"
    filterable
    @change="handleSupplierChange"
  >
    <el-option
      v-for="item in supplierList"
      :key="item.id"
      :label="item.supplierName"
      :value="item.id"
    />
  </el-select>
</el-form-item>
<el-form-item label="总合同号" prop="masterContractNo">
  <el-select
    v-model="form.masterContractNo"
    placeholder="请先选择供应商"
    filterable
    clearable
    :disabled="!form.supplierId"
  >
    <el-option
      v-for="item in contractList"
      :key="item.id"
      :label="item.masterContractNo"
      :value="item.masterContractNo"
    />
  </el-select>
</el-form-item>
```
### 4. é‡‡è´­å°è´¦åˆ—表页 - æ“ä½œæ æ·»åŠ æŸ¥çœ‹æ€»åˆåŒé™„ä»¶æŒ‰é’®
```html
<el-table-column label="操作" width="200">
  <template #default="{ row }">
    <el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
    <el-button type="text" size="small" @click="handleDelete(row)">删除</el-button>
    <el-button
      type="text"
      size="small"
      v-if="row.masterContractNo"
      @click="handleViewContractFiles(row)"
    >查看附件</el-button>
  </template>
</el-table-column>
```
### 5. æ€»åˆåŒé™„件查看弹窗
```html
<el-dialog title="总合同附件" v-model="contractFileDialogVisible" width="600px">
  <el-table :data="contractFileList" v-if="contractFileList.length">
    <el-table-column prop="name" label="文件名称" />
    <el-table-column prop="byteSize" label="文件大小" width="120">
      <template #default="{ row }">
        {{ formatFileSize(row.byteSize) }}
      </template>
    </el-table-column>
    <el-table-column label="操作" width="150">
      <template #default="{ row }">
        <el-link type="primary" :href="row.previewURL" target="_blank">预览</el-link>
        <el-link type="primary" :href="row.downloadURL" style="margin-left: 10px">下载</el-link>
      </template>
    </el-table-column>
  </el-table>
  <el-empty v-else description="暂无附件" />
</el-dialog>
```
### 6. data æ•°æ®
```js
data() {
  return {
    // åˆåŒåˆ—表(供下拉选择)
    contractList: [],
    // åˆåŒè®°å½•列表(供应商详情页)
    contractRecordList: [],
    contractDialogVisible: false,
    contractDialogTitle: '',
    contractForm: {
      id: null,
      supplierId: null,
      masterContractNo: '',
      entryDate: '',
      entryPerson: '',
      entryPersonId: null,
      remark: '',
      storageBlobDTOs: []
    },
    contractRules: {
      masterContractNo: [{ required: true, message: '请输入总合同号', trigger: 'blur' }],
      entryDate: [{ required: true, message: '请选择录入日期', trigger: 'change' }]
    },
    // æ€»åˆåŒé™„件弹窗
    contractFileDialogVisible: false,
    contractFileList: []
  }
}
```
### 7. æ–¹æ³•
```js
methods: {
  // ä¾›åº”商选择变化时,加载该供应商的合同列表
  async handleSupplierChange(supplierId) {
    this.form.masterContractNo = ''  // æ¸…空总合同号
    this.contractList = []
    if (!supplierId) return
    const res = await this.$http.get(`/basic/supplier-contract/list/${supplierId}`)
    if (res.code === 200) {
      this.contractList = res.data
    }
  },
  // èŽ·å–ä¾›åº”å•†åˆåŒè®°å½•åˆ—è¡¨ï¼ˆä¾›åº”å•†è¯¦æƒ…é¡µï¼‰
  async getContractList(supplierId) {
    const res = await this.$http.get(`/basic/supplier-contract/list/${supplierId}`)
    if (res.code === 200) {
      this.contractRecordList = res.data
    }
  },
  // æ–°å¢žåˆåŒè®°å½•
  handleAddContract() {
    this.contractDialogTitle = '新增合同记录'
    this.contractForm = {
      id: null,
      supplierId: this.supplierId,
      masterContractNo: '',
      entryDate: '',
      entryPerson: '',
      entryPersonId: null,
      remark: '',
      storageBlobDTOs: []
    }
    this.contractDialogVisible = true
  },
  // ç¼–辑合同记录
  async handleEditContract(row) {
    this.contractDialogTitle = '编辑合同记录'
    const res = await this.$http.get(`/basic/supplier-contract/${row.id}`)
    if (res.code === 200) {
      this.contractForm = {
        ...res.data,
        storageBlobDTOs: res.data.fileList ? res.data.fileList.map(f => ({ id: f.id })) : []
      }
      this.contractDialogVisible = true
    }
  },
  // åˆ é™¤åˆåŒè®°å½•
  async handleDeleteContract(row) {
    await this.$confirm('确定删除该合同记录吗?', '提示', { type: 'warning' })
    const res = await this.$http.delete(`/basic/supplier-contract/${row.id}`)
    if (res.code === 200) {
      this.$message.success('删除成功')
      this.getContractList(this.supplierId)
    }
  },
  // æäº¤åˆåŒè®°å½•表单
  async submitContractForm() {
    await this.$refs.contractFormRef.validate()
    const res = await this.$http.post('/basic/supplier-contract', this.contractForm)
    if (res.code === 200) {
      this.$message.success('操作成功')
      this.contractDialogVisible = false
      this.getContractList(this.supplierId)
    }
  },
  // æŸ¥çœ‹æ€»åˆåŒé™„ä»¶
  async handleViewContractFiles(row) {
    if (!row.masterContractNo) {
      this.$message.warning('该记录没有总合同号')
      return
    }
    const res = await this.$http.get('/basic/supplier-contract/files/byContractNo', {
      params: { masterContractNo: row.masterContractNo }
    })
    if (res.code === 200) {
      this.contractFileList = res.data || []
      this.contractFileDialogVisible = true
    }
  },
  // æ ¼å¼åŒ–文件大小
  formatFileSize(bytes) {
    if (!bytes) return '0 B'
    const k = 1024
    const sizes = ['B', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  },
  // æŸ¥çœ‹é™„件列表
  handleViewFiles(row) {
    this.contractFileList = row.fileList || []
    this.contractFileDialogVisible = true
  },
  // ç¼–辑页打开时回显总合同号
  async handleEdit(row) {
    this.form = { ...row }
    this.dialogVisible = true
    // åŠ è½½ä¾›åº”å•†çš„åˆåŒåˆ—è¡¨
    if (row.supplierId) {
      const res = await this.$http.get(`/basic/supplier-contract/list/${row.supplierId}`)
      if (res.code === 200) {
        this.contractList = res.data
      }
    }
  }
}
```
### 8. é‡‡è´­å°è´¦åˆ—表页 - è¡¨æ ¼æ˜¾ç¤ºæ€»åˆåŒå·åˆ—
```html
<el-table-column prop="masterContractNo" label="总合同号" width="150" show-overflow-tooltip />
```
## åŽç«¯æ–‡ä»¶æ¸…单
| æ–‡ä»¶è·¯å¾„ | è¯´æ˜Ž |
|---------|------|
| `src/main/java/com/ruoyi/basic/pojo/SupplierContractRecord.java` | å®žä½“ç±» |
| `src/main/java/com/ruoyi/basic/dto/SupplierContractRecordDto.java` | DTO |
| `src/main/java/com/ruoyi/basic/mapper/SupplierContractRecordMapper.java` | Mapper接口 |
| `src/main/resources/mapper/basic/SupplierContractRecordMapper.xml` | Mapper XML |
| `src/main/java/com/ruoyi/basic/service/SupplierContractRecordService.java` | Service接口 |
| `src/main/java/com/ruoyi/basic/service/impl/SupplierContractRecordServiceImpl.java` | Service实现 |
| `src/main/java/com/ruoyi/basic/controller/SupplierContractRecordController.java` | Controller |
| `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java` | æ·»åŠ æžšä¸¾å€¼ `SUPPLIER_CONTRACT_RECORD` |
## æ³¨æ„äº‹é¡¹
- æ€»åˆåŒå·æ¥æºäºŽä¾›åº”商管理的合同记录子表
- é€‰æ‹©é¡ºåºï¼šå…ˆé€‰ä¾›åº”商 -> è‡ªåŠ¨åŠ è½½è¯¥ä¾›åº”å•†çš„åˆåŒåˆ—è¡¨ -> å†é€‰æ€»åˆåŒå·
- é‡‡è´­å°è´¦è¡¨å·²æœ‰ `master_contract_no` å­—段,无需修改数据库
- æŸ¥çœ‹é™„件按钮仅在总合同号存在时显示
- é™„件列表通过总合同号查询,支持预览和下载
doc/sales_master_contract_linkage.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,216 @@
# é”€å”®å°è´¦/销售报价-总合同号联动选择及附件查看
## æ¶‰åŠé¡µé¢
- é”€å”®å°è´¦åˆ—表页/新增编辑页
- é”€å”®æŠ¥ä»·åˆ—表页/新增编辑页
## æ–°å¢žAPI
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /basic/customer-contract/list/{customerId} | æŸ¥è¯¢å®¢æˆ·åˆåŒè®°å½•列表(供下拉选择) |
| GET | /basic/customer-contract/files/byContractNo?masterContractNo=xxx | æ ¹æ®æ€»åˆåŒå·æŸ¥è¯¢é™„ä»¶ |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é”€å”®å°è´¦æ–°å¢ž/编辑页 - å®¢æˆ·é€‰æ‹©åŽè”动总合同号
```html
<el-form-item label="客户名称" prop="customerId">
  <el-select
    v-model="form.customerId"
    placeholder="请选择客户"
    filterable
    @change="handleCustomerChange"
  >
    <el-option
      v-for="item in customerList"
      :key="item.id"
      :label="item.customerName"
      :value="item.id"
    />
  </el-select>
</el-form-item>
<el-form-item label="总合同号" prop="masterContractNo">
  <el-select
    v-model="form.masterContractNo"
    placeholder="请先选择客户"
    filterable
    clearable
    :disabled="!form.customerId"
  >
    <el-option
      v-for="item in contractList"
      :key="item.id"
      :label="item.masterContractNo"
      :value="item.masterContractNo"
    />
  </el-select>
</el-form-item>
```
### 2. é”€å”®æŠ¥ä»·æ–°å¢ž/编辑页 - åŒæ ·æ·»åŠ å®¢æˆ·å’Œæ€»åˆåŒå·è”åŠ¨
```html
<el-form-item label="客户名称" prop="customerId">
  <el-select
    v-model="form.customerId"
    placeholder="请选择客户"
    filterable
    @change="handleCustomerChange"
  >
    <el-option
      v-for="item in customerList"
      :key="item.id"
      :label="item.customerName"
      :value="item.id"
    />
  </el-select>
</el-form-item>
<el-form-item label="总合同号" prop="masterContractNo">
  <el-select
    v-model="form.masterContractNo"
    placeholder="请先选择客户"
    filterable
    clearable
    :disabled="!form.customerId"
  >
    <el-option
      v-for="item in contractList"
      :key="item.id"
      :label="item.masterContractNo"
      :value="item.masterContractNo"
    />
  </el-select>
</el-form-item>
```
### 3. é”€å”®å°è´¦åˆ—表页 - æ“ä½œæ æ·»åŠ æŸ¥çœ‹æ€»åˆåŒé™„ä»¶æŒ‰é’®
```html
<el-table-column label="操作" width="200">
  <template #default="{ row }">
    <el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
    <el-button type="text" size="small" @click="handleDelete(row)">删除</el-button>
    <el-button
      type="text"
      size="small"
      v-if="row.masterContractNo"
      @click="handleViewContractFiles(row)"
    >查看附件</el-button>
  </template>
</el-table-column>
```
### 4. æ€»åˆåŒé™„件查看弹窗
```html
<el-dialog title="总合同附件" v-model="contractFileDialogVisible" width="600px">
  <el-table :data="contractFileList" v-if="contractFileList.length">
    <el-table-column prop="name" label="文件名称" />
    <el-table-column prop="byteSize" label="文件大小" width="120">
      <template #default="{ row }">
        {{ formatFileSize(row.byteSize) }}
      </template>
    </el-table-column>
    <el-table-column label="操作" width="150">
      <template #default="{ row }">
        <el-link type="primary" :href="row.previewURL" target="_blank">预览</el-link>
        <el-link type="primary" :href="row.downloadURL" style="margin-left: 10px">下载</el-link>
      </template>
    </el-table-column>
  </el-table>
  <el-empty v-else description="暂无附件" />
</el-dialog>
```
### 5. data æ•°æ®
```js
data() {
  return {
    // åˆåŒåˆ—表(供下拉选择)
    contractList: [],
    // æ€»åˆåŒé™„件弹窗
    contractFileDialogVisible: false,
    contractFileList: []
  }
}
```
### 6. æ–¹æ³•
```js
methods: {
  // å®¢æˆ·é€‰æ‹©å˜åŒ–时,加载该客户的合同列表
  async handleCustomerChange(customerId) {
    this.form.masterContractNo = ''  // æ¸…空总合同号
    this.contractList = []
    if (!customerId) return
    const res = await this.$http.get(`/basic/customer-contract/list/${customerId}`)
    if (res.code === 200) {
      this.contractList = res.data
    }
  },
  // æŸ¥çœ‹æ€»åˆåŒé™„ä»¶
  async handleViewContractFiles(row) {
    if (!row.masterContractNo) {
      this.$message.warning('该记录没有总合同号')
      return
    }
    const res = await this.$http.get('/basic/customer-contract/files/byContractNo', {
      params: { masterContractNo: row.masterContractNo }
    })
    if (res.code === 200) {
      this.contractFileList = res.data || []
      this.contractFileDialogVisible = true
    }
  },
  // æ ¼å¼åŒ–文件大小
  formatFileSize(bytes) {
    if (!bytes) return '0 B'
    const k = 1024
    const sizes = ['B', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  }
}
```
### 7. é”€å”®å°è´¦/报价列表页 - è¡¨æ ¼æ˜¾ç¤ºæ€»åˆåŒå·åˆ—
```html
<el-table-column prop="masterContractNo" label="总合同号" width="150" show-overflow-tooltip />
```
### 8. ç¼–辑页打开时回显总合同号
```js
// æ‰“开编辑弹窗时,加载客户的合同列表并回显
async handleEdit(row) {
  this.form = { ...row }
  this.dialogVisible = true
  // åŠ è½½å®¢æˆ·çš„åˆåŒåˆ—è¡¨
  if (row.customerId) {
    const res = await this.$http.get(`/basic/customer-contract/list/${row.customerId}`)
    if (res.code === 200) {
      this.contractList = res.data
    }
  }
}
```
## æ³¨æ„äº‹é¡¹
- æ€»åˆåŒå·æ¥æºäºŽå®¢æˆ·æ¡£æ¡ˆç§æµ·çš„合同记录子表
- é€‰æ‹©é¡ºåºï¼šå…ˆé€‰å®¢æˆ· -> è‡ªåŠ¨åŠ è½½è¯¥å®¢æˆ·çš„åˆåŒåˆ—è¡¨ -> å†é€‰æ€»åˆåŒå·
- é”€å”®å°è´¦è¡¨å·²æœ‰ `master_contract_no` å­—段,销售报价表需新增该字段
- æŸ¥çœ‹é™„件按钮仅在总合同号存在时显示
- é™„件列表通过总合同号查询,支持预览和下载
src/api/basicData/customerContract.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import request from '@/utils/request'
// æŸ¥è¯¢åˆåŒè®°å½•列表
export function getCustomerContractList(customerId) {
    return request({
        url: `/basic/customer-contract/list/${customerId}`,
        method: 'get'
    })
}
// èŽ·å–åˆåŒè®°å½•è¯¦æƒ…
export function getCustomerContractById(id) {
    return request({
        url: `/basic/customer-contract/${id}`,
        method: 'get'
    })
}
// æ–°å¢žåˆåŒè®°å½•
export function addCustomerContract(data) {
    return request({
        url: '/basic/customer-contract',
        method: 'post',
        data: data
    })
}
// ä¿®æ”¹åˆåŒè®°å½•
export function updateCustomerContract(data) {
    return request({
        url: '/basic/customer-contract',
        method: 'put',
        data: data
    })
}
// åˆ é™¤åˆåŒè®°å½•
export function delCustomerContract(id) {
    return request({
        url: `/basic/customer-contract/${id}`,
        method: 'delete'
    })
}
// æ ¹æ®æ€»åˆåŒå·æŸ¥è¯¢é™„ä»¶
export function getContractFilesByNo(masterContractNo) {
    return request({
        url: '/basic/customer-contract/files/byContractNo',
        method: 'get',
        params: { masterContractNo }
    })
}
src/api/basicData/supplierContract.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import request from '@/utils/request'
// æŸ¥è¯¢ä¾›åº”商合同记录列表
export function getSupplierContractList(supplierId) {
    return request({
        url: `/basic/supplier-contract/list/${supplierId}`,
        method: 'get'
    })
}
// èŽ·å–ä¾›åº”å•†åˆåŒè®°å½•è¯¦æƒ…
export function getSupplierContractById(id) {
    return request({
        url: `/basic/supplier-contract/${id}`,
        method: 'get'
    })
}
// æ–°å¢žä¾›åº”商合同记录
export function addSupplierContract(data) {
    return request({
        url: '/basic/supplier-contract',
        method: 'post',
        data: data
    })
}
// ä¿®æ”¹ä¾›åº”商合同记录
export function updateSupplierContract(data) {
    return request({
        url: '/basic/supplier-contract',
        method: 'put',
        data: data
    })
}
// åˆ é™¤ä¾›åº”商合同记录
export function delSupplierContract(id) {
    return request({
        url: `/basic/supplier-contract/${id}`,
        method: 'delete'
    })
}
// æ ¹æ®æ€»åˆåŒå·æŸ¥è¯¢é™„ä»¶
export function getSupplierContractFilesByNo(masterContractNo) {
    return request({
        url: '/basic/supplier-contract/files/byContractNo',
        method: 'get',
        params: { masterContractNo }
    })
}
src/views/basicData/customerFile/index.vue
@@ -357,70 +357,129 @@
          </el-row>
        </div>
      </div>
      <!-- æ´½è°ˆè¿›åº¦è®°å½• -->
      <div class="detail-section">
        <div class="section-header">
          <h3 class="section-title">洽谈进度记录</h3>
          <el-button type="primary"
                     size="small"
                     @click="openNegotiationDialog(detailForm)">
            æ·»åŠ è¿›åº¦
          </el-button>
        </div>
        <el-table :data="negotiationRecords"
                  border
                  style="width: 100%">
          <el-table-column prop="followUpTime"
                           label="跟进时间"
                           width="160" />
          <el-table-column prop="followUpMethod"
                           label="跟进方式"
                           width="100" />
          <el-table-column prop="followUpLevel"
                           label="跟进程度" />
          <el-table-column prop="followerUserName"
                           label="跟进人"
                           width="100" />
          <el-table-column prop="content"
                           label="内容"
                           show-overflow-tooltip />
          <el-table-column label="附件"
                           width="100"
                           align="center">
            <template #default="{ row }">
              <el-button type="info"
                         link
                         @click="openAttachmentDialog(row)">
                <el-icon>
                  <Paperclip />
                </el-icon>
                é™„ä»¶
                <!-- {{ row.fileList && row.fileList.length > 0 ? row.fileList.length : '上传' }} -->
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="操作"
                           width="150"
                           align="center">
            <template #default="{ row, $index }">
              <el-button type="primary"
                         link
                         @click="editNegotiationRecord(row, $index)">
                ä¿®æ”¹
              </el-button>
              <el-button type="danger"
                         link
                         @click="deleteNegotiationRecord(row, $index)" v-hasPermi="['basicData:customerFile:remove']">
                åˆ é™¤
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <div v-if="negotiationRecords.length === 0"
             class="no-records">
          æš‚无洽谈进度记录
        </div>
      </div>
      <!-- Tab切换区域 -->
      <el-tabs v-model="detailActiveTab" class="detail-tabs">
        <!-- æ´½è°ˆè¿›åº¦è®°å½• -->
        <el-tab-pane label="洽谈进度记录" name="negotiation">
          <div class="section-header">
            <el-button type="primary"
                       size="small"
                       @click="openNegotiationDialog(detailForm)">
              æ·»åŠ è¿›åº¦
            </el-button>
          </div>
          <el-table :data="negotiationRecords"
                    border
                    style="width: 100%">
            <el-table-column prop="followUpTime"
                             label="跟进时间"
                             width="160" />
            <el-table-column prop="followUpMethod"
                             label="跟进方式"
                             width="100" />
            <el-table-column prop="followUpLevel"
                             label="跟进程度" />
            <el-table-column prop="followerUserName"
                             label="跟进人"
                             width="100" />
            <el-table-column prop="content"
                             label="内容"
                             show-overflow-tooltip />
            <el-table-column label="附件"
                             width="100"
                             align="center">
              <template #default="{ row }">
                <el-button type="info"
                           link
                           @click="openAttachmentDialog(row)">
                  <el-icon>
                    <Paperclip />
                  </el-icon>
                  é™„ä»¶
                </el-button>
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             width="150"
                             align="center">
              <template #default="{ row, $index }">
                <el-button type="primary"
                           link
                           @click="editNegotiationRecord(row, $index)">
                  ä¿®æ”¹
                </el-button>
                <el-button type="danger"
                           link
                           @click="deleteNegotiationRecord(row, $index)" v-hasPermi="['basicData:customerFile:remove']">
                  åˆ é™¤
                </el-button>
              </template>
            </el-table-column>
          </el-table>
          <div v-if="negotiationRecords.length === 0"
               class="no-records">
            æš‚无洽谈进度记录
          </div>
        </el-tab-pane>
        <!-- åˆåŒè®°å½• -->
        <el-tab-pane label="合同记录" name="contract">
          <div class="section-header">
            <el-button type="primary"
                       size="small"
                       @click="handleAddContract">
              æ–°å¢žåˆåŒè®°å½•
            </el-button>
          </div>
          <el-table :data="contractList"
                    border
                    style="width: 100%">
            <el-table-column prop="masterContractNo"
                             label="总合同号"
                             show-overflow-tooltip />
            <el-table-column prop="entryDate"
                             label="录入日期"
                             width="180" />
            <el-table-column prop="entryPerson"
                             label="录入人"
                             width="120" />
            <el-table-column prop="remark"
                             label="备注"
                             show-overflow-tooltip />
            <el-table-column label="附件"
                             width="100"
                             align="center">
              <template #default="{ row }">
                <el-link type="primary"
                         v-if="row.fileList && row.fileList.length"
                         @click="handleViewContractFiles(row)">
                  æŸ¥çœ‹({{ row.fileList.length }})
                </el-link>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             width="150"
                             align="center">
              <template #default="{ row }">
                <el-button type="primary"
                           link
                           @click="handleEditContract(row)">
                  ç¼–辑
                </el-button>
                <el-button type="danger"
                           link
                           @click="handleDeleteContract(row)" v-hasPermi="['basicData:customerFile:remove']">
                  åˆ é™¤
                </el-button>
              </template>
            </el-table-column>
          </el-table>
          <div v-if="contractList.length === 0"
               class="no-records">
            æš‚无合同记录
          </div>
        </el-tab-pane>
      </el-tabs>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDetailDialog">关闭</el-button>
@@ -503,6 +562,79 @@
        </div>
      </template>
    </el-dialog>
    <!-- åˆåŒè®°å½•新增/编辑弹窗 -->
    <el-dialog :title="contractDialogTitle"
               v-model="contractDialogVisible"
               width="600px"
               @close="closeContractDialog">
      <el-form :model="contractForm"
               :rules="contractRules"
               ref="contractFormRef"
               label-width="100px">
        <el-form-item label="总合同号" prop="masterContractNo">
          <el-input v-model="contractForm.masterContractNo"
                    placeholder="请输入总合同号" />
        </el-form-item>
        <el-form-item label="录入日期" prop="entryDate">
          <el-date-picker v-model="contractForm.entryDate"
                          type="datetime"
                          placeholder="选择日期时间"
                          value-format="YYYY-MM-DD HH:mm:ss"
                          style="width: 100%" />
        </el-form-item>
        <el-form-item label="录入人" prop="entryPerson">
          <el-input v-model="contractForm.entryPerson"
                    placeholder="请输入录入人" />
        </el-form-item>
        <el-form-item label="附件">
          <FileUpload v-model:file-list="contractForm.storageBlobDTOs" />
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="contractForm.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="contractDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitContractForm">确定</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- åˆåŒé™„件查看弹窗 -->
    <el-dialog title="附件列表"
               v-model="contractFileDialogVisible"
               width="600px">
      <el-table :data="currentContractFiles"
                border
                style="width: 100%">
        <el-table-column prop="name"
                         label="文件名"
                         show-overflow-tooltip />
        <el-table-column label="操作"
                         width="120"
                         align="center">
          <template #default="{ row }">
            <el-button type="primary"
                       link
                       @click="downloadContractFile(row)">
              ä¸‹è½½
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div v-if="!currentContractFiles || currentContractFiles.length === 0"
           class="no-records">
        æš‚无附件
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="contractFileDialogVisible = false">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
@@ -516,11 +648,19 @@
    addReturnVisit,
    getReturnVisit,
  } from "@/api/basicData/customerFile.js";
  import {
    getCustomerContractList,
    getCustomerContractById,
    addCustomerContract,
    updateCustomerContract,
    delCustomerContract,
  } from "@/api/basicData/customerContract.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";
  import { getToken } from "@/utils/auth.js";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
  const { proxy } = getCurrentInstance();
  const userStore = useUserStore();
@@ -570,6 +710,7 @@
  // è¯¦æƒ…相关
  const detailDialogVisible = ref(false);
  const detailActiveTab = ref("negotiation");
  const detailForm = reactive({
    customerName: "",
    customerType: "",
@@ -579,6 +720,32 @@
    maintenanceTime: "",
  });
  const negotiationRecords = ref([]);
  // åˆåŒè®°å½•相关
  const contractList = ref([]);
  const contractDialogVisible = ref(false);
  const contractDialogTitle = ref("");
  const contractFormRef = ref();
  const contractForm = reactive({
    id: null,
    customerId: null,
    masterContractNo: "",
    entryDate: "",
    entryPerson: "",
    entryPersonId: null,
    remark: "",
    storageBlobDTOs: [],
  });
  const contractRules = {
    masterContractNo: [
      { required: true, message: "请输入总合同号", trigger: "blur" },
    ],
    entryDate: [
      { required: true, message: "请选择录入日期", trigger: "change" },
    ],
  };
  const contractFileDialogVisible = ref(false);
  const currentContractFiles = ref([]);
  // é™„件相关
  const attachmentDialogVisible = ref(false);
@@ -1087,20 +1254,132 @@
  // æ‰“开详情弹窗
  const openDetailDialog = row => {
    detailActiveTab.value = "negotiation";
    getCustomer(row.id).then(res => {
      // å¡«å……客户基本信息
      Object.assign(detailForm, res.data);
      detailForm.id = row.id; // ä¿å­˜å®¢æˆ·ID
      // èŽ·å–æ´½è°ˆè¿›åº¦è®°å½•
      negotiationRecords.value = res.data.followUpList || [];
      detailDialogVisible.value = true;
    });
    // èŽ·å–åˆåŒè®°å½•åˆ—è¡¨
    getContractList(row.id);
  };
  // èŽ·å–åˆåŒè®°å½•åˆ—è¡¨
  const getContractList = customerId => {
    getCustomerContractList(customerId).then(res => {
      if (res.code === 200) {
        contractList.value = res.data || [];
      }
    });
  };
  // æ–°å¢žåˆåŒè®°å½•
  const handleAddContract = () => {
    contractDialogTitle.value = "新增合同记录";
    contractForm.id = null;
    contractForm.customerId = detailForm.id;
    contractForm.masterContractNo = "";
    contractForm.entryDate = "";
    contractForm.entryPerson = userStore.nickName;
    contractForm.entryPersonId = userStore.id;
    contractForm.remark = "";
    contractForm.storageBlobDTOs = [];
    contractDialogVisible.value = true;
  };
  // ç¼–辑合同记录
  const handleEditContract = row => {
    contractDialogTitle.value = "编辑合同记录";
    getCustomerContractById(row.id).then(res => {
      if (res.code === 200) {
        contractForm.id = res.data.id;
        contractForm.customerId = res.data.customerId;
        contractForm.masterContractNo = res.data.masterContractNo;
        contractForm.entryDate = res.data.entryDate;
        contractForm.entryPerson = res.data.entryPerson;
        contractForm.entryPersonId = res.data.entryPersonId;
        contractForm.remark = res.data.remark;
        contractForm.storageBlobDTOs = res.data.fileList || [];
        contractDialogVisible.value = true;
      }
    });
  };
  // åˆ é™¤åˆåŒè®°å½•
  const handleDeleteContract = row => {
    ElMessageBox.confirm("确定删除该合同记录吗?", "提示", {
      type: "warning",
    }).then(() => {
      delCustomerContract(row.id).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess("删除成功");
          getContractList(detailForm.id);
        }
      });
    }).catch(() => {
      proxy.$modal.msg("已取消");
    });
  };
  // æäº¤åˆåŒè®°å½•表单
  const submitContractForm = () => {
    proxy.$refs.contractFormRef.validate(valid => {
      if (valid) {
        const submitData = {
          id: contractForm.id,
          customerId: contractForm.customerId,
          masterContractNo: contractForm.masterContractNo,
          entryDate: contractForm.entryDate,
          entryPerson: contractForm.entryPerson,
          entryPersonId: contractForm.entryPersonId,
          remark: contractForm.remark,
          storageBlobDTOs: contractForm.storageBlobDTOs.map(f => ({ id: f.id })),
        };
        const request = contractForm.id ? updateCustomerContract : addCustomerContract;
        request(submitData).then(res => {
          if (res.code === 200) {
            proxy.$modal.msgSuccess("操作成功");
            contractDialogVisible.value = false;
            getContractList(detailForm.id);
          }
        });
      }
    });
  };
  // å…³é—­åˆåŒè®°å½•弹窗
  const closeContractDialog = () => {
    proxy.resetForm("contractFormRef");
    contractDialogVisible.value = false;
  };
  // æŸ¥çœ‹åˆåŒé™„ä»¶
  const handleViewContractFiles = row => {
    currentContractFiles.value = row.fileList || [];
    contractFileDialogVisible.value = true;
  };
  // ä¸‹è½½åˆåŒé™„ä»¶
  const downloadContractFile = row => {
    if (row.downloadURL || row.url) {
      proxy.$download.byUrl(row.downloadURL || row.url, row.name || row.originalFilename);
    } else {
      proxy.$modal.msgError("下载链接不存在");
    }
  };
  // å…³é—­è¯¦æƒ…弹窗
  const closeDetailDialog = () => {
    detailDialogVisible.value = false;
    detailActiveTab.value = "negotiation";
    contractList.value = [];
  };
  // ä¿®æ”¹æ´½è°ˆè®°å½•
@@ -1356,6 +1635,10 @@
    font-size: 14px;
  }
  .detail-tabs {
    margin-top: 20px;
  }
  .attachment-section {
    .upload-area {
      margin-bottom: 20px;
src/views/basicData/supplierManage/components/HomeTab.vue
@@ -44,155 +44,259 @@
        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="supplierName">
              <el-input
                  v-model="form.supplierName"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item
                label="纳税人识别号:"
                prop="taxpayerIdentificationNum"
            >
              <el-input
                  v-model="form.taxpayerIdentificationNum"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="公司地址:" prop="companyAddress">
              <el-input
                  v-model="form.companyAddress"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="公司电话:" prop="companyPhone">
              <el-input
                  v-model="form.companyPhone"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="开户行:" prop="bankAccountName">
              <el-input
                  v-model="form.bankAccountName"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="账号:" prop="bankAccountNum">
              <el-input
                  v-model="form.bankAccountNum"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="联系人:" prop="contactUserName">
              <el-input
                  v-model="form.contactUserName"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="联系电话:" prop="contactUserPhone">
              <el-input
                  v-model="form.contactUserPhone"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="维护人:" prop="maintainUserId">
              <el-select
                  v-model="form.maintainUserId"
                  placeholder="请选择"
                  clearable
                  disabled
              >
                <el-option
                    v-for="item in userList"
                    :key="item.nickName"
                    :label="item.nickName"
                    :value="item.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="维护时间:" prop="maintainTime">
              <el-date-picker
                  style="width: 100%"
                  v-model="form.maintainTime"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  type="date"
                  placeholder="请选择"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="供应商类型:" prop="supplierType">
              <el-select v-model="form.supplierType" placeholder="请选择" clearable>
                <el-option label="甲" value="甲" />
                <el-option label="乙" value="乙" />
                <el-option label="丙" value="丙" />
                <el-option label="丁" value="丁" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否白名单:" prop="isWhite">
              <el-select v-model="form.isWhite" placeholder="请选择" clearable>
                <el-option label="是" :value="0" />
                <el-option label="否" :value="1" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <el-tabs v-model="supplierActiveTab">
        <!-- åŸºæœ¬ä¿¡æ¯ Tab -->
        <el-tab-pane label="基本信息" name="basic">
          <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="supplierName">
                  <el-input
                      v-model="form.supplierName"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item
                    label="纳税人识别号:"
                    prop="taxpayerIdentificationNum"
                >
                  <el-input
                      v-model="form.taxpayerIdentificationNum"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="30">
              <el-col :span="12">
                <el-form-item label="公司地址:" prop="companyAddress">
                  <el-input
                      v-model="form.companyAddress"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="公司电话:" prop="companyPhone">
                  <el-input
                      v-model="form.companyPhone"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="30">
              <el-col :span="12">
                <el-form-item label="开户行:" prop="bankAccountName">
                  <el-input
                      v-model="form.bankAccountName"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="账号:" prop="bankAccountNum">
                  <el-input
                      v-model="form.bankAccountNum"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="30">
              <el-col :span="12">
                <el-form-item label="联系人:" prop="contactUserName">
                  <el-input
                      v-model="form.contactUserName"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="联系电话:" prop="contactUserPhone">
                  <el-input
                      v-model="form.contactUserPhone"
                      placeholder="请输入"
                      clearable
                  />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="30">
              <el-col :span="12">
                <el-form-item label="维护人:" prop="maintainUserId">
                  <el-select
                      v-model="form.maintainUserId"
                      placeholder="请选择"
                      clearable
                      disabled
                  >
                    <el-option
                        v-for="item in userList"
                        :key="item.nickName"
                        :label="item.nickName"
                        :value="item.userId"
                    />
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="维护时间:" prop="maintainTime">
                  <el-date-picker
                      style="width: 100%"
                      v-model="form.maintainTime"
                      value-format="YYYY-MM-DD"
                      format="YYYY-MM-DD"
                      type="date"
                      placeholder="请选择"
                      clearable
                  />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="30">
              <el-col :span="12">
                <el-form-item label="供应商类型:" prop="supplierType">
                  <el-select v-model="form.supplierType" placeholder="请选择" clearable>
                    <el-option label="甲" value="甲" />
                    <el-option label="乙" value="乙" />
                    <el-option label="丙" value="丙" />
                    <el-option label="丁" value="丁" />
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="是否白名单:" prop="isWhite">
                  <el-select v-model="form.isWhite" placeholder="请选择" clearable>
                    <el-option label="是" :value="0" />
                    <el-option label="否" :value="1" />
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
          </el-form>
        </el-tab-pane>
        <!-- åˆåŒè®°å½• Tab -->
        <el-tab-pane label="合同记录" name="contract" :disabled="operationType === 'add' || !currentSupplierId">
          <div style="margin-bottom: 15px;">
            <el-button type="primary" size="small" @click="handleAddContract">新增合同记录</el-button>
          </div>
          <el-table :data="contractRecordList" border style="width: 100%">
            <el-table-column prop="masterContractNo" label="总合同号" show-overflow-tooltip />
            <el-table-column prop="entryDate" label="录入日期" width="180" />
            <el-table-column prop="entryPerson" label="录入人" width="120" />
            <el-table-column prop="remark" label="备注" show-overflow-tooltip />
            <el-table-column label="附件" width="100">
              <template #default="{ row }">
                <el-link type="primary" v-if="row.fileList && row.fileList.length" @click="handleViewContractFiles(row)">
                  æŸ¥çœ‹({{ row.fileList.length }})
                </el-link>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="150">
              <template #default="{ row }">
                <el-button link type="primary" @click="handleEditContract(row)">编辑</el-button>
                <el-button link type="danger" @click="handleDeleteContract(row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-tab-pane>
      </el-tabs>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- åˆåŒè®°å½•弹窗 -->
    <el-dialog
        v-model="contractDialogVisible"
        :title="contractDialogTitle"
        width="600px"
        :close-on-click-modal="false"
    >
      <el-form
          :model="contractForm"
          :rules="contractRules"
          ref="contractFormRef"
          label-width="100px"
      >
        <el-form-item label="总合同号" prop="masterContractNo">
          <el-input v-model="contractForm.masterContractNo" placeholder="请输入总合同号" clearable />
        </el-form-item>
        <el-form-item label="录入日期" prop="entryDate">
          <el-date-picker
              v-model="contractForm.entryDate"
              type="datetime"
              placeholder="选择日期时间"
              value-format="YYYY-MM-DD HH:mm:ss"
              style="width: 100%"
              clearable
          />
        </el-form-item>
        <el-form-item label="录入人" prop="entryPerson">
          <el-input v-model="contractForm.entryPerson" placeholder="请输入录入人" clearable />
        </el-form-item>
        <el-form-item label="附件">
          <FileUpload
              v-model="contractForm.storageBlobDTOs"
              :record-type="'supplier_contract_record'"
              :record-id="contractForm.id"
          />
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="contractForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitContractForm">确认</el-button>
          <el-button @click="contractDialogVisible = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- åˆåŒé™„件查看弹窗 -->
    <el-dialog
        v-model="contractFileDialogVisible"
        title="合同附件"
        width="700px"
        :close-on-click-modal="false"
    >
      <el-table :data="contractFileList" border v-if="contractFileList.length">
        <el-table-column prop="name" label="文件名称" show-overflow-tooltip />
        <el-table-column prop="byteSize" label="文件大小" width="120">
          <template #default="{ row }">
            {{ formatFileSize(row.byteSize) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150">
          <template #default="{ row }">
            <el-link type="primary" :href="row.previewURL" target="_blank">预览</el-link>
            <el-link type="primary" :href="row.downloadURL" style="margin-left: 10px">下载</el-link>
          </template>
        </el-table-column>
      </el-table>
      <el-empty v-else description="暂无附件" />
    </el-dialog>
    <!-- ä¾›åº”商导入对话框 -->
@@ -256,8 +360,16 @@
  listSupplier,
  updateSupplier,
} from "@/api/basicData/supplierManageFile.js";
import {
  getSupplierContractList,
  getSupplierContractById,
  addSupplierContract,
  updateSupplierContract,
  delSupplierContract,
} from "@/api/basicData/supplierContract.js";
import useUserStore from "@/store/modules/user";
import { getToken } from "@/utils/auth.js";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
);
@@ -355,6 +467,34 @@
  size: 100,
  total: 0,
});
// ä¾›åº”商详情Tab相关
const supplierActiveTab = ref('basic');
const currentSupplierId = ref(null);
const contractRecordList = ref([]);
// åˆåŒè®°å½•弹窗相关
const contractDialogVisible = ref(false);
const contractDialogTitle = ref('');
const contractFormRef = ref();
const contractForm = reactive({
  id: null,
  supplierId: null,
  masterContractNo: '',
  entryDate: '',
  entryPerson: '',
  entryPersonId: null,
  remark: '',
  storageBlobDTOs: []
});
const contractRules = {
  masterContractNo: [{ required: true, message: '请输入总合同号', trigger: 'blur' }],
  entryDate: [{ required: true, message: '请选择录入日期', trigger: 'change' }]
};
// åˆåŒé™„件弹窗相关
const contractFileDialogVisible = ref(false);
const contractFileList = ref([]);
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
@@ -472,21 +612,122 @@
  selectedRows.value = selection;
};
// æ‰“开弹框
const openForm = (type, row) => {
const openForm = async (type, row) => {
  operationType.value = type;
  form.value = {};
  form.value.maintainUserId = userStore.id;
  form.value.maintainTime = getCurrentDate();
  supplierActiveTab.value = 'basic';
  currentSupplierId.value = null;
  contractRecordList.value = [];
  userListNoPage().then((res) => {
    userList.value = res.data;
  });
  if (type === "edit") {
    currentSupplierId.value = row.id;
    getSupplier(row.id).then((res) => {
      form.value = { ...res.data };
    });
    // åŠ è½½åˆåŒè®°å½•åˆ—è¡¨
    loadContractList(row.id);
  }
  dialogFormVisible.value = true;
};
// åŠ è½½åˆåŒè®°å½•åˆ—è¡¨
const loadContractList = async (supplierId) => {
  try {
    const res = await getSupplierContractList(supplierId);
    if (res.code === 200) {
      contractRecordList.value = res.data || [];
    }
  } catch (error) {
    console.error('获取合同记录列表失败:', error);
  }
};
// æ–°å¢žåˆåŒè®°å½•
const handleAddContract = () => {
  contractDialogTitle.value = '新增合同记录';
  Object.assign(contractForm, {
    id: null,
    supplierId: currentSupplierId.value,
    masterContractNo: '',
    entryDate: '',
    entryPerson: userStore.nickName,
    entryPersonId: userStore.id,
    remark: '',
    storageBlobDTOs: []
  });
  contractDialogVisible.value = true;
};
// ç¼–辑合同记录
const handleEditContract = async (row) => {
  contractDialogTitle.value = '编辑合同记录';
  try {
    const res = await getSupplierContractById(row.id);
    if (res.code === 200) {
      Object.assign(contractForm, {
        ...res.data,
        storageBlobDTOs: res.data.fileList ? res.data.fileList.map(f => ({ id: f.id })) : []
      });
      contractDialogVisible.value = true;
    }
  } catch (error) {
    console.error('获取合同记录详情失败:', error);
  }
};
// åˆ é™¤åˆåŒè®°å½•
const handleDeleteContract = async (row) => {
  try {
    await ElMessageBox.confirm('确定删除该合同记录吗?', '提示', { type: 'warning' });
    const res = await delSupplierContract(row.id);
    if (res.code === 200) {
      proxy.$modal.msgSuccess('删除成功');
      loadContractList(currentSupplierId.value);
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除合同记录失败:', error);
    }
  }
};
// æäº¤åˆåŒè®°å½•表单
const submitContractForm = async () => {
  try {
    await contractFormRef.value.validate();
    const api = contractForm.id ? updateSupplierContract : addSupplierContract;
    const res = await api(contractForm);
    if (res.code === 200) {
      proxy.$modal.msgSuccess('操作成功');
      contractDialogVisible.value = false;
      loadContractList(currentSupplierId.value);
    }
  } catch (error) {
    console.error('提交合同记录失败:', error);
  }
};
// æŸ¥çœ‹åˆåŒé™„ä»¶
const handleViewContractFiles = (row) => {
  contractFileList.value = row.fileList || [];
  contractFileDialogVisible.value = true;
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (bytes) => {
  if (!bytes) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// æäº¤è¡¨å•
const submitForm = () => {
  proxy.$refs["formRef"].validate((valid) => {
src/views/procurementManagement/procurementLedger/index.vue
@@ -278,7 +278,7 @@
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="120"
                         width="200"
                         align="center">
          <template #default="scope">
            <el-button link
@@ -288,6 +288,11 @@
            <el-button link
                       type="primary"
                       @click="openFileDialog(scope.row)">附件</el-button>
            <el-button link
                       type="primary"
                       v-if="scope.row.masterContractNo"
                       @click="openContractFileDialog(scope.row)">总合同附件
            </el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -310,15 +315,6 @@
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="总合同号:">
              <el-input v-model="form.masterContractNo"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="采购合同号:"
@@ -351,7 +347,8 @@
              <el-select v-model="form.supplierId"
                         placeholder="请选择"
                         filterable
                         clearable>
                         clearable
                         @change="handleSupplierChange">
                <el-option v-for="item in supplierList"
                           :key="item.id"
                           :label="item.supplierName"
@@ -360,6 +357,22 @@
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="总合同号:">
              <el-select v-model="form.masterContractNo"
                         placeholder="请先选择供应商"
                         filterable
                         clearable
                         :disabled="!form.supplierId">
                <el-option v-for="item in contractList"
                           :key="item.id"
                           :label="item.masterContractNo"
                           :value="item.masterContractNo" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="项目名称"
                          prop="projectName">
@@ -857,6 +870,28 @@
              v-model:visible="fileListDialogVisible"
              record-type="purchase_ledger"
              :record-id="recordId" />
    <!-- æ€»åˆåŒé™„件弹窗 -->
    <el-dialog
      v-model="contractFileDialogVisible"
      title="总合同附件"
      width="700px"
      :close-on-click-modal="false">
      <el-table :data="contractFileList" border v-if="contractFileList.length">
        <el-table-column prop="name" label="文件名称" show-overflow-tooltip />
        <el-table-column prop="byteSize" label="文件大小" width="120">
          <template #default="{ row }">
            {{ formatFileSize(row.byteSize) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150">
          <template #default="{ row }">
            <el-link type="primary" :href="row.previewURL" target="_blank">预览</el-link>
            <el-link type="primary" :href="row.downloadURL" style="margin-left: 10px">下载</el-link>
          </template>
        </el-table-column>
      </el-table>
      <el-empty v-else description="暂无附件" />
    </el-dialog>
  </div>
</template>
@@ -914,6 +949,9 @@
  const sparePartsOptions = ref([]);
  const salesContractList = ref([]);
  const supplierList = ref([]);
  const contractList = ref([]); // ä¾›åº”商合同列表
  const contractFileDialogVisible = ref(false);
  const contractFileList = ref([]);
  const tableLoading = ref(false);
  const recordId = ref();
  const fileListDialogVisible = ref(false);
@@ -926,6 +964,7 @@
  import useUserStore from "@/store/modules/user";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import { getSparePartsList } from "@/api/equipmentManagement/spareParts.js";
  import { getSupplierContractList, getSupplierContractFilesByNo } from "@/api/basicData/supplierContract.js";
  import dayjs from "dayjs";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
@@ -966,6 +1005,7 @@
      å¾…入库: "info", // å¾…入库 - ç°è‰²
      å…¥åº“中: "warning", // å…¥åº“中 - æ©™è‰²
      å®Œå…¨å…¥åº“: "success", // å®Œå…¨å…¥åº“ - ç»¿è‰²
      å·²é©³å›ž: "danger", // å·²é©³å›ž - çº¢è‰²
    };
    return typeMap[status] || "";
  };
@@ -1509,6 +1549,17 @@
            type: 2,
          });
          productData.value = productRes.data || [];
          // ç¼–辑时加载供应商的合同列表以便回显总合同号
          if (purchaseRes.supplierId) {
            try {
              const contractRes = await getSupplierContractList(purchaseRes.supplierId);
              if (contractRes.code === 200) {
                contractList.value = contractRes.data || [];
              }
            } catch (error) {
              console.error('获取供应商合同列表失败:', error);
            }
          }
        } catch (error) {
          console.error("加载采购台账数据失败:", error);
          proxy.$modal.msgError("加载数据失败");
@@ -2200,6 +2251,48 @@
    }
  };
  // ä¾›åº”商选择变化时,加载该供应商的合同列表
  const handleSupplierChange = async (supplierId) => {
    form.value.masterContractNo = '';
    contractList.value = [];
    if (!supplierId) return;
    try {
      const res = await getSupplierContractList(supplierId);
      if (res.code === 200) {
        contractList.value = res.data || [];
      }
    } catch (error) {
      console.error('获取供应商合同列表失败:', error);
    }
  };
  // æŸ¥çœ‹æ€»åˆåŒé™„ä»¶
  const openContractFileDialog = async (row) => {
    if (!row.masterContractNo) {
      proxy.$modal.msgWarning('该记录没有总合同号');
      return;
    }
    try {
      const res = await getSupplierContractFilesByNo(row.masterContractNo);
      if (res.code === 200) {
        contractFileList.value = res.data || [];
        contractFileDialogVisible.value = true;
      }
    } catch (error) {
      console.error('获取合同附件失败:', error);
    }
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = (bytes) => {
    if (!bytes) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  };
  onMounted(() => {
    getList();
    getTemplateList();
src/views/salesManagement/returnOrder/components/formDia.vue
@@ -274,8 +274,8 @@
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);
  if (!Number.isFinite(total) || !Number.isFinite(un)) return '0.00';
  return Math.max(total - un, 0).toFixed(2);
};
const tableColumn = ref([
src/views/salesManagement/salesLedger/index.vue
@@ -284,7 +284,7 @@
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column fixed="right" label="操作" width="220" align="center">
        <el-table-column fixed="right" label="操作" width="280" align="center">
          <template #default="scope">
                        <el-button
                            link
@@ -297,6 +297,13 @@
            </el-button>
            <el-button link type="primary" @click="openFileDialog(scope.row)"
              >附件
            </el-button>
            <el-button
              link
              type="primary"
              v-if="scope.row.masterContractNo"
              @click="openContractFileDialog(scope.row)"
              >总合同附件
            </el-button>
          </template>
        </el-table-column>
@@ -338,18 +345,6 @@
            <el-button type="primary" plain @click="openQuotationDialog">
              ä»Žé”€å”®æŠ¥ä»·å¯¼å…¥
            </el-button>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="总合同号:">
              <el-input
                v-model="form.masterContractNo"
                placeholder="请输入"
                clearable
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
@@ -409,6 +404,7 @@
                clearable
                :disabled="operationType === 'view'"
                filterable
                @change="handleCustomerChange"
              >
                <el-option
                  v-for="item in customerOption"
@@ -424,6 +420,26 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="总合同号:">
              <el-select
                v-model="form.masterContractNo"
                placeholder="请先选择客户"
                clearable
                filterable
                :disabled="operationType === 'view' || !form.customerId"
              >
                <el-option
                  v-for="item in contractList"
                  :key="item.id"
                  :label="item.masterContractNo"
                  :value="item.masterContractNo"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectName">
              <el-input
                v-model="form.projectName"
@@ -433,8 +449,6 @@
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="签订日期:" prop="executionDate">
              <el-date-picker
@@ -449,6 +463,8 @@
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="付款方式">
              <el-select
@@ -466,8 +482,6 @@
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="采购合同号:" prop="purchaseContractNumber">
              <el-select
@@ -947,6 +961,29 @@
      record-type="sales_ledger"
      :record-id="recordId"
    />
    <!-- æ€»åˆåŒé™„件弹窗 -->
    <el-dialog
      v-model="contractFileDialogVisible"
      title="总合同附件"
      width="700px"
      :close-on-click-modal="false"
    >
      <el-table :data="contractFileList" border v-if="contractFileList.length">
        <el-table-column prop="name" label="文件名称" show-overflow-tooltip />
        <el-table-column prop="byteSize" label="文件大小" width="120">
          <template #default="{ row }">
            {{ formatFileSize(row.byteSize) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150">
          <template #default="{ row }">
            <el-link type="primary" :href="row.previewURL" target="_blank">预览</el-link>
            <el-link type="primary" :href="row.downloadURL" style="margin-left: 10px">下载</el-link>
          </template>
        </el-table-column>
      </el-table>
      <el-empty v-else description="暂无附件" />
    </el-dialog>
    <!-- æ‰“印预览弹窗 -->
    <el-dialog
      v-model="printPreviewVisible"
@@ -1266,6 +1303,7 @@
import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
import { getCurrentDate } from "@/utils/index.js";
import { listCustomer } from "@/api/basicData/customer.js";
import { getCustomerContractList, getContractFilesByNo } from "@/api/basicData/customerContract.js";
const FileList = defineAsyncComponent(() =>
  import("@/components/Dialog/FileList.vue")
@@ -1293,6 +1331,11 @@
const total = ref(0);
const fileList = ref([]);
const deliveryFileList = ref([]);
// åˆåŒåˆ—表(供下拉选择)
const contractList = ref([]);
// æ€»åˆåŒé™„件弹窗
const contractFileDialogVisible = ref(false);
const contractFileList = ref([]);
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
@@ -1824,13 +1867,24 @@
    form.value.autoGenerateContractNo = true;
  } else {
    currentId.value = row.id;
    getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => {
    getSalesLedgerWithProducts({ id: row.id, type: 1 }).then(async (res) => {
      form.value = { ...res };
      form.value.entryPerson = Number(res.entryPerson);
      productData.value = form.value.productData;
      fileList.value = form.value.storageBlobVOs;
      // ç¼–辑时设置自动生成为false,允许手动修改
      form.value.autoGenerateContractNo = false;
      // ç¼–辑时加载客户的合同列表以便回显总合同号
      if (res.customerId) {
        try {
          const contractRes = await getCustomerContractList(res.customerId);
          if (contractRes.code === 200) {
            contractList.value = contractRes.data || [];
          }
        } catch (error) {
          console.error('获取客户合同列表失败:', error);
        }
      }
    });
  }
  // let userAll = await userStore.getInfo()
@@ -3064,6 +3118,48 @@
  return statusMap[returnStatus] || 'info';
};
// å®¢æˆ·é€‰æ‹©å˜åŒ–时,加载该客户的合同列表
const handleCustomerChange = async (customerId) => {
  form.value.masterContractNo = '';
  contractList.value = [];
  if (!customerId) return;
  try {
    const res = await getCustomerContractList(customerId);
    if (res.code === 200) {
      contractList.value = res.data || [];
    }
  } catch (error) {
    console.error('获取客户合同列表失败:', error);
  }
};
// æŸ¥çœ‹æ€»åˆåŒé™„ä»¶
const openContractFileDialog = async (row) => {
  if (!row.masterContractNo) {
    proxy.$modal.msgWarning('该记录没有总合同号');
    return;
  }
  try {
    const res = await getContractFilesByNo(row.masterContractNo);
    if (res.code === 200) {
      contractFileList.value = res.data || [];
      contractFileDialogVisible.value = true;
    }
  } catch (error) {
    console.error('获取合同附件失败:', error);
  }
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (bytes) => {
  if (!bytes) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
onMounted(() => {
  searchForm.salesContractNo = route.query.salesContractNo;
  getList();
src/views/salesManagement/salesQuotation/index.vue
@@ -52,6 +52,7 @@
      >
                <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column prop="quotationNo" label="报价单号" />
        <el-table-column prop="masterContractNo" label="总合同号" width="150" show-overflow-tooltip />
        <el-table-column prop="customer" label="客户名称" />
        <el-table-column prop="salesperson" label="业务员" width="100" />
        <el-table-column prop="quotationDate" label="报价日期" width="120" />
@@ -103,11 +104,20 @@
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="客户名称" prop="customerId">
                  <el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%" clearable filterable>
                  <el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%" clearable filterable @change="handleCustomerChange">
                    <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id"></el-option>
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="总合同号">
                  <el-select v-model="form.masterContractNo" placeholder="请先选择客户" style="width: 100%" clearable filterable :disabled="!form.customerId">
                    <el-option v-for="item in contractList" :key="item.id" :label="item.masterContractNo" :value="item.masterContractNo"></el-option>
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="业务员" prop="salesperson">
                  <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable filterable>
@@ -264,6 +274,7 @@
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
        <el-descriptions-item label="总合同号">{{ currentQuotation.masterContractNo || '--' }}</el-descriptions-item>
        <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
        <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
        <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
@@ -309,6 +320,7 @@
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {listCustomer} from "@/api/basicData/customer.js";
import { userListNoPage } from "@/api/system/user.js";
import { getCustomerContractList } from "@/api/basicData/customerContract.js";
// å“åº”式数据
const loading = ref(false)
@@ -322,6 +334,7 @@
const userList = ref([])
const productOptions = ref([]);
const modelOptions  = ref([]);
const contractList = ref([]);
const pagination = reactive({
  total: 3,
  currentPage: 1,
@@ -341,6 +354,7 @@
  paymentMethod: '',
  status: '草稿',
  remark: '',
  masterContractNo: '',
  products: [],
  subtotal: 0,
  freight: 0,
@@ -381,6 +395,7 @@
// è®¡ç®—属性
const filteredList = computed(() => {
  let list = quotationList.value
  console.log(list)
  return list
})
@@ -419,6 +434,23 @@
    customerOption.value = res.data.records;
  });
}
// å®¢æˆ·é€‰æ‹©å˜åŒ–时,加载该客户的合同列表
const handleCustomerChange = async (customerId) => {
  form.masterContractNo = '';
  contractList.value = [];
  if (!customerId) return;
  try {
    const res = await getCustomerContractList(customerId);
    if (res.code === 200) {
      contractList.value = res.data || [];
    }
  } catch (error) {
    console.error('获取客户合同列表失败:', error);
  }
};
const getProductOptions = () => {
    // è¿”回 Promise,便于编辑时 await ç¡®ä¿èƒ½åæ˜¾
    return productTreeList().then((res) => {
@@ -521,6 +553,7 @@
    quotationDate: row.quotationDate || '',
    validDate: row.validDate || '',
    paymentMethod: row.paymentMethod || '',
    masterContractNo: row.masterContractNo || '',
    status: row.status || '',
    remark: row.remark || '',
    products: row.products ? row.products.map(product => ({
@@ -557,6 +590,22 @@
  form.paymentMethod = row.paymentMethod || ''
  form.status = row.status || '草稿'
  form.remark = row.remark || ''
  // ç¼–辑时加载客户的合同列表以便回显总合同号
  if (row.customerId) {
    try {
      const contractRes = await getCustomerContractList(row.customerId);
      if (contractRes.code === 200) {
        contractList.value = contractRes.data || [];
      }
    } catch (error) {
      console.error('获取客户合同列表失败:', error);
    }
  }
  // å…¼å®¹åŽç«¯è¿”回的 masterContractNo æˆ– master_contract_no,放在合同列表加载后设置
  form.masterContractNo = row.masterContractNo || row.master_contract_no || ''
  form.products = row.products ? await Promise.all(row.products.map(async (product) => {
    const productName = product.product || product.productName || ''
    // ä¼˜å…ˆç”¨ productId;如果只有名称,尝试反查 id ä»¥ä¾¿æ ‘选择器反显
@@ -634,11 +683,13 @@
  form.paymentMethod = ''
  form.status = '草稿'
  form.remark = ''
  form.masterContractNo = ''
  form.products = []
  form.subtotal = 0
  form.freight = 0
  form.otherFee = 0
  form.totalAmount = 0
  contractList.value = []
}
const addProduct = () => {
@@ -749,6 +800,7 @@
        customer: item.customer || '',
        customerId: item.customerId || undefined,
        salesperson: item.salesperson || '',
        masterContractNo: item.masterContractNo || '',
        quotationDate: item.quotationDate || '',
        validDate: item.validDate || '',
        paymentMethod: item.paymentMethod || '',