zlwu 11 месяцев назад
Родитель
Сommit
770800b664

+ 10 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/CrmQuote.java

@@ -185,6 +185,16 @@ public class CrmQuote extends TenantEntity {
      */
     private String ownerUserName;
 
+    /**
+     * 报价主体ID(关联crm_supplier_info表)
+     */
+    private Long supplierId;
+
+    /**
+     * 报价主体名称(冗余字段)
+     */
+    private String supplierName;
+
     /**
      * 关联合同ID(转合同后)
      */

+ 11 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/bo/CrmQuoteBo.java

@@ -134,6 +134,17 @@ public class CrmQuoteBo extends BaseEntity {
      */
     private String ownerUserName;
 
+    /**
+     * 报价主体ID(关联crm_supplier_info表)
+     */
+    @NotNull(message = "报价主体不能为空", groups = { AddGroup.class, EditGroup.class })
+    private Long supplierId;
+
+    /**
+     * 报价主体名称(冗余字段)
+     */
+    private String supplierName;
+
     /**
      * 关联合同ID
      */

+ 11 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/vo/CrmQuoteVo.java

@@ -210,6 +210,17 @@ public class CrmQuoteVo implements Serializable {
      */
     private String ownerUserMobile;
 
+    /**
+     * 报价主体ID
+     */
+    private Long supplierId;
+
+    /**
+     * 报价主体名称
+     */
+    @ExcelProperty(value = "报价主体")
+    private String supplierName;
+
     /**
      * 关联合同ID
      */

+ 37 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/service/impl/CrmQuoteServiceImpl.java

@@ -61,6 +61,7 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
     private final ICrmContactService contactService;
     private final ISysUserService userService;
     private final CrmCustomerMapper customerMapper;
+    private final ICrmSupplierInfoService supplierInfoService;
 
     /**
      * 构建查询条件
@@ -197,6 +198,18 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
                     log.warn("查询负责人手机号失败, ownerUserId: {}", vo.getOwnerUserId(), e);
                 }
             }
+
+            // 填充报价主体名称
+            if (vo.getSupplierId() != null && StringUtils.isBlank(vo.getSupplierName())) {
+                try {
+                    var supplierInfo = supplierInfoService.queryById(vo.getSupplierId());
+                    if (supplierInfo != null) {
+                        vo.setSupplierName(supplierInfo.getSupplierName());
+                    }
+                } catch (Exception e) {
+                    log.warn("查询报价主体信息失败, supplierId: {}", vo.getSupplierId(), e);
+                }
+            }
         }
         return vo;
     }
@@ -265,6 +278,18 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
             }
         }
 
+        // 填充报价主体名称
+        if (entity.getSupplierId() != null && StringUtils.isBlank(entity.getSupplierName())) {
+            try {
+                var supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
+                if (supplierInfo != null) {
+                    entity.setSupplierName(supplierInfo.getSupplierName());
+                }
+            } catch (Exception e) {
+                log.warn("查询报价主体信息失败, supplierId: {}", entity.getSupplierId(), e);
+            }
+        }
+
         // 计算到期日期
         if (entity.getValidityDays() != null && entity.getQuoteDate() != null) {
             entity.setExpireDate(entity.getQuoteDate().plusDays(entity.getValidityDays()));
@@ -312,6 +337,18 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
             }
         }
 
+        // 填充报价主体名称
+        if (entity.getSupplierId() != null && StringUtils.isBlank(entity.getSupplierName())) {
+            try {
+                var supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
+                if (supplierInfo != null) {
+                    entity.setSupplierName(supplierInfo.getSupplierName());
+                }
+            } catch (Exception e) {
+                log.warn("查询报价主体信息失败, supplierId: {}", entity.getSupplierId(), e);
+            }
+        }
+
         // 计算到期日期
         if (entity.getValidityDays() != null && entity.getQuoteDate() != null) {
             entity.setExpireDate(entity.getQuoteDate().plusDays(entity.getValidityDays()));

+ 6 - 0
sql/add_quote_supplier_id.sql

@@ -0,0 +1,6 @@
+-- 在报价单表中添加报价主体字段
+ALTER TABLE `crm_quote` 
+ADD COLUMN `supplier_id` bigint(20) DEFAULT NULL COMMENT '报价主体ID(关联crm_supplier_info表)' AFTER `owner_user_name`,
+ADD COLUMN `supplier_name` varchar(200) DEFAULT NULL COMMENT '报价主体名称(冗余字段)' AFTER `supplier_id`,
+ADD KEY `idx_supplier_id` (`supplier_id`);
+

+ 57 - 0
sql/crm_supplier_info.sql

@@ -0,0 +1,57 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server         : 47.99.164.66  hn02le.34lkdLKD
+ Source Server Type    : MySQL
+ Source Server Version : 50736 (5.7.36)
+ Source Host           : 47.99.164.66:3306
+ Source Schema         : sx
+
+ Target Server Type    : MySQL
+ Target Server Version : 50736 (5.7.36)
+ File Encoding         : 65001
+
+ Date: 15/11/2025 08:17:31
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for crm_supplier_info
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_supplier_info`;
+CREATE TABLE `crm_supplier_info` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `supplier_name` varchar(200) NOT NULL COMMENT '供方名称(章)',
+  `short_name` varchar(100) DEFAULT NULL COMMENT '简称',
+  `supplier_code` varchar(50) DEFAULT NULL COMMENT '编码',
+  `supplier_address` varchar(500) DEFAULT NULL COMMENT '单位地址',
+  `legal_person` varchar(100) DEFAULT NULL COMMENT '法定代表人',
+  `agent` varchar(100) DEFAULT NULL COMMENT '代理人',
+  `phone_fax` varchar(100) DEFAULT NULL COMMENT '电话/传真',
+  `bank_name` varchar(200) DEFAULT NULL COMMENT '开户银行',
+  `bank_account` varchar(100) DEFAULT NULL COMMENT '账号',
+  `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
+  `create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
+  `del_flag` char(1) DEFAULT '0' COMMENT '逻辑删除标志(0存在 1删除)',
+  PRIMARY KEY (`id`),
+  KEY `idx_tenant_id` (`tenant_id`),
+  KEY `idx_del_flag` (`del_flag`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='供方信息表';
+
+-- ----------------------------
+-- Records of crm_supplier_info
+-- ----------------------------
+BEGIN;
+INSERT INTO `crm_supplier_info` (`id`, `supplier_name`, `short_name`, `supplier_code`, `supplier_address`, `legal_person`, `agent`, `phone_fax`, `bank_name`, `bank_account`, `status`, `remark`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `del_flag`) VALUES (1, '安庆市山峡化工有限公司', '山峡', 'SX', '安徽省安庆市高新区霞虹路 5 号', '吴春生', '', '0556-5348918', '中国建设银行安庆市城中支行', '34050168860800000121', '0', NULL, NULL, NULL, '2025-11-13 07:19:14', NULL, NULL, '000000', '0');
+INSERT INTO `crm_supplier_info` (`id`, `supplier_name`, `short_name`, `supplier_code`, `supplier_address`, `legal_person`, `agent`, `phone_fax`, `bank_name`, `bank_account`, `status`, `remark`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `del_flag`) VALUES (2, '安徽云栋化工有限公司', '云栋', 'YD', '安徽省安庆市运动', '吴', '', '0556-5348911', '中国', '340501688608000123123', '0', NULL, NULL, NULL, '2025-11-13 07:25:13', NULL, NULL, '000000', '0');
+COMMIT;
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 148 - 146
web/src/api/crm/quote/index.ts

@@ -1,142 +1,152 @@
-import request from '@/utils/request'
+import request from '@/utils/request';
 
 /**
  * 报价单查询参数
  */
 export interface QuoteQuery {
-  pageNum?: number
-  pageSize?: number
-  quoteNo?: string
-  quoteTitle?: string
-  customerId?: number
-  customerName?: string
-  opportunityId?: number
-  status?: string
-  ownerUserId?: number
-  isLatest?: number
-  minAmount?: number
-  maxAmount?: number
-  quoteDateBegin?: string
-  quoteDateEnd?: string
-  beginTime?: string
-  endTime?: string
+  pageNum?: number;
+  pageSize?: number;
+  quoteNo?: string;
+  quoteTitle?: string;
+  customerId?: number;
+  customerName?: string;
+  opportunityId?: number;
+  status?: string;
+  ownerUserId?: number;
+  isLatest?: number;
+  minAmount?: number;
+  maxAmount?: number;
+  quoteDateBegin?: string;
+  quoteDateEnd?: string;
+  beginTime?: string;
+  endTime?: string;
 }
 
 /**
  * 报价单表单
  */
 export interface QuoteForm {
-  id?: number
-  quoteNo?: string
-  quoteTitle: string
-  customerId: number
-  customerName?: string
-  opportunityId?: number
-  contactId?: number
-  contactName?: string
-  contactMobile?: string
-  contactEmail?: string
-  version?: string
-  parentId?: number
-  discountRate?: number
-  validityDays?: number
-  quoteDate?: string
-  expireDate?: string
-  ownerUserId?: number
-  ownerUserName?: string
-  paymentTerms?: string
-  deliveryTerms?: string
-  attachments?: string
-  remark?: string
-  products: QuoteProductForm[]
+  id?: number;
+  quoteNo?: string;
+  quoteTitle: string;
+  customerId: number;
+  customerName?: string;
+  opportunityId?: number;
+  contactId?: number;
+  contactName?: string;
+  contactMobile?: string;
+  contactEmail?: string;
+  version?: string;
+  parentId?: number;
+  discountRate?: number;
+  validityDays?: number;
+  quoteDate?: string;
+  expireDate?: string;
+  ownerUserId?: number;
+  ownerUserName?: string;
+  supplierId?: number;
+  supplierName?: string;
+  paymentTerms?: string;
+  deliveryTerms?: string;
+  attachments?: string;
+  remark?: string;
+  products: QuoteProductForm[];
 }
 
 /**
  * 报价单产品表单
  */
 export interface QuoteProductForm {
-  id?: number
-  quoteId?: number
-  productId: number
-  productName?: string
-  productNo?: string
-  categoryId?: number
-  categoryName?: string
-  model?: string
-  unit?: string
-  quantity: number
-  price: number
-  discount?: number
-  subtotal?: number
-  description?: string
-  sort?: number
-  remark?: string
+  id?: number;
+  quoteId?: number;
+  productId: number;
+  productName?: string;
+  productNo?: string;
+  categoryId?: number;
+  categoryName?: string;
+  model?: string;
+  unit?: string;
+  quantity: number;
+  price: number;
+  discount?: number;
+  subtotal?: number;
+  description?: string;
+  sort?: number;
+  remark?: string;
 }
 
 /**
  * 报价单视图对象
  */
 export interface QuoteVO {
-  id: number
-  quoteNo: string
-  quoteTitle: string
-  customerId: number
-  customerName: string
-  opportunityId?: number
-  contactId?: number
-  contactName: string
-  contactMobile: string
-  contactEmail: string
-  version: string
-  parentId: number
-  isLatest: number
-  totalAmount: number
-  discountRate: number
-  discountAmount: number
-  finalAmount: number
-  validityDays: number
-  quoteDate: string
-  expireDate: string
-  status: string
-  rejectReason?: string
-  pdfUrl?: string
-  pdfGenerated: number
-  shareLink?: string
-  shareCode?: string
-  viewCount: number
-  lastViewTime?: string
-  ownerUserId: number
-  ownerUserName: string
-  contractId?: number
-  paymentTerms: string
-  deliveryTerms: string
-  attachments?: string
-  createTime: string
-  remark?: string
-  productList?: QuoteProductVO[]
+  id: number;
+  quoteNo: string;
+  quoteTitle: string;
+  customerId: number;
+  customerName: string;
+  opportunityId?: number;
+  contactId?: number;
+  contactName: string;
+  contactMobile: string;
+  contactEmail: string;
+  version: string;
+  parentId: number;
+  isLatest: number;
+  totalAmount: number;
+  discountRate: number;
+  discountAmount: number;
+  finalAmount: number;
+  validityDays: number;
+  quoteDate: string;
+  expireDate: string;
+  status: string;
+  rejectReason?: string;
+  pdfUrl?: string;
+  pdfGenerated: number;
+  shareLink?: string;
+  shareCode?: string;
+  viewCount: number;
+  lastViewTime?: string;
+  ownerUserId: number;
+  ownerUserName: string;
+  supplierId?: number;
+  supplierName?: string;
+  supplierCode?: string;
+  supplierShortName?: string;
+  contractId?: number;
+  paymentTerms: string;
+  deliveryTerms: string;
+  attachments?: string;
+  createTime: string;
+  remark?: string;
+  productList?: QuoteProductVO[];
 }
 
 /**
  * 报价单产品视图对象
  */
 export interface QuoteProductVO {
-  id: number
-  quoteId: number
-  productId: number
-  productName: string
-  productNo: string
-  categoryId?: number
-  categoryName?: string
-  model?: string
-  unit: string
-  quantity: number
-  price: number
-  discount: number
-  subtotal: number
-  description?: string
-  sort: number
-  createTime: string
-  remark?: string
+  id: number;
+  quoteId: number;
+  productId: number;
+  productName: string;
+  productNo: string;
+  categoryId?: number;
+  categoryName?: string;
+  model?: string;
+  unit: string;
+  quantity: number;
+  price: number;
+  discount: number;
+  subtotal: number;
+  taxAmount?: number;
+  taxRate?: number;
+  packageSpec?: string;
+  deliveryMethod?: string;
+  description?: string;
+  sort: number;
+  createTime: string;
+  remark?: string;
 }
 
 /**
@@ -147,7 +157,7 @@ export function listQuote(query: QuoteQuery) {
     url: '/crm/quote/list',
     method: 'get',
     params: query
-  })
+  });
 }
 
 /**
@@ -157,7 +167,7 @@ export function getQuote(id: number) {
   return request({
     url: `/crm/quote/${id}`,
     method: 'get'
-  })
+  });
 }
 
 /**
@@ -168,7 +178,7 @@ export function addQuote(data: QuoteForm) {
     url: '/crm/quote',
     method: 'post',
     data: data
-  })
+  });
 }
 
 /**
@@ -179,7 +189,7 @@ export function updateQuote(data: QuoteForm) {
     url: '/crm/quote',
     method: 'put',
     data: data
-  })
+  });
 }
 
 /**
@@ -189,7 +199,7 @@ export function delQuote(ids: number | number[]) {
   return request({
     url: `/crm/quote/${ids}`,
     method: 'delete'
-  })
+  });
 }
 
 /**
@@ -200,7 +210,7 @@ export function submitQuote(data: { id: number; approverIds: number[]; remark?:
     url: '/crm/quote/submit',
     method: 'put',
     data: data
-  })
+  });
 }
 
 /**
@@ -211,7 +221,7 @@ export function generatePdf(id: number) {
     url: '/crm/quote/generatePdf',
     method: 'post',
     data: { id }
-  })
+  });
 }
 
 /**
@@ -222,7 +232,7 @@ export function generateShare(data: { id: number; validityDays?: number; passwor
     url: '/crm/quote/generateShare',
     method: 'post',
     data: data
-  })
+  });
 }
 
 /**
@@ -233,7 +243,7 @@ export function viewByShareCode(shareCode: string, password?: string) {
     url: `/crm/quote/share/${shareCode}`,
     method: 'get',
     params: { password }
-  })
+  });
 }
 
 /**
@@ -244,7 +254,7 @@ export function acceptQuote(data: { id: number; remark?: string }) {
     url: '/crm/quote/accept',
     method: 'put',
     data: data
-  })
+  });
 }
 
 /**
@@ -255,25 +265,18 @@ export function rejectQuote(data: { id: number; rejectReason: string; remark?: s
     url: '/crm/quote/reject',
     method: 'put',
     data: data
-  })
+  });
 }
 
 /**
  * 转为合同
  */
-export function toContract(data: {
-  id: number
-  contractNo: string
-  contractTitle: string
-  signDate?: string
-  startDate?: string
-  endDate?: string
-}) {
+export function toContract(data: { id: number; contractNo: string; contractTitle: string; signDate?: string; startDate?: string; endDate?: string }) {
   return request({
     url: '/crm/quote/toContract',
     method: 'post',
     data: data
-  })
+  });
 }
 
 /**
@@ -284,7 +287,7 @@ export function createNewVersion(data: { id: number; version: string; remark?: s
     url: '/crm/quote/newVersion',
     method: 'post',
     data: data
-  })
+  });
 }
 
 /**
@@ -295,7 +298,7 @@ export function queryVersions(quoteNo: string) {
     url: '/crm/quote/versions',
     method: 'get',
     params: { quoteNo }
-  })
+  });
 }
 
 /**
@@ -307,7 +310,7 @@ export function exportQuote(query: QuoteQuery) {
     method: 'post',
     data: query,
     responseType: 'blob'
-  })
+  });
 }
 
 // ==================== 报价单产品接口 ====================
@@ -320,7 +323,7 @@ export function listQuoteProduct(quoteId: number) {
     url: '/crm/quote/product/list',
     method: 'get',
     params: { quoteId }
-  })
+  });
 }
 
 /**
@@ -331,7 +334,7 @@ export function addQuoteProduct(data: QuoteProductForm) {
     url: '/crm/quote/product',
     method: 'post',
     data: data
-  })
+  });
 }
 
 /**
@@ -342,7 +345,7 @@ export function updateQuoteProduct(data: QuoteProductForm) {
     url: '/crm/quote/product',
     method: 'put',
     data: data
-  })
+  });
 }
 
 /**
@@ -352,7 +355,7 @@ export function delQuoteProduct(ids: number | number[]) {
   return request({
     url: `/crm/quote/product/${ids}`,
     method: 'delete'
-  })
+  });
 }
 
 // ==================== 报价单审批接口 ====================
@@ -365,7 +368,7 @@ export function listQuoteApproval(quoteId: number) {
     url: '/crm/quote/approval/list',
     method: 'get',
     params: { quoteId }
-  })
+  });
 }
 
 /**
@@ -376,7 +379,7 @@ export function approveQuote(data: { id: number; approvalResult: string; approva
     url: '/crm/quote/approval',
     method: 'post',
     data: data
-  })
+  });
 }
 
 /**
@@ -387,7 +390,7 @@ export function withdrawApproval(data: { id: number; remark?: string }) {
     url: '/crm/quote/approval/withdraw',
     method: 'put',
     data: data
-  })
+  });
 }
 
 // ==================== 统计分析接口 ====================
@@ -400,7 +403,7 @@ export function statisticsOverview(params?: { ownerUserId?: number; beginTime?:
     url: '/crm/quote/statistics/overview',
     method: 'get',
     params: params
-  })
+  });
 }
 
 /**
@@ -410,7 +413,7 @@ export function statisticsStatus() {
   return request({
     url: '/crm/quote/statistics/status',
     method: 'get'
-  })
+  });
 }
 
 /**
@@ -421,7 +424,7 @@ export function statisticsTrend(params: { type?: string; beginTime?: string; end
     url: '/crm/quote/statistics/trend',
     method: 'get',
     params: params
-  })
+  });
 }
 
 // ==================== 分享访问日志接口 ====================
@@ -434,7 +437,7 @@ export function listShareLog(quoteId: number) {
     url: '/crm/quote/shareLog/list',
     method: 'get',
     params: { quoteId }
-  })
+  });
 }
 
 /**
@@ -445,6 +448,5 @@ export function shareLogStatistics(quoteId: number) {
     url: '/crm/quote/shareLog/statistics',
     method: 'get',
     params: { quoteId }
-  })
+  });
 }
-

+ 13 - 4
web/src/components/TinyMceEditor/index.vue

@@ -14,6 +14,7 @@ interface Props {
   height?: number;
   toolbar?: boolean;
   contractNo?: string; // 合同编号,用于提取供方编码
+  supplierCode?: string; // 供应商编码,直接传入(优先级高于从contractNo提取)
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -21,7 +22,8 @@ const props = withDefaults(defineProps<Props>(), {
   disabled: false,
   height: 600,
   toolbar: true,
-  contractNo: ''
+  contractNo: '',
+  supplierCode: ''
 });
 
 const emit = defineEmits(['update:modelValue']);
@@ -75,10 +77,17 @@ const getSupplierCode = (contractNo?: string): string => {
   return match && match[1] ? match[1] : 'SX';
 };
 
+// 获取供应商编码(优先使用直接传入的supplierCode,否则从contractNo提取)
+const getCurrentSupplierCode = computed(() => {
+  if (props.supplierCode) {
+    return props.supplierCode;
+  }
+  return getSupplierCode(props.contractNo);
+});
+
 // 判断是否为SX供方编码
 const isSXSupplier = computed(() => {
-  const supplierCode = getSupplierCode(props.contractNo);
-  return supplierCode === 'SX';
+  return getCurrentSupplierCode.value === 'SX';
 });
 
 // TinyMCE 配置
@@ -426,7 +435,7 @@ const editorInit = computed(() => {
 
         // 根据供方编码控制logo显示的函数
         const hideLogoIfNeeded = () => {
-          const supplierCode = getSupplierCode(props.contractNo);
+          const supplierCode = getCurrentSupplierCode.value;
           const isSX = supplierCode === 'SX';
 
           if (!isSX) {

+ 1 - 0
web/src/views/crm/quote/detail.vue

@@ -42,6 +42,7 @@
               </span>
             </el-descriptions-item>
             <el-descriptions-item label="负责人">{{ quote?.ownerUserName }}</el-descriptions-item>
+            <el-descriptions-item label="报价主体">{{ quote?.supplierName || '-' }}</el-descriptions-item>
             <el-descriptions-item label="创建时间">{{ parseTime(quote?.createTime) }}</el-descriptions-item>
           </el-descriptions>
 

+ 86 - 16
web/src/views/crm/quote/form.vue

@@ -17,20 +17,17 @@
         </el-divider>
         <div>
           <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="报价单编号" prop="quoteNo">
                 <el-input v-model="form.quoteNo" placeholder="系统自动生成" disabled />
               </el-form-item>
             </el-col>
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="报价主题" prop="quoteTitle">
                 <el-input v-model="form.quoteTitle" placeholder="可不填,系统将自动生成" maxlength="100" show-word-limit />
               </el-form-item>
             </el-col>
-          </el-row>
-
-          <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="客户名称" prop="customerId" required>
                 <el-input v-model="form.customerName" placeholder="请选择客户" readonly>
                   <template #append>
@@ -39,7 +36,7 @@
                 </el-input>
               </el-form-item>
             </el-col>
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="联系人" prop="contactId" required>
                 <el-input v-model="form.contactName" placeholder="请选择联系人" readonly :disabled="!form.customerId">
                   <template #append>
@@ -51,7 +48,7 @@
           </el-row>
 
           <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="关联商机" prop="opportunityId">
                 <el-input v-model="opportunityDisplayName" placeholder="请选择商机(可选)" readonly clearable>
                   <template #append>
@@ -60,7 +57,7 @@
                 </el-input>
               </el-form-item>
             </el-col>
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="负责人" prop="ownerUserId" required>
                 <el-input v-model="form.ownerUserName" placeholder="请选择负责人" readonly>
                   <template #append>
@@ -69,20 +66,27 @@
                 </el-input>
               </el-form-item>
             </el-col>
-          </el-row>
-
-          <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="报价日期" prop="quoteDate" required>
                 <el-date-picker v-model="form.quoteDate" type="date" placeholder="选择报价日期" value-format="YYYY-MM-DD" style="width: 100%" />
               </el-form-item>
             </el-col>
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="有效期(天)" prop="validityDays" required>
                 <el-input-number v-model="form.validityDays" :min="1" :max="365" style="width: 100%" />
               </el-form-item>
             </el-col>
           </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="6">
+              <el-form-item label="报价主体" prop="supplierId" required>
+                <el-select v-model="form.supplierId" placeholder="请选择报价主体" clearable style="width: 100%" @change="handleSupplierChange">
+                  <el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
         </div>
 
         <!-- 产品明细 -->
@@ -366,6 +370,8 @@ import OpportunitySelector from '@/components/OpportunitySelector.vue';
 import UserSelector from '@/components/UserSelector.vue';
 import { listUser } from '@/api/system/user';
 import type { UserVO } from '@/api/system/user/types';
+import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo';
+import { getContactByCustomerId } from '@/api/crm/customer';
 
 // 临时类型声明
 interface CustomerVO {
@@ -376,6 +382,7 @@ interface CustomerVO {
 interface ContactVO {
   id: number;
   contactName: string;
+  isPrimary?: number;
 }
 
 interface OpportunityVO {
@@ -411,6 +418,8 @@ const form = ref<QuoteForm>({
   validityDays: 30,
   ownerUserId: undefined,
   ownerUserName: '',
+  supplierId: undefined,
+  supplierName: '',
   remark: '1. 以上报价含税13%、含运费 ,款到发货 ,最终价格须盖章回传。\n2. 结算以实际到货数量为准,价格随行就市 。',
   products: []
 });
@@ -421,7 +430,8 @@ const rules = ref({
   contactId: [{ required: true, message: '请选择联系人', trigger: 'change' }],
   quoteDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }],
   validityDays: [{ required: true, message: '请输入有效期', trigger: 'blur' }],
-  ownerUserId: [{ required: true, message: '请选择负责人', trigger: 'change' }]
+  ownerUserId: [{ required: true, message: '请选择负责人', trigger: 'change' }],
+  supplierId: [{ required: true, message: '请选择报价主体', trigger: 'change' }]
 });
 
 // 显示用的变量
@@ -463,6 +473,9 @@ const submitFormRef = ref<FormInstance>();
 // 用户列表(审批人候选)
 const userList = ref<UserVO[]>([]);
 
+// 供方信息列表
+const supplierList = ref<SupplierInfoVO[]>([]);
+
 // 附件列表
 const attachmentList = ref<any[]>([]);
 
@@ -619,7 +632,7 @@ const handleSelectCustomer = () => {
 };
 
 /** 客户选择确认 */
-const handleCustomerConfirm = (customer: CustomerVO) => {
+const handleCustomerConfirm = async (customer: CustomerVO) => {
   if (customer) {
     form.value.customerId = customer.id;
     form.value.customerName = customer.customerName;
@@ -628,6 +641,24 @@ const handleCustomerConfirm = (customer: CustomerVO) => {
     form.value.contactName = '';
     form.value.opportunityId = undefined;
     opportunityDisplayName.value = '';
+
+    // 自动加载该客户的联系人并设置默认联系人
+    try {
+      const res = await getContactByCustomerId(customer.id);
+      const contacts = res.data || [];
+      if (contacts.length > 0) {
+        // 优先选择主要联系人,如果没有主要联系人,选择第一个
+        const primaryContact = contacts.find((c: ContactVO) => c.isPrimary === 1);
+        const defaultContact = primaryContact || contacts[0];
+        if (defaultContact) {
+          form.value.contactId = defaultContact.id;
+          form.value.contactName = defaultContact.contactName;
+        }
+      }
+    } catch (error) {
+      console.error('加载联系人列表失败:', error);
+      // 加载失败不影响客户选择,只是不自动填充联系人
+    }
   }
 };
 
@@ -694,6 +725,40 @@ const loadUserList = async () => {
   }
 };
 
+/** 加载供方信息列表 */
+const loadSupplierList = async () => {
+  try {
+    const res = await listAllSupplierInfo({ status: '0' }); // 只加载正常状态的供方
+    supplierList.value = res.data || [];
+
+    // 如果是新建报价单且还没有设置报价主体,则默认选择code为"SX"的供方
+    if (!form.value.id && !form.value.supplierId) {
+      const defaultSupplier = supplierList.value.find((s) => s.supplierCode === 'SX');
+      if (defaultSupplier) {
+        form.value.supplierId = defaultSupplier.id;
+        form.value.supplierName = defaultSupplier.supplierName || '';
+      }
+    }
+  } catch (error) {
+    console.error('加载供方信息列表失败:', error);
+    supplierList.value = [];
+  }
+};
+
+/** 报价主体变化处理 */
+const handleSupplierChange = (supplierId: number | undefined) => {
+  if (!supplierId) {
+    form.value.supplierId = undefined;
+    form.value.supplierName = '';
+    return;
+  }
+  const supplier = supplierList.value.find((s) => s.id === supplierId);
+  if (supplier) {
+    form.value.supplierId = supplier.id;
+    form.value.supplierName = supplier.supplierName || '';
+  }
+};
+
 /** 添加产品 */
 const handleAddProduct = () => {
   productSelectorVisible.value = true;
@@ -1156,6 +1221,11 @@ const handleBack = () => {
 };
 
 onMounted(() => {
+  // 加载供方信息列表
+  loadSupplierList();
+  // 加载用户列表(审批人候选)
+  loadUserList();
+
   const id = route.query.id as string;
   if (id) {
     // 直接使用字符串类型,避免大整数精度丢失

+ 106 - 52
web/src/views/crm/quote/index.vue

@@ -352,7 +352,7 @@
       append-to-body
       :close-on-click-modal="false"
     >
-      <TinyMceEditor v-model="quoteContent" :height="600" :disabled="isPreviewMode" :toolbar="!isPreviewMode" />
+      <TinyMceEditor v-model="quoteContent" :height="600" :disabled="isPreviewMode" :toolbar="!isPreviewMode" :supplier-code="currentSupplierCode" />
       <template #footer>
         <div class="dialog-footer">
           <el-button @click="generateQuoteDialog.visible = false">关 闭</el-button>
@@ -476,9 +476,7 @@
         <div class="detail-header">
           <span class="detail-title">{{ detailQuote?.quoteNo }} - {{ detailQuote?.quoteTitle }}</span>
           <div class="detail-actions">
-            <el-button type="success" icon="Document" @click="handleGenerateQuote(detailQuote)"
-              >生成报价单</el-button
-            >
+            <el-button type="success" icon="Document" @click="handleGenerateQuote(detailQuote)">生成报价单</el-button>
             <el-button type="default" icon="Edit" @click="handleDetailEdit" v-if="detailQuote?.status === 'draft'">修改</el-button>
             <el-button type="danger" icon="Delete" @click="handleDetailDelete" v-if="detailQuote?.status !== 'invalid'">删除</el-button>
             <el-button type="warning" icon="Check" @click="handleDetailSubmit" v-if="detailQuote?.status === 'draft'">提交审批</el-button>
@@ -512,6 +510,7 @@
                 </span>
               </el-descriptions-item>
               <el-descriptions-item label="负责人">{{ detailQuote?.ownerUserName }}</el-descriptions-item>
+              <el-descriptions-item label="报价主体">{{ detailQuote?.supplierName || '-' }}</el-descriptions-item>
               <el-descriptions-item label="创建时间">{{ parseTime(detailQuote?.createTime) }}</el-descriptions-item>
             </el-descriptions>
 
@@ -813,6 +812,7 @@ import request from '@/utils/request';
 import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
 import ImagePreview from '@/components/ImagePreview/index.vue';
 import CustomerDetail from '@/views/crm/customer/detail.vue';
+import { getSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo';
 import logoImg from '@/assets/images/logo.gif';
 import html2canvas from 'html2canvas';
 import jsPDF from 'jspdf';
@@ -1056,6 +1056,7 @@ const quoteContent = ref('');
 const isPreviewMode = ref(false);
 const pdfGenerating = ref(false);
 const currentQuoteId = ref<string | number>(); // 当前报价单ID,用于保存模板
+const currentSupplierCode = ref<string>('SX'); // 当前供应商编码,用于控制背景图片和Logo显示
 
 // 审批对话框
 const approvalDialog = reactive({
@@ -1529,7 +1530,7 @@ const getExpireDays = (expireDate: string): number => {
 };
 
 /** 生成报价单HTML模版 */
-const generateQuoteTemplate = (quote: QuoteVO): string => {
+const generateQuoteTemplate = (quote: QuoteVO, supplierInfo?: SupplierInfoVO): string => {
   // 辅助函数:安全地将值转换为数字并格式化
   const formatNumber = (value: any, decimals: number = 2): string => {
     const num = parseFloat(value || 0);
@@ -1566,6 +1567,17 @@ const generateQuoteTemplate = (quote: QuoteVO): string => {
     return `${year}/${month}/${day}`;
   };
 
+  // 获取供应商编码,默认为SX
+  const supplierCode = supplierInfo?.supplierCode || quote.supplierCode || 'SX';
+  // 获取供应商简称和名称
+  const supplierShortName = supplierInfo?.shortName || quote.supplierShortName || '';
+  const supplierName = supplierInfo?.supplierName || quote.supplierName || '安庆市山峡化工有限公司';
+
+  // 根据供应商编码决定是否显示Logo和背景图片
+  const showLogo = supplierCode === 'SX';
+  // 根据供应商编码决定标题
+  const quoteTitle = supplierCode === 'SX' ? '山峡销售报价单' : `${supplierShortName || supplierName}销售报价单`;
+
   // 构建产品明细表格行
   const productRows =
     quote.productList
@@ -1590,13 +1602,17 @@ const generateQuoteTemplate = (quote: QuoteVO): string => {
   // 注意:水印已在TinyMCE编辑器中统一设置,这里无需重复添加
   return `
     <div style="max-width: 794px; margin: 0 auto; padding: 20px; font-family: Arial, '微软雅黑', sans-serif; font-size: 13px;">
-      <!-- Logo和标题 -->
+      ${
+        showLogo
+          ? `<!-- Logo和标题 -->
         <div style="text-align: left; margin-bottom: 20px;">
           <img src="${logoImg}" alt="山峡化工" style="height: 30px; vertical-align: middle;" />
-        </div>
+        </div>`
+          : ''
+      }
 
         <!-- 标题 -->
-        <h1 style="text-align: center; font-size: 28px; margin-bottom: 40px; font-weight: bold;">山峡销售报价单</h1>
+        <h1 style="text-align: center; font-size: 28px; margin-bottom: 40px; font-weight: bold;">${quoteTitle}</h1>
 
         <!-- 报价单基本信息 -->
         <div style="margin-bottom: 30px; font-size: 14px; line-height: 1.2; display: flex; justify-content: center;">
@@ -1607,7 +1623,7 @@ const generateQuoteTemplate = (quote: QuoteVO): string => {
             </div>
             <div style="display: flex; justify-content: space-between; margin-bottom: 3px;">
               <div style="flex: 1; text-align: left;">需&nbsp;&nbsp;方:&nbsp;&nbsp;${quote.customerName}</div>
-              <div style="flex: 1; text-align: left;">供&nbsp;&nbsp;方:&nbsp;&nbsp;安庆市山峡化工有限公司</div>
+              <div style="flex: 1; text-align: left;">供&nbsp;&nbsp;方:&nbsp;&nbsp;${supplierName}</div>
             </div>
             <div style="display: flex; justify-content: space-between;">
               <div style="flex: 1; text-align: left;">联系人:&nbsp;&nbsp;${addSpaceBetweenChars(quote.contactName || '')}</div>
@@ -1708,6 +1724,23 @@ const handleGenerateQuote = async (row?: QuoteVO) => {
 
     console.log('报价单详情数据:', quoteDetail);
 
+    // 获取供应商详细信息
+    let supplierInfo: SupplierInfoVO | undefined = undefined;
+    if (quoteDetail.supplierId) {
+      try {
+        const supplierRes = await getSupplierInfo(quoteDetail.supplierId);
+        supplierInfo = supplierRes.data;
+        console.log('供应商信息:', supplierInfo);
+        // 设置当前供应商编码,用于控制TinyMCE编辑器的背景图片和Logo显示
+        currentSupplierCode.value = supplierInfo?.supplierCode || 'SX';
+      } catch (error) {
+        console.warn('获取供应商信息失败:', error);
+        currentSupplierCode.value = 'SX'; // 默认值
+      }
+    } else {
+      currentSupplierCode.value = 'SX'; // 默认值
+    }
+
     // 先尝试从数据库获取已保存的模板
     let htmlContent = '';
     try {
@@ -1720,13 +1753,13 @@ const handleGenerateQuote = async (row?: QuoteVO) => {
         console.log('✅ 从数据库加载模板成功,长度:', htmlContent.length);
       } else {
         // 如果没有保存的模板,则生成新的HTML内容
-        htmlContent = generateQuoteTemplate(quoteDetail);
+        htmlContent = generateQuoteTemplate(quoteDetail, supplierInfo);
         console.log('❌ 数据库无模板,生成新的HTML内容,长度:', htmlContent.length);
       }
     } catch (error) {
       // 如果获取模板失败,则生成新的HTML内容
       console.warn('⚠️ 获取模板失败,将生成新模板:', error);
-      htmlContent = generateQuoteTemplate(quoteDetail);
+      htmlContent = generateQuoteTemplate(quoteDetail, supplierInfo);
       console.log('生成新的HTML内容,长度:', htmlContent.length);
     }
 
@@ -1750,24 +1783,16 @@ const handleGenerateQuote = async (row?: QuoteVO) => {
 
 /** 打印报价单 */
 const handlePrint = () => {
-  // 获取完整的水印图片URL
+  // 根据供应商编码决定是否显示背景图片
+  const showBackground = currentSupplierCode.value === 'SX';
   const watermarkUrl = window.location.origin + '/watermask.jpg';
 
   // 创建打印窗口
   const printWindow = window.open('', '_blank');
   if (printWindow) {
-    printWindow.document.write(`
-      <!DOCTYPE html>
-      <html>
-      <head>
-        <meta charset="utf-8">
-        <title>打印报价单</title>
-        <style>
-          body {
-            margin: 0;
-            padding: 20px;
-            position: relative;
-          }
+    // 根据供应商编码生成不同的样式
+    const backgroundStyle = showBackground
+      ? `
           /* 水印层 - 使用完整URL */
           body::before {
             content: '';
@@ -1790,10 +1815,6 @@ const handlePrint = () => {
             z-index: 1;
           }
           @media print {
-            body {
-              margin: 0;
-              padding: 20px;
-            }
             /* 打印时也显示水印 */
             body::before {
               position: absolute;
@@ -1801,6 +1822,34 @@ const handlePrint = () => {
               -webkit-print-color-adjust: exact;
             }
           }
+        `
+      : `
+          /* 非SX编码时不显示背景图片 */
+          body > * {
+            position: relative;
+            z-index: 1;
+          }
+        `;
+
+    printWindow.document.write(`
+      <!DOCTYPE html>
+      <html>
+      <head>
+        <meta charset="utf-8">
+        <title>打印报价单</title>
+        <style>
+          body {
+            margin: 0;
+            padding: 20px;
+            position: relative;
+          }
+          ${backgroundStyle}
+          @media print {
+            body {
+              margin: 0;
+              padding: 20px;
+            }
+          }
         </style>
       </head>
       <body>
@@ -1887,6 +1936,9 @@ const handleDownloadHtml = () => {
 const handleGeneratePdf = async () => {
   pdfGenerating.value = true;
   try {
+    // 根据供应商编码决定是否显示背景图片
+    const showBackground = currentSupplierCode.value === 'SX';
+
     // 创建临时容器
     const tempDiv = document.createElement('div');
     tempDiv.style.position = 'absolute';
@@ -1896,36 +1948,38 @@ const handleGeneratePdf = async () => {
     tempDiv.style.background = 'white';
     tempDiv.style.padding = '20px';
 
-    // 创建带水印的HTML结构
-    const watermarkDiv = document.createElement('div');
-    watermarkDiv.style.position = 'relative';
-    watermarkDiv.style.width = '100%';
-
-    // 添加水印背景层
-    const watermarkBg = document.createElement('div');
-    watermarkBg.style.cssText = `
-      position: absolute;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-      background-image: url('/watermask.jpg');
-      background-repeat: no-repeat;
-      background-position: center center;
-      background-size: 50%;
-      opacity: 0.15;
-      z-index: 0;
-      pointer-events: none;
-    `;
+    // 创建HTML结构
+    const containerDiv = document.createElement('div');
+    containerDiv.style.position = 'relative';
+    containerDiv.style.width = '100%';
+
+    // 如果是SX编码,添加水印背景层
+    if (showBackground) {
+      const watermarkBg = document.createElement('div');
+      watermarkBg.style.cssText = `
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background-image: url('/watermask.jpg');
+        background-repeat: no-repeat;
+        background-position: center center;
+        background-size: 50%;
+        opacity: 0.15;
+        z-index: 0;
+        pointer-events: none;
+      `;
+      containerDiv.appendChild(watermarkBg);
+    }
 
     // 添加内容层
     const contentDiv = document.createElement('div');
     contentDiv.style.cssText = 'position: relative; z-index: 1;';
     contentDiv.innerHTML = quoteContent.value;
 
-    watermarkDiv.appendChild(watermarkBg);
-    watermarkDiv.appendChild(contentDiv);
-    tempDiv.appendChild(watermarkDiv);
+    containerDiv.appendChild(contentDiv);
+    tempDiv.appendChild(containerDiv);
     document.body.appendChild(tempDiv);
 
     // 使用html2canvas转换为canvas