瀏覽代碼

feat(customer): 添加客户合同管理功能

- 将供应商等级选择框的值转换为字符串类型以确保正确绑定
- 更新合同类型选项的值从中文文本改为数字编码(0/1/2)
- 在客户详情页面添加合同搜索和重置按钮
- 实现合同表格中显示合同类型的文本映射
- 将地址表单项的验证字段从addressRegion改为shippingProvincial
- 添加完整的合同管理对话框组件,包含合同表单的所有字段
- 集成文件上传组件用于合同附件管理
- 实现合同的增删改查功能和表单验证
- 优化异步数据加载顺序,确保下拉选项正确显示
- 修复地址和付款信息表单中的供应商ID绑定问题
- 将供应商地址API接口路径从/system/address调整为/customer/supplieraddress
- 添加供应商ID字段到地址表单数据结构中
肖路 2 月之前
父節點
當前提交
6d312e9d0e
共有 3 個文件被更改,包括 322 次插入28 次删除
  1. 5 5
      src/api/supplier/address/index.ts
  2. 5 0
      src/api/supplier/address/types.ts
  3. 312 23
      src/views/customer/info/detail.vue

+ 5 - 5
src/api/supplier/address/index.ts

@@ -10,7 +10,7 @@ import { AddressVO, AddressForm, AddressQuery } from '@/api/supplier/address/typ
 
 export const listAddress = (query?: AddressQuery): AxiosPromise<AddressVO[]> => {
   return request({
-    url: '/system/address/list',
+    url: '/customer/supplieraddress/list',
     method: 'get',
     params: query
   });
@@ -22,7 +22,7 @@ export const listAddress = (query?: AddressQuery): AxiosPromise<AddressVO[]> =>
  */
 export const getAddress = (id: string | number): AxiosPromise<AddressVO> => {
   return request({
-    url: '/system/address/' + id,
+    url: '/customer/supplieraddress/' + id,
     method: 'get'
   });
 };
@@ -33,7 +33,7 @@ export const getAddress = (id: string | number): AxiosPromise<AddressVO> => {
  */
 export const addAddress = (data: AddressForm) => {
   return request({
-    url: '/system/address',
+    url: '/customer/supplieraddress',
     method: 'post',
     data: data
   });
@@ -45,7 +45,7 @@ export const addAddress = (data: AddressForm) => {
  */
 export const updateAddress = (data: AddressForm) => {
   return request({
-    url: '/system/address',
+    url: '/customer/supplieraddress',
     method: 'put',
     data: data
   });
@@ -57,7 +57,7 @@ export const updateAddress = (data: AddressForm) => {
  */
 export const delAddress = (id: string | number | Array<string | number>) => {
   return request({
-    url: '/system/address/' + id,
+    url: '/customer/supplieraddress/' + id,
     method: 'delete'
   });
 };

+ 5 - 0
src/api/supplier/address/types.ts

@@ -92,6 +92,11 @@ export interface AddressForm extends BaseEntity {
    */
   supplierNo?: string;
 
+  /**
+   * 供应商ID
+   */
+  supplierId?: string | number;
+
   /**
    * 配送公司名称
    */

+ 312 - 23
src/views/customer/info/detail.vue

@@ -61,7 +61,7 @@
                         v-for="level in supplierLevelOptions"
                         :key="level.id"
                         :label="level.supplierLevelName"
-                        :value="level.id"
+                        :value="String(level.id)"
                       />
                     </el-select>
                   </el-form-item>
@@ -603,9 +603,9 @@
               </el-form-item>
               <el-form-item label="合同类型">
                 <el-select v-model="contractSearchParams.contractType" placeholder="请选择" clearable filterable style="width: 150px;">
-                  <el-option label="年度合作" value="年度合作" />
-                  <el-option label="项目采购" value="项目采购" />
-                  <el-option label="其他合作" value="其他合作" />
+                  <el-option label="年度合作" value="0" />
+                  <el-option label="项目采购" value="1" />
+                  <el-option label="其他合作" value="2" />
                 </el-select>
               </el-form-item>
               <el-form-item label="开始时间">
@@ -635,6 +635,7 @@
               <el-form-item>
                 <el-button type="primary" icon="Search" @click="handleContractSearch">搜索</el-button>
                 <el-button icon="Refresh" @click="handleContractReset">重置</el-button>
+                <el-button icon="Plus" type="primary" @click="handleAddContract">添加合同</el-button>
               </el-form-item>
             </el-form>
 
@@ -644,7 +645,11 @@
             <el-table :data="contractList" border style="width: 100%">
               <el-table-column prop="contractNo" label="合同编号" align="center" />
               <el-table-column prop="contractName" label="合同名称" align="center" />
-              <el-table-column prop="contractType" label="合同类型" align="center" />
+              <el-table-column prop="contractType" label="合同类型" align="center">
+                <template #default="scope">
+                  <span>{{ getContractTypeText(scope.row.contractType) }}</span>
+                </template>
+              </el-table-column>
               <el-table-column prop="contractAmount" label="金额(万)" align="center">
                 <template #default="scope">
                   <span>{{ scope.row.contractAmount || '-' }}</span>
@@ -974,7 +979,7 @@
 
         <el-row :gutter="20">
           <el-col :span="24">
-            <el-form-item label="地址:" prop="addressRegion" required>
+            <el-form-item label="地址:" prop="shippingProvincial" required>
               <el-cascader
                 v-model="selectedAddressRegion"
                 :options="regionOptions"
@@ -1018,6 +1023,147 @@
       </template>
     </el-dialog>
 
+    <!-- 合同管理对话框 -->
+    <el-dialog
+      v-model="contractDialogVisible"
+      :title="contractDialogTitle"
+      width="900px"
+      :close-on-click-modal="false"
+    >
+      <el-form
+        ref="contractFormRef"
+        :model="contractForm"
+        :rules="contractFormRules"
+        label-width="140px"
+        :disabled="contractDialogReadonly"
+      >
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="合同编号:" prop="contractNo">
+              <el-input v-model="contractForm.contractNo" placeholder="请输入合同编号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="合同名称:" prop="contractName">
+              <el-input v-model="contractForm.contractName" placeholder="请输入合同名称" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="合同类型:" prop="contractType">
+              <el-select v-model="contractForm.contractType" placeholder="请选择" style="width: 100%;">
+                <el-option label="年度合作" :value="0" />
+                <el-option label="项目采购" :value="1" />
+                <el-option label="其他合作" :value="2" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="合同金额(万):" prop="contractAmount">
+              <el-input v-model="contractForm.contractAmount" placeholder="请输入合同金额" type="number" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="合同生效时间:" prop="contractStartTime">
+              <el-date-picker
+                v-model="contractForm.contractStartTime"
+                type="date"
+                placeholder="请选择"
+                style="width: 100%;"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="合同到期时间:" prop="contractEndTime">
+              <el-date-picker
+                v-model="contractForm.contractEndTime"
+                type="date"
+                placeholder="请选择"
+                style="width: 100%;"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="合同状态:" prop="contractStatus">
+              <el-select v-model="contractForm.contractStatus" placeholder="请选择" style="width: 100%;">
+                <el-option label="草稿" :value="0" />
+                <el-option label="生效" :value="1" />
+                <el-option label="失效" :value="2" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="税率(%):" prop="taxRate">
+              <el-input v-model="contractForm.taxRate" placeholder="请输入税率" type="number" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="结算方式:" prop="settlementMethod">
+              <el-input v-model="contractForm.settlementMethod" placeholder="请输入结算方式" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="发票类型:" prop="invoiceType">
+              <el-input v-model="contractForm.invoiceType" placeholder="请输入发票类型" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="到期提醒(天):" prop="demandReminderTime">
+              <el-input v-model="contractForm.demandReminderTime" placeholder="请输入提前提醒天数" type="number" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="合同附件:" prop="contractAttachment">
+              <FileUpload
+                v-model="contractForm.contractAttachment"
+                :limit="5"
+                :file-size="50"
+                :file-type="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png']"
+                :disabled="contractDialogReadonly"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="合同说明:" prop="contractDescription">
+              <el-input
+                v-model="contractForm.contractDescription"
+                type="textarea"
+                :rows="3"
+                placeholder="请输入合同说明"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="contractDialogVisible = false">取消</el-button>
+          <el-button v-if="!contractDialogReadonly" type="primary" @click="handleContractSubmit" :loading="contractSubmitLoading">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
     <!-- 品牌新增对话框 -->
     <el-dialog
       v-model="brandDialogVisible"
@@ -1089,6 +1235,7 @@ import { ArrowLeft, Plus } from '@element-plus/icons-vue';
 import { ElMessage } from 'element-plus';
 import FileSelector from '@/components/FileSelector/index.vue';
 import RegionCascader from '@/components/RegionCascader/index.vue';
+import FileUpload from '@/components/FileUpload/index.vue';
 import { getInfo, addInfo, updateInfo, getStaffListSplice, getSupplierStaffIds, getContactListById, getProductCategoryList, getSupplierCategories, getSupplierContractsById, getBankBySupplierId, getAuthorizeDetailList, savePurchaseInfo } from '@/api/customer/info';
 import { getBank, updateBank, addBank } from '@/api/customer/bank';
 import { BankForm } from '@/api/customer/bank/types';
@@ -1109,6 +1256,8 @@ import { listBrand, getBrand } from '@/api/product/brand';
 import { BrandVO } from '@/api/product/brand/types';
 import { listAddress, getAddress, addAddress, updateAddress } from '@/api/supplier/address';
 import { AddressForm } from '@/api/supplier/address/types';
+import { listContract, getContract, addContract, updateContract } from '@/api/supplier/contract';
+import { ContractForm } from '@/api/supplier/contract/types';
 
 const route = useRoute();
 const router = useRouter();
@@ -1221,6 +1370,7 @@ const addressSubmitLoading = ref(false);
 const selectedAddressRegion = ref<string[]>([]); // 地址的省市区代码数组
 const addressForm = ref<AddressForm>({
   supplierNo: '',
+  supplierId: undefined,
   shipperName: '',
   shipperPhone: '',
   shippingPostCode: '',
@@ -1235,7 +1385,7 @@ const addressForm = ref<AddressForm>({
 const addressFormRules = {
   shipperName: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
   shipperPhone: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
-  addressRegion: [{ required: true, message: '请选择地址', trigger: 'change' }]
+  shippingProvincial: [{ required: true, message: '请选择地址', trigger: 'change' }]
 };
 
 // 付款信息列表
@@ -1300,6 +1450,37 @@ const contractPagination = ref({
   total: 0
 });
 
+// 合同对话框相关
+const contractDialogVisible = ref(false);
+const contractDialogTitle = ref('');
+const contractDialogReadonly = ref(false);
+const contractFormRef = ref<ElFormInstance>();
+const contractSubmitLoading = ref(false);
+const contractForm = ref<ContractForm>({
+  supplierNo: '',
+  supplierId: undefined,
+  contractNo: '',
+  contractName: '',
+  contractType: '',
+  contractStartTime: '',
+  contractEndTime: '',
+  contractStatus: 0,
+  demandReminderTime: undefined,
+  taxRate: undefined,
+  contractAmount: undefined,
+  contractDescription: '',
+  contractAttachment: '',
+  settlementMethod: '',
+  invoiceType: ''
+});
+
+// 合同表单验证规则
+const contractFormRules = {
+  contractNo: [{ required: true, message: '请输入合同编号', trigger: 'blur' }],
+  contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
+  contractType: [{ required: true, message: '请选择合同类型', trigger: 'change' }]
+};
+
 /** 返回列表页 */
 const goBack = () => {
   // 使用浏览器后退,返回上一页
@@ -2106,6 +2287,16 @@ const getContractStatusText = (status: number) => {
   return statusMap[status] || '未知';
 };
 
+/** 获取合同类型文本 */
+const getContractTypeText = (type: string | number) => {
+  const typeMap = {
+    '0': '年度合作',
+    '1': '项目采购',
+    '2': '其他合作',
+  };
+  return typeMap[type] || '未知';
+};
+
 /** 获取授权状态文本 */
 const getAuthorizedStatusText = (status: string) => {
   const statusMap = {
@@ -2160,15 +2351,96 @@ const handleViewAttachment = (row: any) => {
 };
 
 /** 查看合同 */
-const handleViewContract = (row: any) => {
-  console.log('查看合同:', row);
-  // 这里处理合同查看逻辑
+const handleViewContract = async (row: any) => {
+  try {
+    const res = await getContract(row.id);
+    Object.assign(contractForm.value, res.data);
+    contractDialogTitle.value = '查看合同';
+    contractDialogReadonly.value = true;
+    contractDialogVisible.value = true;
+  } catch (e) {
+    console.error('获取合同详情失败:', e);
+    ElMessage.error('获取合同详情失败');
+  }
 };
 
 /** 编辑合同 */
-const handleEditContract = (row: any) => {
-  console.log('编辑合同:', row);
-  // 这里处理合同编辑逻辑
+const handleEditContract = async (row: any) => {
+  try {
+    const res = await getContract(row.id);
+    Object.assign(contractForm.value, res.data);
+    contractDialogTitle.value = '编辑合同';
+    contractDialogReadonly.value = false;
+    contractDialogVisible.value = true;
+  } catch (e) {
+    console.error('获取合同详情失败:', e);
+    ElMessage.error('获取合同详情失败');
+  }
+};
+
+/** 新增合同 */
+const handleAddContract = () => {
+  // 重置表单
+  contractForm.value = {
+    supplierNo: detailData.value.supplierNo,
+    supplierId: route.query.id as any,
+    contractNo: '',
+    contractName: '',
+    contractType: '',
+    contractStartTime: '',
+    contractEndTime: '',
+    contractStatus: 0,
+    demandReminderTime: undefined,
+    taxRate: undefined,
+    contractAmount: undefined,
+    contractDescription: '',
+    contractAttachment: '',
+    settlementMethod: '',
+    invoiceType: ''
+  };
+  contractDialogTitle.value = '添加合同';
+  contractDialogReadonly.value = false;
+  contractDialogVisible.value = true;
+};
+
+/** 提交合同 */
+const handleContractSubmit = async () => {
+  if (!contractFormRef.value) return;
+
+  contractFormRef.value.validate(async (valid: boolean) => {
+    if (!valid) return;
+
+    try {
+      contractSubmitLoading.value = true;
+
+      // 设置供应商编号和ID
+      if (!contractForm.value.supplierNo && detailData.value.supplierNo) {
+        contractForm.value.supplierNo = detailData.value.supplierNo;
+      }
+      if (!contractForm.value.supplierId && route.query.id) {
+        contractForm.value.supplierId = route.query.id as any;
+      }
+
+      if ((contractForm.value as any).id) {
+        // 更新
+        await updateContract(contractForm.value);
+        ElMessage.success('更新成功');
+      } else {
+        // 新增
+        await addContract(contractForm.value);
+        ElMessage.success('新增成功');
+      }
+
+      contractDialogVisible.value = false;
+      // 刷新合同列表
+      await getContractList();
+    } catch (e) {
+      console.error('保存合同失败:', e);
+      ElMessage.error('保存合同失败');
+    } finally {
+      contractSubmitLoading.value = false;
+    }
+  });
 };
 
 /** 分页大小改变 */
@@ -2213,6 +2485,12 @@ const handleEditPayment = async (row: any) => {
 
 /** 新增付款信息 */
 const handleAddPayment = () => {
+  // 检查是否已保存供应商基本信息
+  if (!detailData.value.id || !detailData.value.supplierNo) {
+    ElMessage.warning('请先保存供应商基本信息后再新增付款信息');
+    return;
+  }
+  
   // 重置表单
   paymentForm.value = {
     num: undefined,
@@ -2291,6 +2569,7 @@ const handleAddAddress = () => {
   // 重置表单
   addressForm.value = {
     supplierNo: detailData.value.supplierNo,
+    supplierId: detailData.value.id,
     shipperName: '',
     shipperPhone: '',
     shippingPostCode: '',
@@ -2422,10 +2701,13 @@ const handleAddressSubmit = async () => {
     try {
       addressSubmitLoading.value = true;
 
-      // 设置供应商编号
+      // 设置供应商编号和ID
       if (!addressForm.value.supplierNo && detailData.value.supplierNo) {
         addressForm.value.supplierNo = detailData.value.supplierNo;
       }
+      if (!addressForm.value.supplierId && detailData.value.id) {
+        addressForm.value.supplierId = detailData.value.id;
+      }
 
       if ((addressForm.value as any).id) {
         // 更新
@@ -2449,15 +2731,22 @@ const handleAddressSubmit = async () => {
   });
 };
 
-onMounted(() => {
-  initRegionOptions(); // 初始化省市区数据
-  getDetail();
-  getStaffOptions();
-  getCompanyOptions(); // 获取公司下拉选项
-  getEnterpriseScaleOptions(); // 获取企业规模下拉选项
-  getIndustryCategoryOptions(); // 获取行业类别下拉选项
-  getSupplierLevelOptions(); // 获取供应商等级下拉选项
-  getSupplierTypeOptions(); // 获取供应商类型下拉选项
+onMounted(async () => {
+  // 先初始化省市区数据
+  initRegionOptions();
+  
+  // 并行加载所有下拉框选项数据
+  await Promise.all([
+    getStaffOptions(),
+    getCompanyOptions(), // 获取公司下拉选项
+    getEnterpriseScaleOptions(), // 获取企业规模下拉选项
+    getIndustryCategoryOptions(), // 获取行业类别下拉选项
+    getSupplierLevelOptions(), // 获取供应商等级下拉选项
+    getSupplierTypeOptions() // 获取供应商类型下拉选项
+  ]);
+  
+  // 下拉框选项加载完成后,再获取详情数据,确保下拉框能正确显示文字标签
+  await getDetail();
 
   // 只有在编辑模式或基础信息已保存时才加载其他数据
   const id = route.query.id as string;