feat(bid): 完成甲方报价模块全量功能开发
1. 新增甲方报价业务实体,继承基础实体类 2. 新增供应商报价明细查询接口,支持按供应商ID展开物料明细 3. 重构甲方报价关联逻辑,通过material_id精确关联物料表 4. 新增甲方报价历史统计、月度趋势、快速新建等服务功能 5. 完善菜单配置,修正甲方报价菜单结构,添加完整权限控制 6. 新增物料搜索自动补全功能,优化报价单详情页面 7. 在供应商详情页新增报价历史Tab页签,展示该供应商的所有报价物料明细 8. 在物料详情页新增甲方报价记录Tab页签,展示该物料的所有甲方报价历史 9. 新增数据库优化脚本,添加索引并修复历史数据关联
This commit is contained in:
@@ -8,6 +8,7 @@ import com.ruoyi.system.service.bid.IBizClientQuoteService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/bid/clientquote")
|
||||
@@ -41,4 +42,30 @@ public class BizClientQuoteController extends BaseController {
|
||||
public AjaxResult remove(@PathVariable Long quoteId) {
|
||||
return toAjax(service.deleteClientQuoteById(quoteId));
|
||||
}
|
||||
|
||||
// ========== 甲方报价历史 - 统计 ==========
|
||||
|
||||
@GetMapping("/statistics")
|
||||
public AjaxResult statistics(BizClientQuote query) {
|
||||
return success(service.selectClientQuoteStatistics(query));
|
||||
}
|
||||
|
||||
@GetMapping("/monthly-trend")
|
||||
public AjaxResult monthlyTrend() {
|
||||
return success(service.selectMonthlyTrend());
|
||||
}
|
||||
|
||||
// ========== 按物料ID查询报价历史 ==========
|
||||
|
||||
@GetMapping("/by-material/{materialId}")
|
||||
public AjaxResult byMaterial(@PathVariable Long materialId) {
|
||||
return success(service.selectClientQuotesByMaterialId(materialId));
|
||||
}
|
||||
|
||||
// ========== 基于历史报价快速新建 ==========
|
||||
|
||||
@PostMapping("/quick-create/{quoteId}")
|
||||
public AjaxResult quickCreate(@PathVariable Long quoteId) {
|
||||
return success(service.copyFromExisting(quoteId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.web.controller.bid;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -11,11 +12,13 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.system.domain.bid.BizQuotation;
|
||||
import com.ruoyi.system.service.bid.IBizQuotationService;
|
||||
import com.ruoyi.system.mapper.bid.BizQuotationItemMapper;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/bid/quotation")
|
||||
public class BizQuotationController extends BaseController {
|
||||
@Autowired private IBizQuotationService service;
|
||||
@Autowired private BizQuotationItemMapper quotationItemMapper;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:quotation:list')")
|
||||
@GetMapping("/list")
|
||||
@@ -70,4 +73,15 @@ public class BizQuotationController extends BaseController {
|
||||
public AjaxResult remove(@PathVariable Long[] quotationIds) {
|
||||
return toAjax(service.deleteBizQuotationByIds(quotationIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按供应商ID查询报价明细(展开为每行一条物料)
|
||||
* 用于供应商管理页面的"报价历史"Tab
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('bid:quotation:list')")
|
||||
@GetMapping("/supplier-items/{supplierId}")
|
||||
public AjaxResult supplierItems(@PathVariable Long supplierId) {
|
||||
List<Map<String, Object>> list = quotationItemMapper.selectItemsBySupplierId(supplierId);
|
||||
return success(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.ruoyi.system.domain.bid;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class BizClientQuote {
|
||||
public class BizClientQuote extends BaseEntity {
|
||||
private Long quoteId;
|
||||
private Long tenantId;
|
||||
private String quoteNo;
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.math.BigDecimal;
|
||||
public class BizClientQuoteItem {
|
||||
private Long itemId;
|
||||
private Long quoteId;
|
||||
private Long materialId;
|
||||
private String materialName;
|
||||
private String spec;
|
||||
private String modelNo;
|
||||
@@ -20,6 +21,8 @@ public class BizClientQuoteItem {
|
||||
public void setItemId(Long v){itemId=v;}
|
||||
public Long getQuoteId(){return quoteId;}
|
||||
public void setQuoteId(Long v){quoteId=v;}
|
||||
public Long getMaterialId(){return materialId;}
|
||||
public void setMaterialId(Long v){materialId=v;}
|
||||
public String getMaterialName(){return materialName;}
|
||||
public void setMaterialName(String v){materialName=v;}
|
||||
public String getSpec(){return spec;}
|
||||
|
||||
@@ -4,8 +4,10 @@ import com.ruoyi.system.domain.bid.BizClientQuote;
|
||||
import com.ruoyi.system.domain.bid.BizClientQuoteItem;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BizClientQuoteMapper {
|
||||
// ========== CRUD ==========
|
||||
List<BizClientQuote> selectClientQuoteList(@Param("q") BizClientQuote query);
|
||||
BizClientQuote selectClientQuoteById(@Param("quoteId") Long quoteId);
|
||||
List<BizClientQuoteItem> selectItemsByQuoteId(@Param("quoteId") Long quoteId);
|
||||
@@ -15,4 +17,14 @@ public interface BizClientQuoteMapper {
|
||||
int deleteClientQuoteById(@Param("quoteId") Long quoteId);
|
||||
int deleteItemsByQuoteId(@Param("quoteId") Long quoteId);
|
||||
String selectNextQuoteNo();
|
||||
|
||||
// ========== 甲方报价历史 - 统计 ==========
|
||||
Map<String, Object> selectClientQuoteStatistics(@Param("q") BizClientQuote query);
|
||||
List<Map<String, Object>> selectMonthlyTrend();
|
||||
|
||||
// ========== 按物料ID关联查询 ==========
|
||||
List<Map<String, Object>> selectClientQuotesByMaterialId(@Param("materialId") Long materialId);
|
||||
|
||||
// ========== 快速复制 ==========
|
||||
List<BizClientQuoteItem> selectItemsByQuoteIdForCopy(@Param("quoteId") Long quoteId);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.ruoyi.system.mapper.bid;
|
||||
|
||||
import com.ruoyi.system.domain.bid.BizQuotationItem;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BizQuotationItemMapper {
|
||||
List<BizQuotationItem> selectItemsByQuotationId(Long quotationId);
|
||||
List<Map<String, Object>> selectItemsBySupplierId(@Param("supplierId") Long supplierId);
|
||||
int insertBizQuotationItem(BizQuotationItem item);
|
||||
int deleteByQuotationId(Long quotationId);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.system.service.bid;
|
||||
|
||||
import com.ruoyi.system.domain.bid.BizClientQuote;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IBizClientQuoteService {
|
||||
List<BizClientQuote> selectClientQuoteList(BizClientQuote query);
|
||||
@@ -9,4 +10,14 @@ public interface IBizClientQuoteService {
|
||||
int insertClientQuote(BizClientQuote quote);
|
||||
int updateClientQuote(BizClientQuote quote);
|
||||
int deleteClientQuoteById(Long quoteId);
|
||||
|
||||
// 甲方报价历史 - 统计
|
||||
Map<String, Object> selectClientQuoteStatistics(BizClientQuote query);
|
||||
List<Map<String, Object>> selectMonthlyTrend();
|
||||
|
||||
// 按物料ID查询报价历史
|
||||
List<Map<String, Object>> selectClientQuotesByMaterialId(Long materialId);
|
||||
|
||||
// 基于历史报价快速新建
|
||||
BizClientQuote copyFromExisting(Long quoteId);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class BizClientQuoteServiceImpl implements IBizClientQuoteService {
|
||||
@@ -85,4 +86,70 @@ public class BizClientQuoteServiceImpl implements IBizClientQuoteService {
|
||||
mapper.deleteItemsByQuoteId(quoteId);
|
||||
return mapper.deleteClientQuoteById(quoteId);
|
||||
}
|
||||
|
||||
// ========== 甲方报价历史 - 统计 ==========
|
||||
|
||||
@Override
|
||||
public Map<String, Object> selectClientQuoteStatistics(BizClientQuote query) {
|
||||
return mapper.selectClientQuoteStatistics(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> selectMonthlyTrend() {
|
||||
return mapper.selectMonthlyTrend();
|
||||
}
|
||||
|
||||
// ========== 按物料ID查询 ==========
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> selectClientQuotesByMaterialId(Long materialId) {
|
||||
return mapper.selectClientQuotesByMaterialId(materialId);
|
||||
}
|
||||
|
||||
// ========== 基于历史报价快速新建 ==========
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public BizClientQuote copyFromExisting(Long quoteId) {
|
||||
BizClientQuote existing = mapper.selectClientQuoteById(quoteId);
|
||||
if (existing == null) {
|
||||
throw new RuntimeException("报价单不存在: " + quoteId);
|
||||
}
|
||||
// 创建新的报价单(复制主表信息,重置状态和金额)
|
||||
BizClientQuote newQuote = new BizClientQuote();
|
||||
newQuote.setQuoteNo(mapper.selectNextQuoteNo());
|
||||
newQuote.setClientName(existing.getClientName());
|
||||
newQuote.setRfqId(existing.getRfqId());
|
||||
newQuote.setRfqNo(existing.getRfqNo());
|
||||
newQuote.setRfqTitle(existing.getRfqTitle());
|
||||
newQuote.setStatus("draft");
|
||||
newQuote.setValidityDate(existing.getValidityDate());
|
||||
newQuote.setCurrency(existing.getCurrency());
|
||||
newQuote.setRemark(existing.getRemark());
|
||||
newQuote.setTotalAmount(BigDecimal.ZERO);
|
||||
|
||||
int rows = mapper.insertClientQuote(newQuote);
|
||||
|
||||
// 复制明细
|
||||
List<BizClientQuoteItem> items = mapper.selectItemsByQuoteId(quoteId);
|
||||
if (items != null && !items.isEmpty()) {
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (BizClientQuoteItem item : items) {
|
||||
// 重置为新行
|
||||
item.setItemId(null);
|
||||
item.setQuoteId(newQuote.getQuoteId());
|
||||
// 保留原物料信息和价格,方便用户修改
|
||||
if (item.getUnitPrice() != null && item.getQuantity() != null) {
|
||||
BigDecimal line = item.getUnitPrice().multiply(item.getQuantity()).setScale(2, RoundingMode.HALF_UP);
|
||||
item.setTotalPrice(line);
|
||||
total = total.add(line);
|
||||
}
|
||||
mapper.insertClientQuoteItem(item);
|
||||
}
|
||||
newQuote.setTotalAmount(total);
|
||||
mapper.updateClientQuote(newQuote);
|
||||
}
|
||||
newQuote.setItems(items);
|
||||
return newQuote;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,48 @@
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.system.mapper.bid.BizClientQuoteMapper">
|
||||
|
||||
<!-- ========== 甲方报价主表操作 ========== -->
|
||||
|
||||
<sql id="quoteColumns">
|
||||
quote_id AS quoteId, tenant_id AS tenantId, quote_no AS quoteNo,
|
||||
client_name AS clientName, rfq_id AS rfqId, rfq_no AS rfqNo, rfq_title AS rfqTitle,
|
||||
status, validity_date AS validityDate, total_amount AS totalAmount,
|
||||
currency, remark, create_by AS createBy, create_time AS createTime
|
||||
</sql>
|
||||
|
||||
<select id="selectClientQuoteList" resultType="com.ruoyi.system.domain.bid.BizClientQuote">
|
||||
SELECT quote_id AS quoteId, tenant_id AS tenantId, quote_no AS quoteNo,
|
||||
client_name AS clientName, rfq_id AS rfqId, rfq_no AS rfqNo, rfq_title AS rfqTitle,
|
||||
status, validity_date AS validityDate, total_amount AS totalAmount,
|
||||
currency, remark, create_by AS createBy, create_time AS createTime
|
||||
SELECT <include refid="quoteColumns"/>
|
||||
FROM biz_client_quote
|
||||
<where>
|
||||
<if test="q.clientName != null and q.clientName != ''">AND client_name LIKE CONCAT('%',#{q.clientName},'%')</if>
|
||||
<if test="q.status != null and q.status != ''">AND status = #{q.status}</if>
|
||||
<if test="q.quoteNo != null and q.quoteNo != ''">AND quote_no LIKE CONCAT('%',#{q.quoteNo},'%')</if>
|
||||
<if test="q.params.beginTime != null and q.params.beginTime != ''">
|
||||
AND create_time >= #{q.params.beginTime}
|
||||
</if>
|
||||
<if test="q.params.endTime != null and q.params.endTime != ''">
|
||||
AND create_time <= #{q.params.endTime}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectClientQuoteById" resultType="com.ruoyi.system.domain.bid.BizClientQuote">
|
||||
SELECT quote_id AS quoteId, tenant_id AS tenantId, quote_no AS quoteNo,
|
||||
client_name AS clientName, rfq_id AS rfqId, rfq_no AS rfqNo, rfq_title AS rfqTitle,
|
||||
status, validity_date AS validityDate, total_amount AS totalAmount,
|
||||
currency, remark, create_by AS createBy, create_time AS createTime
|
||||
SELECT <include refid="quoteColumns"/>
|
||||
FROM biz_client_quote WHERE quote_id = #{quoteId}
|
||||
</select>
|
||||
|
||||
<!-- ========== 甲方报价明细操作 ========== -->
|
||||
|
||||
<sql id="itemColumns">
|
||||
item_id AS itemId, quote_id AS quoteId, material_id AS materialId,
|
||||
material_name AS materialName, spec, model_no AS modelNo, unit, quantity,
|
||||
cost_price AS costPrice, unit_price AS unitPrice, total_price AS totalPrice,
|
||||
delivery_days AS deliveryDays, remark
|
||||
</sql>
|
||||
|
||||
<select id="selectItemsByQuoteId" resultType="com.ruoyi.system.domain.bid.BizClientQuoteItem">
|
||||
SELECT item_id AS itemId, quote_id AS quoteId, material_name AS materialName,
|
||||
spec, model_no AS modelNo, unit, quantity, cost_price AS costPrice,
|
||||
unit_price AS unitPrice, total_price AS totalPrice, delivery_days AS deliveryDays, remark
|
||||
SELECT <include refid="itemColumns"/>
|
||||
FROM biz_client_quote_item WHERE quote_id = #{quoteId} ORDER BY item_id
|
||||
</select>
|
||||
|
||||
@@ -44,9 +61,9 @@
|
||||
</insert>
|
||||
|
||||
<insert id="insertClientQuoteItem" useGeneratedKeys="true" keyProperty="itemId">
|
||||
INSERT INTO biz_client_quote_item (quote_id,material_name,spec,model_no,unit,quantity,
|
||||
INSERT INTO biz_client_quote_item (quote_id,material_id,material_name,spec,model_no,unit,quantity,
|
||||
cost_price,unit_price,total_price,delivery_days,remark)
|
||||
VALUES (#{quoteId},#{materialName},#{spec},#{modelNo},#{unit},#{quantity},
|
||||
VALUES (#{quoteId},#{materialId},#{materialName},#{spec},#{modelNo},#{unit},#{quantity},
|
||||
#{costPrice},#{unitPrice},#{totalPrice},#{deliveryDays},#{remark})
|
||||
</insert>
|
||||
|
||||
@@ -59,4 +76,63 @@
|
||||
|
||||
<delete id="deleteClientQuoteById">DELETE FROM biz_client_quote WHERE quote_id=#{quoteId}</delete>
|
||||
<delete id="deleteItemsByQuoteId">DELETE FROM biz_client_quote_item WHERE quote_id=#{quoteId}</delete>
|
||||
|
||||
<!-- ========== 甲方报价历史 - 统计查询 ========== -->
|
||||
|
||||
<select id="selectClientQuoteStatistics" resultType="java.util.Map">
|
||||
SELECT
|
||||
COUNT(*) AS total_count,
|
||||
COUNT(DISTINCT client_name) AS client_count,
|
||||
COALESCE(SUM(total_amount), 0) AS total_amount_sum,
|
||||
MAX(total_amount) AS max_amount,
|
||||
AVG(total_amount) AS avg_amount
|
||||
FROM biz_client_quote
|
||||
<where>
|
||||
<if test="q.clientName != null and q.clientName != ''">AND client_name LIKE CONCAT('%',#{q.clientName},'%')</if>
|
||||
<if test="q.status != null and q.status != ''">AND status = #{q.status}</if>
|
||||
<if test="q.params.beginTime != null and q.params.beginTime != ''">
|
||||
AND create_time >= #{q.params.beginTime}
|
||||
</if>
|
||||
<if test="q.params.endTime != null and q.params.endTime != ''">
|
||||
AND create_time <= #{q.params.endTime}
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- ========== 按物料ID查询甲方报价历史(使用material_id精确关联) ========== -->
|
||||
|
||||
<select id="selectClientQuotesByMaterialId" resultType="java.util.Map">
|
||||
SELECT cqi.item_id AS itemId, cqi.spec, cqi.model_no AS modelNo, cqi.unit, cqi.quantity,
|
||||
cqi.cost_price AS costPrice, cqi.unit_price AS unitPrice, cqi.total_price AS totalPrice,
|
||||
cqi.delivery_days AS deliveryDays, cqi.material_id AS materialId,
|
||||
cqi.material_name AS materialName,
|
||||
cq.quote_id AS quoteId, cq.quote_no AS quoteNo, cq.client_name AS clientName,
|
||||
cq.status AS quoteStatus, cq.create_time AS createTime
|
||||
FROM biz_client_quote_item cqi
|
||||
JOIN biz_client_quote cq ON cqi.quote_id = cq.quote_id
|
||||
WHERE cqi.material_id = #{materialId}
|
||||
ORDER BY cq.create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- ========== 快速创建 - 复制历史报价单的明细(按报价单ID) ========== -->
|
||||
|
||||
<select id="selectItemsByQuoteIdForCopy" resultType="com.ruoyi.system.domain.bid.BizClientQuoteItem">
|
||||
SELECT <include refid="itemColumns"/>
|
||||
FROM biz_client_quote_item WHERE quote_id = #{quoteId}
|
||||
ORDER BY item_id
|
||||
</select>
|
||||
|
||||
<!-- ========== 甲方报价历史 - 按日期统计趋势 ========== -->
|
||||
|
||||
<select id="selectMonthlyTrend" resultType="java.util.Map">
|
||||
SELECT
|
||||
DATE_FORMAT(create_time, '%Y-%m') AS month,
|
||||
COUNT(*) AS quote_count,
|
||||
COALESCE(SUM(total_amount), 0) AS amount_sum
|
||||
FROM biz_client_quote
|
||||
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
GROUP BY DATE_FORMAT(create_time, '%Y-%m')
|
||||
ORDER BY month
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -92,14 +92,14 @@
|
||||
ORDER BY q.submit_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 甲方报价历史:通过物料名称关联 -->
|
||||
<!-- 甲方报价历史:通过material_id精确关联 -->
|
||||
<select id="selectClientQuotesByMaterialId" resultType="java.util.Map">
|
||||
SELECT cqi.item_id, cqi.spec, cqi.model_no, cqi.unit, cqi.quantity,
|
||||
cqi.cost_price, cqi.unit_price, cqi.total_price, cqi.delivery_days,
|
||||
cq.quote_id, cq.quote_no, cq.client_name, cq.status AS quote_status, cq.create_time
|
||||
FROM biz_client_quote_item cqi
|
||||
JOIN biz_client_quote cq ON cqi.quote_id = cq.quote_id
|
||||
WHERE cqi.material_name = (SELECT material_name FROM biz_material WHERE material_id = #{materialId})
|
||||
WHERE cqi.material_id = #{materialId}
|
||||
ORDER BY cq.create_time DESC
|
||||
</select>
|
||||
|
||||
|
||||
@@ -19,6 +19,23 @@
|
||||
SELECT * FROM biz_quotation_item WHERE quotation_id=#{quotationId}
|
||||
</select>
|
||||
|
||||
<!-- 按供应商ID查询报价明细(展开为每行一条物料) -->
|
||||
<select id="selectItemsBySupplierId" resultType="java.util.Map">
|
||||
SELECT qi.item_id AS itemId, qi.quotation_id AS quotationId,
|
||||
qi.material_name AS materialName, qi.spec, qi.unit,
|
||||
qi.quantity, qi.unit_price AS unitPrice, qi.total_price AS totalPrice,
|
||||
qi.delivery_days AS deliveryDays,
|
||||
ri.material_id AS materialId,
|
||||
q.quote_no AS quoteNo, q.submit_time AS submitTime,
|
||||
q.status AS quoteStatus, q.total_amount AS totalAmount,
|
||||
q.valid_days AS validDays
|
||||
FROM biz_quotation_item qi
|
||||
JOIN biz_quotation q ON qi.quotation_id = q.quotation_id
|
||||
LEFT JOIN biz_rfq_item ri ON qi.rfq_item_id = ri.item_id
|
||||
WHERE q.supplier_id = #{supplierId}
|
||||
ORDER BY q.submit_time DESC, qi.item_id
|
||||
</select>
|
||||
|
||||
<insert id="insertBizQuotationItem" useGeneratedKeys="true" keyProperty="itemId">
|
||||
INSERT INTO biz_quotation_item(quotation_id,rfq_item_id,material_name,spec,unit,quantity,unit_price,total_price,delivery_days,remark)
|
||||
VALUES(#{quotationId},#{rfqItemId},#{materialName},#{spec},#{unit},#{quantity},#{unitPrice},#{totalPrice},#{deliveryDays},#{remark})
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import request from '@/utils/request'
|
||||
const baseUrl = '/bid/clientquote'
|
||||
|
||||
// ========== CRUD ==========
|
||||
export const listClientQuote = (params) => request({ url: baseUrl + '/list', method: 'get', params })
|
||||
export const getClientQuote = (id) => request({ url: baseUrl + '/' + id, method: 'get' })
|
||||
export const addClientQuote = (data) => request({ url: baseUrl, method: 'post', data })
|
||||
export const updateClientQuote = (data) => request({ url: baseUrl, method: 'put', data })
|
||||
export const delClientQuote = (id) => request({ url: baseUrl + '/' + id, method: 'delete' })
|
||||
|
||||
// ========== 甲方报价历史 ==========
|
||||
// 统计数据
|
||||
export const getClientQuoteStatistics = (params) => request({ url: baseUrl + '/statistics', method: 'get', params })
|
||||
// 月度趋势
|
||||
export const getMonthlyTrend = () => request({ url: baseUrl + '/monthly-trend', method: 'get' })
|
||||
// 按物料ID查询报价历史
|
||||
export const getClientQuoteByMaterial = (materialId) => request({ url: baseUrl + '/by-material/' + materialId, method: 'get' })
|
||||
// 基于历史报价快速新建
|
||||
export const quickCreateFromQuote = (quoteId) => request({ url: baseUrl + '/quick-create/' + quoteId, method: 'post' })
|
||||
|
||||
@@ -8,3 +8,6 @@ export const submitQuotation = (id) => request({ url: baseUrl + '/submit/' + id,
|
||||
export const acceptQuotation = (id) => request({ url: baseUrl + '/accept/' + id, method: 'put' })
|
||||
export const rejectQuotation = (id) => request({ url: baseUrl + '/reject/' + id, method: 'put' })
|
||||
export const delQuotation = (ids) => request({ url: baseUrl + '/' + ids, method: 'delete' })
|
||||
|
||||
// 按供应商ID查询报价明细(展开为每行一条物料)
|
||||
export const getSupplierQuoteItems = (supplierId) => request({ url: baseUrl + '/supplier-items/' + supplierId, method: 'get' })
|
||||
|
||||
@@ -112,6 +112,19 @@ export const dynamicRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/quotemgr/clientquote/detail',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/bid/clientquote/detail'),
|
||||
name: 'ClientQuoteDetail',
|
||||
meta: { title: '甲方报价单详情', activeMenu: '/quotemgr/clientquote' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/user-auth',
|
||||
component: Layout,
|
||||
|
||||
@@ -72,9 +72,36 @@
|
||||
</div>
|
||||
<el-table :data="form.items" border size="small">
|
||||
<el-table-column type="index" width="46" label="#" />
|
||||
<el-table-column label="物料名称" min-width="150">
|
||||
<el-table-column label="物料名称" min-width="200">
|
||||
<template slot-scope="s">
|
||||
<el-input v-model="s.row.materialName" size="mini" placeholder="物料名称" />
|
||||
<el-autocomplete
|
||||
v-model="s.row.materialName"
|
||||
:fetch-suggestions="queryMaterialSearch"
|
||||
placeholder="搜索选择物料"
|
||||
size="mini"
|
||||
style="width:100%"
|
||||
popper-class="material-popper"
|
||||
:popper-append-to-body="true"
|
||||
@select="(item) => onMaterialSelect(s.row, item)"
|
||||
@change="() => onMaterialChange(s.row)"
|
||||
>
|
||||
<template slot-scope="{ item }">
|
||||
<div class="material-suggestion">
|
||||
<div class="ms-name">{{ item.materialName }}</div>
|
||||
<div class="ms-detail">
|
||||
<div v-if="item.spec" class="ms-detail-row">
|
||||
<span class="ms-label">规格:</span>
|
||||
<span class="ms-spec">{{ item.spec }}</span>
|
||||
</div>
|
||||
<div class="ms-detail-row">
|
||||
<span v-if="item.brand" class="ms-brand">{{ item.brand }}</span>
|
||||
<span v-if="item.unit" class="ms-unit">单位: {{ item.unit }}</span>
|
||||
<span v-if="item.categoryName" class="ms-category">{{ item.categoryName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" width="120">
|
||||
@@ -92,22 +119,22 @@
|
||||
<el-input v-model="s.row.unit" size="mini" placeholder="单位" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="80">
|
||||
<el-table-column label="数量" width="130">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.quantity" :min="0" size="mini" controls-position="right" style="width:100%" @change="calcRow(s.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成本价(元)" width="110">
|
||||
<el-table-column label="成本价(元)" width="140">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.costPrice" :min="0" :precision="2" size="mini" controls-position="right" style="width:100%" @change="calcRow(s.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价(元)" width="110">
|
||||
<el-table-column label="报价(元)" width="140">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.unitPrice" :min="0" :precision="2" size="mini" controls-position="right" style="width:100%" @change="calcRow(s.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额(元)" width="110" align="right">
|
||||
<el-table-column label="金额(元)" width="120" align="right">
|
||||
<template slot-scope="s">
|
||||
<strong style="color:#409EFF">¥{{ s.row.totalPrice || '0.00' }}</strong>
|
||||
</template>
|
||||
@@ -200,6 +227,7 @@
|
||||
<script>
|
||||
import { getClientQuote, addClientQuote, updateClientQuote } from "@/api/bid/clientquote";
|
||||
import { listRfq } from "@/api/bid/rfq";
|
||||
import { listMaterial } from "@/api/bid/material";
|
||||
import logoImg from "@/assets/logo/logo.png";
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
@@ -218,6 +246,7 @@ export default {
|
||||
rfqOptions: [],
|
||||
logoSrc: logoImg,
|
||||
today: new Date().toLocaleDateString("zh-CN"),
|
||||
materialCache: [],
|
||||
form: {
|
||||
quoteId: null, quoteNo: "", clientName: "", rfqId: null, rfqNo: "", rfqTitle: "",
|
||||
status: "draft", validityDate: "", currency: "CNY", remark: "", totalAmount: 0,
|
||||
@@ -235,6 +264,14 @@ export default {
|
||||
const qid = this.$route.query.quoteId;
|
||||
if (qid && qid !== "__new__") {
|
||||
this.loadData(qid);
|
||||
// Auto-export PDF if triggered from history page
|
||||
if (this.$route.query.exportPdf === '1') {
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (this.form.quoteId) this.exportPdf();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.isNew = true;
|
||||
this.form.items = [];
|
||||
@@ -253,8 +290,36 @@ export default {
|
||||
const rfq = this.rfqOptions.find(r => r.rfqId === rfqId);
|
||||
if (rfq) { this.form.rfqNo = rfq.rfqNo; this.form.rfqTitle = rfq.rfqTitle; }
|
||||
},
|
||||
// 物料搜索(自动补全)
|
||||
queryMaterialSearch(query, cb) {
|
||||
if (!query || query.length < 1) {
|
||||
cb(this.materialCache.slice(0, 20));
|
||||
return;
|
||||
}
|
||||
listMaterial({ materialName: query, pageSize: 20 }).then(res => {
|
||||
const list = res.rows || [];
|
||||
this.materialCache = list.map(m => ({
|
||||
...m,
|
||||
value: m.materialName + (m.spec ? ' (' + m.spec + ')' : '')
|
||||
}));
|
||||
cb(this.materialCache.slice(0, 20));
|
||||
}).catch(() => cb([]));
|
||||
},
|
||||
onMaterialSelect(row, item) {
|
||||
if (!item) return;
|
||||
row.materialId = item.materialId;
|
||||
row.materialName = item.materialName;
|
||||
row.spec = item.spec || '';
|
||||
row.modelNo = item.modelNo || item.spec || '';
|
||||
row.unit = item.unit || '件';
|
||||
},
|
||||
onMaterialChange(row) {
|
||||
// If user manually changed material name without selecting from list, clear materialId
|
||||
if (!row.materialId) return;
|
||||
// We keep the materialId even if name changes, it'll be refreshed on save
|
||||
},
|
||||
addItem() {
|
||||
this.form.items.push({ materialName: "", spec: "", modelNo: "", unit: "件", quantity: 1, costPrice: 0, unitPrice: 0, totalPrice: "0.00", deliveryDays: null });
|
||||
this.form.items.push({ materialId: null, materialName: "", spec: "", modelNo: "", unit: "件", quantity: 1, costPrice: 0, unitPrice: 0, totalPrice: "0.00", deliveryDays: null });
|
||||
},
|
||||
removeItem(idx) { this.form.items.splice(idx, 1); },
|
||||
calcRow(row) {
|
||||
@@ -325,6 +390,84 @@ export default {
|
||||
.page-title { font-size:18px; font-weight:700; color:#1a2c4e; }
|
||||
.info-card ::v-deep .el-card__body { padding: 20px; }
|
||||
.total-bar { display:flex; align-items:center; justify-content:flex-end; padding:16px 20px; border-top:1px solid #f0f2f5; background:#fafbfc; }
|
||||
|
||||
/* 物料搜索建议下拉 */
|
||||
/* 物料搜索下拉样式 - 加宽并优化展示 */
|
||||
>>> .el-autocomplete-suggestion {
|
||||
min-width: 400px !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
>>> .el-autocomplete-suggestion__wrap {
|
||||
max-height: 400px !important;
|
||||
}
|
||||
|
||||
>>> .el-autocomplete-suggestion__list {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
>>> .el-autocomplete-suggestion__list li {
|
||||
padding: 8px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.material-suggestion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.ms-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ms-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ms-detail-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ms-spec {
|
||||
color: #606266;
|
||||
background: #f5f7fa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ms-brand {
|
||||
color: #409EFF;
|
||||
background: #ecf5ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ms-unit {
|
||||
color: #909399;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.ms-category {
|
||||
color: #67c23a;
|
||||
font-size: 11px;
|
||||
background: #f0f9eb;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
/* PDF */
|
||||
.pdf-area { padding:28px; font-family:"Microsoft YaHei","Noto Sans SC",Arial,sans-serif; font-size:13px; color:#222; }
|
||||
.pdf-header { display:flex; align-items:flex-start; padding-bottom:12px; }
|
||||
|
||||
352
ruoyi-ui/src/views/bid/clientquote/history/index.vue
Normal file
352
ruoyi-ui/src/views/bid/clientquote/history/index.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- ========== 统计卡片 ========== -->
|
||||
<el-row :gutter="20" class="stat-row">
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-card-total">
|
||||
<div class="stat-icon"><i class="el-icon-document-copy"></i></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ stats.total_count || 0 }}</div>
|
||||
<div class="stat-label">报价单总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-card-client">
|
||||
<div class="stat-icon"><i class="el-icon-user"></i></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ stats.client_count || 0 }}</div>
|
||||
<div class="stat-label">客户数量</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-card-amount">
|
||||
<div class="stat-icon"><i class="el-icon-money"></i></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">¥{{ (stats.total_amount_sum || 0) | money }}</div>
|
||||
<div class="stat-label">报价总金额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-card-avg">
|
||||
<div class="stat-icon"><i class="el-icon-s-data"></i></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">¥{{ (stats.avg_amount || 0) | money }}</div>
|
||||
<div class="stat-label">平均金额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- ========== 搜索筛选 ========== -->
|
||||
<el-card shadow="never" class="search-card">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
|
||||
<el-form-item label="客户名称" prop="clientName">
|
||||
<el-input v-model="queryParams.clientName" placeholder="请输入客户名称" clearable
|
||||
@keyup.enter.native="handleQuery" style="width:180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:110px">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已发送" value="sent" />
|
||||
<el-option label="已确认" value="confirmed" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建日期">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
|
||||
end-placeholder="结束日期" value-format="yyyy-MM-dd" style="width:240px" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- ========== 操作栏 ========== -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新建报价单</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button plain icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- ========== 报价单表格 ========== -->
|
||||
<el-table v-loading="loading" :data="quoteList" border stripe highlight-current-row>
|
||||
<el-table-column label="报价单号" prop="quoteNo" width="170" fixed>
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" size="small" style="font-weight:600" @click="goDetail(s.row)">
|
||||
{{ s.row.quoteNo }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户名称" prop="clientName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="关联询价单" prop="rfqTitle" min-width="150" show-overflow-tooltip>
|
||||
<template slot-scope="s">{{ s.row.rfqNo || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总金额" width="130" align="right">
|
||||
<template slot-scope="s">
|
||||
<strong style="color:#409EFF">¥{{ s.row.totalAmount | money }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="币种" prop="currency" width="70" align="center" />
|
||||
<el-table-column label="有效期" width="110" align="center">
|
||||
<template slot-scope="s">{{ s.row.validityDate | date }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-tag :type="statusType(s.row.status)" size="mini" effect="dark">
|
||||
{{ statusLabel(s.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建人" prop="createBy" width="100" align="center" />
|
||||
<el-table-column label="创建时间" prop="createTime" width="160" align="center" />
|
||||
<el-table-column label="操作" align="center" width="220" fixed="right">
|
||||
<template slot-scope="s">
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="goDetail(s.row)">详情</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-download" @click="exportPdf(s.row)">PDF</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-document-copy" @click="handleQuickCreate(s.row)">快速新建</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(s.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<!-- ========== 报价单详情弹窗 ========== -->
|
||||
<el-dialog :title="'报价单详情 - ' + (detailForm.quoteNo || '')" :visible.sync="detailVisible" width="900px"
|
||||
top="5vh" class="detail-dialog" :close-on-click-modal="false">
|
||||
<div v-loading="detailLoading">
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions :column="3" border size="small" class="detail-desc">
|
||||
<el-descriptions-item label="报价单号">{{ detailForm.quoteNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">{{ detailForm.clientName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusType(detailForm.status)" size="mini">{{ statusLabel(detailForm.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="币种">{{ detailForm.currency || 'CNY' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">{{ detailForm.validityDate | date }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联询价单">{{ detailForm.rfqNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总金额" :span="3">
|
||||
<strong style="color:#409EFF;font-size:16px">¥{{ detailForm.totalAmount | money }}</strong>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="3">{{ detailForm.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 明细表格 -->
|
||||
<div style="margin-top:16px;font-weight:600;font-size:14px;color:#303133;padding:8px 0;border-bottom:2px solid #409EFF">
|
||||
报价明细
|
||||
</div>
|
||||
<el-table :data="detailForm.items || []" border size="small" style="margin-top:12px">
|
||||
<el-table-column type="index" width="46" label="#" />
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="140" />
|
||||
<el-table-column label="规格" prop="spec" width="120" />
|
||||
<el-table-column label="型号" prop="modelNo" width="110" />
|
||||
<el-table-column label="单位" prop="unit" width="60" align="center" />
|
||||
<el-table-column label="数量" prop="quantity" width="80" align="right" />
|
||||
<el-table-column label="成本价" width="100" align="right">
|
||||
<template slot-scope="s">¥{{ s.row.costPrice | money }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价" width="100" align="right">
|
||||
<template slot-scope="s">¥{{ s.row.unitPrice | money }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="110" align="right">
|
||||
<template slot-scope="s"><strong style="color:#409EFF">¥{{ s.row.totalPrice | money }}</strong></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交期(天)" prop="deliveryDays" width="80" align="center" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-button size="small" @click="detailVisible = false">关闭</el-button>
|
||||
<el-button size="small" type="primary" icon="el-icon-document-copy" @click="handleQuickCreate(detailForm)">基于此报价快速新建</el-button>
|
||||
<el-button size="small" icon="el-icon-download" @click="exportPdf(detailForm)">导出PDF</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listClientQuote, delClientQuote, getClientQuote, getClientQuoteStatistics, quickCreateFromQuote, addClientQuote } from "@/api/bid/clientquote";
|
||||
|
||||
export default {
|
||||
name: "ClientQuoteHistory",
|
||||
filters: {
|
||||
money(v) { return v ? Number(v).toFixed(2) : "0.00"; },
|
||||
date(v) { return v ? v.substring(0, 10) : "-"; }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
quoteList: [],
|
||||
total: 0,
|
||||
stats: {},
|
||||
dateRange: null,
|
||||
queryParams: {
|
||||
pageNum: 1, pageSize: 10,
|
||||
quoteNo: null, clientName: null, status: null,
|
||||
params: { beginTime: null, endTime: null }
|
||||
},
|
||||
// 详情弹窗
|
||||
detailVisible: false,
|
||||
detailLoading: false,
|
||||
detailForm: {}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.getStats();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true;
|
||||
// 处理日期范围
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
this.queryParams.params.beginTime = this.dateRange[0] + ' 00:00:00';
|
||||
this.queryParams.params.endTime = this.dateRange[1] + ' 23:59:59';
|
||||
} else {
|
||||
this.queryParams.params.beginTime = null;
|
||||
this.queryParams.params.endTime = null;
|
||||
}
|
||||
listClientQuote(this.queryParams).then(res => {
|
||||
this.quoteList = res.rows || [];
|
||||
this.total = res.total || 0;
|
||||
this.loading = false;
|
||||
}).catch(() => { this.loading = false; });
|
||||
},
|
||||
getStats() {
|
||||
const params = {};
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
params.params = {
|
||||
beginTime: this.dateRange[0] + ' 00:00:00',
|
||||
endTime: this.dateRange[1] + ' 23:59:59'
|
||||
};
|
||||
}
|
||||
getClientQuoteStatistics(params).then(res => {
|
||||
this.stats = res.data || {};
|
||||
});
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList(); this.getStats(); },
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.dateRange = null;
|
||||
this.queryParams.params = { beginTime: null, endTime: null };
|
||||
this.handleQuery();
|
||||
},
|
||||
handleAdd() {
|
||||
addClientQuote({ status: "draft", currency: "CNY", items: [] }).then(res => {
|
||||
const id = res.data && res.data.quoteId;
|
||||
if (id) this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: id } });
|
||||
else this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: "__new__" } });
|
||||
}).catch(() => {
|
||||
this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: "__new__" } });
|
||||
});
|
||||
},
|
||||
goDetail(row) {
|
||||
this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: row.quoteId } });
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm("确认删除报价单【" + row.quoteNo + "】?").then(() => {
|
||||
return delClientQuote(row.quoteId);
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
this.getList();
|
||||
this.getStats();
|
||||
});
|
||||
},
|
||||
// 快速新建(基于历史报价)
|
||||
handleQuickCreate(row) {
|
||||
this.$modal.confirm("确认基于报价单【" + row.quoteNo + "】快速新建?").then(() => {
|
||||
return quickCreateFromQuote(row.quoteId);
|
||||
}).then(res => {
|
||||
this.$modal.msgSuccess("已创建新报价单草稿");
|
||||
this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: res.data.quoteId } });
|
||||
}).catch(() => {});
|
||||
},
|
||||
// 查看详情弹窗
|
||||
showDetail(row) {
|
||||
this.detailVisible = true;
|
||||
this.detailLoading = true;
|
||||
getClientQuote(row.quoteId).then(res => {
|
||||
this.detailForm = res.data || {};
|
||||
if (!this.detailForm.items) this.detailForm.items = [];
|
||||
this.detailLoading = false;
|
||||
}).catch(() => { this.detailLoading = false; });
|
||||
},
|
||||
// PDF导出(调用detail页面的实现)
|
||||
async exportPdf(row) {
|
||||
if (!row.items) {
|
||||
try {
|
||||
const res = await getClientQuote(row.quoteId);
|
||||
row = res.data || row;
|
||||
} catch(e) { this.$message.error("获取数据失败"); return; }
|
||||
}
|
||||
// 导航到详情页导出
|
||||
this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: row.quoteId, exportPdf: '1' } });
|
||||
},
|
||||
statusType(s) { return { draft: "info", sent: "primary", confirmed: "success", rejected: "danger" }[s] || "info"; },
|
||||
statusLabel(s) { return { draft: "草稿", sent: "已发送", confirmed: "已确认", rejected: "已拒绝" }[s] || s; }
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ========== 统计卡片 ========== */
|
||||
.stat-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||
}
|
||||
}
|
||||
.stat-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stat-card-total .stat-icon { background: linear-gradient(135deg, #409EFF, #2d7ed9); }
|
||||
.stat-card-client .stat-icon { background: linear-gradient(135deg, #67C23A, #529b2e); }
|
||||
.stat-card-amount .stat-icon { background: linear-gradient(135deg, #E6A23C, #cf9236); }
|
||||
.stat-card-avg .stat-icon { background: linear-gradient(135deg, #909399, #73767a); }
|
||||
|
||||
.stat-body { flex: 1; }
|
||||
.stat-value { font-size: 26px; font-weight: 700; color: #303133; line-height: 1.2; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
|
||||
|
||||
/* ========== 搜索卡片 ========== */
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
::v-deep .el-card__body { padding: 18px 20px 6px; }
|
||||
}
|
||||
|
||||
/* ========== 详情弹窗 ========== */
|
||||
.detail-dialog {
|
||||
::v-deep .el-dialog__body { padding: 20px; }
|
||||
}
|
||||
.detail-desc {
|
||||
::v-deep .el-descriptions__cell { padding: 8px 12px; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +1,691 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
|
||||
<el-form-item label="报价单号" prop="quoteNo">
|
||||
<el-input v-model="queryParams.quoteNo" placeholder="请输入报价单号" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="clientName">
|
||||
<el-input v-model="queryParams.clientName" placeholder="请输入客户名称" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:110px">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已发送" value="sent" />
|
||||
<el-option label="已确认" value="confirmed" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新建报价单</el-button>
|
||||
<div class="app-container clientquote-page">
|
||||
<!-- ── 顶部统计卡片 ── -->
|
||||
<el-row :gutter="14" class="stat-row">
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-all">
|
||||
<div class="stat-num">{{ stats.total_count || 0 }}</div>
|
||||
<div class="stat-lbl">报价单总数</div>
|
||||
<i class="el-icon-document-copy stat-icon"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-client">
|
||||
<div class="stat-num">{{ stats.client_count || 0 }}</div>
|
||||
<div class="stat-lbl">客户数量</div>
|
||||
<i class="el-icon-user stat-icon"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-amount">
|
||||
<div class="stat-num">¥{{ (stats.total_amount_sum || 0) | money }}</div>
|
||||
<div class="stat-lbl">报价总金额</div>
|
||||
<i class="el-icon-money stat-icon"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card stat-avg">
|
||||
<div class="stat-num">¥{{ (stats.avg_amount || 0) | money }}</div>
|
||||
<div class="stat-lbl">平均金额</div>
|
||||
<i class="el-icon-s-data stat-icon"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="quoteList" border>
|
||||
<el-table-column label="报价单号" prop="quoteNo" width="160" />
|
||||
<el-table-column label="客户名称" prop="clientName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="关联询价单" prop="rfqNo" width="140" />
|
||||
<el-table-column label="总金额" prop="totalAmount" width="120" align="right">
|
||||
<!-- ── 搜索栏 ── -->
|
||||
<el-card shadow="never" class="search-card">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
|
||||
<el-form-item label="客户名称">
|
||||
<el-input v-model="queryParams.clientName" placeholder="客户名称" clearable style="width:150px" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报价单号">
|
||||
<el-input v-model="queryParams.quoteNo" placeholder="单号" clearable style="width:150px" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:110px">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已发送" value="sent" />
|
||||
<el-option label="已确认" value="confirmed" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建日期">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始"
|
||||
end-placeholder="结束" value-format="yyyy-MM-dd" style="width:220px" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- ── 工具栏 ── -->
|
||||
<el-row :gutter="10" class="mb8" style="margin-top:12px">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">新建报价单</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- ── 报价单列表 ── -->
|
||||
<el-table v-loading="loading" :data="list" border stripe highlight-current-row>
|
||||
<el-table-column label="报价单号" prop="quoteNo" width="170" fixed>
|
||||
<template slot-scope="s">
|
||||
<strong style="color:#409EFF">¥{{ s.row.totalAmount | money }}</strong>
|
||||
<span style="font-weight:600;color:#303133;cursor:pointer" @click="handleView(s.row)">{{ s.row.quoteNo }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户名称" prop="clientName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="关联询价" min-width="150">
|
||||
<template slot-scope="s">
|
||||
<div style="font-weight:600;color:#303133">{{ s.row.rfqNo || '-' }}</div>
|
||||
<div style="font-size:12px;color:#909399;margin-top:2px" v-if="s.row.rfqTitle">{{ s.row.rfqTitle }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总金额" width="130" align="right">
|
||||
<template slot-scope="s">
|
||||
<strong style="color:#409EFF;font-size:15px">¥{{ s.row.totalAmount | money }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="币种" prop="currency" width="70" align="center" />
|
||||
<el-table-column label="有效期" prop="validityDate" width="110" align="center">
|
||||
<template slot-scope="s">{{ s.row.validityDate | date }}</template>
|
||||
<el-table-column label="有效期" width="110" align="center">
|
||||
<template slot-scope="s">{{ s.row.validityDate | dateFmt }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-tag :type="statusType(s.row.status)" size="mini">{{ statusLabel(s.row.status) }}</el-tag>
|
||||
<div class="status-chip" :class="'status-' + s.row.status">
|
||||
<i :class="statusIcon(s.row.status)"></i>
|
||||
{{ statusLabel(s.row.status) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建人" prop="createBy" width="100" align="center" />
|
||||
<el-table-column label="创建时间" prop="createTime" width="160" align="center" />
|
||||
<el-table-column label="操作" align="center" width="200">
|
||||
<el-table-column label="操作" align="center" width="210" fixed="right">
|
||||
<template slot-scope="s">
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="goDetail(s.row)">详情/编辑</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(s.row)">删除</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(s.row)">详情</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(s.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-document-copy" @click="handleQuickCreate(s.row)">快速新建</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c"
|
||||
@click="handleDelete(s.row)" v-if="s.row.status==='draft'">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- ── 创建/编辑对话框 ── -->
|
||||
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="95%" append-to-body :close-on-click-modal="false" class="cq-edit-dialog">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="90px" size="small">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户名称" prop="clientName">
|
||||
<el-input v-model="form.clientName" placeholder="请输入客户/甲方名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="关联询价单">
|
||||
<el-select v-model="form.rfqId" placeholder="选择关联RFQ(可选)" filterable clearable style="width:100%" @change="onRfqSelect">
|
||||
<el-option v-for="r in rfqOptions" :key="r.rfqId"
|
||||
:label="r.rfqNo + ' · ' + r.rfqTitle" :value="r.rfqId">
|
||||
<div style="display:flex;justify-content:space-between">
|
||||
<span style="font-weight:600">{{ r.rfqNo }}</span>
|
||||
<span style="color:#909399;font-size:12px;margin-left:8px">{{ r.rfqTitle }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="有效期至">
|
||||
<el-date-picker v-model="form.validityDate" type="date" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择有效期" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency" style="width:100%">
|
||||
<el-option label="人民币 CNY" value="CNY" />
|
||||
<el-option label="美元 USD" value="USD" />
|
||||
<el-option label="欧元 EUR" value="EUR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:100%">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已发送" value="sent" />
|
||||
<el-option label="已确认" value="confirmed" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">
|
||||
<span style="font-weight:700;color:#1a2c4e">报价明细</span>
|
||||
<el-button type="text" icon="el-icon-plus" @click="addItem" style="margin-left:12px">添加行</el-button>
|
||||
</el-divider>
|
||||
|
||||
<div class="items-table-wrap">
|
||||
<el-table :data="form.items" border size="small" class="items-table">
|
||||
<el-table-column type="index" width="44" label="#" />
|
||||
<el-table-column label="物料名称" min-width="200">
|
||||
<template slot-scope="s">
|
||||
<el-autocomplete
|
||||
v-model="s.row.materialName"
|
||||
:fetch-suggestions="queryMaterialSearch"
|
||||
placeholder="搜索选择物料"
|
||||
size="mini"
|
||||
style="width:100%"
|
||||
popper-class="material-popper"
|
||||
:popper-append-to-body="true"
|
||||
@select="(item) => onMaterialSelect(s.row, item)"
|
||||
>
|
||||
<template slot-scope="{ item }">
|
||||
<div class="material-suggestion">
|
||||
<div class="ms-top">
|
||||
<span class="ms-name">{{ item.materialName }}</span>
|
||||
<span class="ms-code" v-if="item.materialCode">{{ item.materialCode }}</span>
|
||||
</div>
|
||||
<div class="ms-detail">
|
||||
<span v-if="item.spec" class="ms-tag">规格:{{ item.spec }}</span>
|
||||
<span v-if="item.brand" class="ms-tag ms-brand-tag">品牌:{{ item.brand }}</span>
|
||||
<span v-if="item.unit" class="ms-tag">单位:{{ item.unit }}</span>
|
||||
<span v-if="item.categoryName" class="ms-tag">分类:{{ item.categoryName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" width="120">
|
||||
<template slot-scope="s">
|
||||
<el-input v-model="s.row.spec" size="mini" placeholder="规格" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="型号" width="120">
|
||||
<template slot-scope="s">
|
||||
<el-input v-model="s.row.modelNo" size="mini" placeholder="型号" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="70">
|
||||
<template slot-scope="s">
|
||||
<el-input v-model="s.row.unit" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="130">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.quantity" :min="0" size="mini" controls-position="right" style="width:100%" @change="calcRow(s.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成本价(元)" width="140">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.costPrice" :min="0" :precision="2" size="mini" controls-position="right" style="width:100%" @change="calcRow(s.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价(元)" width="140">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.unitPrice" :min="0" :precision="2" size="mini" controls-position="right" style="width:100%" @change="calcRow(s.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额(元)" width="120" align="right">
|
||||
<template slot-scope="s">
|
||||
<strong style="color:#409EFF">¥{{ itemTotal(s.row) }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="毛利率" width="80" align="center">
|
||||
<template slot-scope="s">
|
||||
<span :style="{ color: marginColor(s.row) }">{{ calcMargin(s.row) }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交期(天)" width="100">
|
||||
<template slot-scope="s">
|
||||
<el-input-number v-model="s.row.deliveryDays" :min="0" size="mini" controls-position="right" style="width:100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" icon="el-icon-delete" style="color:#f56c6c" @click="form.items.splice(s.$index, 1)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="form-total-bar">
|
||||
合计报价:<strong>¥{{ formTotal }}</strong>
|
||||
<span style="margin-left:16px;color:#909399;font-size:12px">{{ form.items.length }} 项物料</span>
|
||||
</div>
|
||||
|
||||
<el-form-item label="备注" style="margin-top:12px">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注说明" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogOpen = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="saving">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ── 详情对话框 ── -->
|
||||
<el-dialog title="报价单详情" :visible.sync="detailOpen" width="860px" append-to-body top="5vh">
|
||||
<div v-if="detailData">
|
||||
<!-- 状态流程条 -->
|
||||
<div class="detail-steps">
|
||||
<div class="step-item" :class="{ active: ['draft','sent','confirmed','rejected'].includes(detailData.status) }">
|
||||
<i class="el-icon-edit-outline"></i><span>草稿</span>
|
||||
</div>
|
||||
<div class="step-line" :class="{ active: ['sent','confirmed','rejected'].includes(detailData.status) }"></div>
|
||||
<div class="step-item" :class="{ active: ['sent','confirmed','rejected'].includes(detailData.status) }">
|
||||
<i class="el-icon-upload2"></i><span>已发送</span>
|
||||
</div>
|
||||
<div class="step-line" :class="{ active: ['confirmed','rejected'].includes(detailData.status) }"></div>
|
||||
<div class="step-item" :class="{ active: detailData.status === 'confirmed', rejected: detailData.status === 'rejected' }">
|
||||
<i :class="detailData.status === 'rejected' ? 'el-icon-circle-close' : 'el-icon-circle-check'"></i>
|
||||
<span>{{ detailData.status === 'rejected' ? '已拒绝' : '已确认' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions :column="3" border size="small" style="margin-bottom:16px">
|
||||
<el-descriptions-item label="报价单号">{{ detailData.quoteNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">{{ detailData.clientName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusType(detailData.status)" size="mini" effect="dark">{{ statusLabel(detailData.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="币种">{{ detailData.currency || 'CNY' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">{{ detailData.validityDate | dateFmt }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联询价">{{ detailData.rfqNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总金额" :span="3">
|
||||
<strong style="color:#409EFF;font-size:18px">¥{{ detailData.totalAmount | money }}</strong>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="3">{{ detailData.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="section-title">报价明细</div>
|
||||
<el-table :data="detailData.items || []" border size="small" style="margin-top:12px">
|
||||
<el-table-column type="index" width="46" label="#" />
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="140" />
|
||||
<el-table-column label="规格" prop="spec" width="110" />
|
||||
<el-table-column label="型号" prop="modelNo" width="100" />
|
||||
<el-table-column label="单位" prop="unit" width="60" align="center" />
|
||||
<el-table-column label="数量" prop="quantity" width="80" align="right" />
|
||||
<el-table-column label="成本价" width="100" align="right">
|
||||
<template slot-scope="s">¥{{ s.row.costPrice | money }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价" width="110" align="right">
|
||||
<template slot-scope="s">¥{{ s.row.unitPrice | money }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="110" align="right">
|
||||
<template slot-scope="s"><strong style="color:#409EFF">¥{{ s.row.totalPrice | money }}</strong></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交期(天)" prop="deliveryDays" width="80" align="center" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-button @click="detailOpen = false">关闭</el-button>
|
||||
<el-button type="primary" icon="el-icon-edit" @click="editFromDetail" v-if="detailData">编辑</el-button>
|
||||
<el-button type="success" icon="el-icon-document-copy" @click="quickCreateFromDetail" v-if="detailData">快速新建</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listClientQuote, delClientQuote, addClientQuote } from "@/api/bid/clientquote";
|
||||
import { listClientQuote, getClientQuote, addClientQuote, updateClientQuote, delClientQuote,
|
||||
getClientQuoteStatistics, quickCreateFromQuote } from "@/api/bid/clientquote";
|
||||
import { listRfq } from "@/api/bid/rfq";
|
||||
import { listMaterial } from "@/api/bid/material";
|
||||
|
||||
export default {
|
||||
name: "ClientQuote",
|
||||
filters: {
|
||||
money(v) { return v ? Number(v).toFixed(2) : "0.00"; },
|
||||
date(v) { return v ? v.substring(0, 10) : "-"; }
|
||||
money: v => v ? Number(v).toFixed(2) : "0.00",
|
||||
dateFmt: v => v ? v.substring(0, 10) : "-"
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 列表
|
||||
loading: false,
|
||||
quoteList: [],
|
||||
list: [],
|
||||
total: 0,
|
||||
queryParams: { pageNum: 1, pageSize: 10, quoteNo: null, clientName: null, status: null }
|
||||
stats: {},
|
||||
dateRange: null,
|
||||
queryParams: {
|
||||
pageNum: 1, pageSize: 10,
|
||||
clientName: null, quoteNo: null, status: null,
|
||||
params: { beginTime: null, endTime: null }
|
||||
},
|
||||
// 创建/编辑
|
||||
dialogOpen: false,
|
||||
dialogTitle: "",
|
||||
saving: false,
|
||||
rfqOptions: [],
|
||||
materialCache: [],
|
||||
form: { items: [], currency: "CNY", status: "draft" },
|
||||
rules: {
|
||||
clientName: [{ required: true, message: "请输入客户名称", trigger: "blur" }]
|
||||
},
|
||||
// 详情
|
||||
detailOpen: false,
|
||||
detailData: null
|
||||
};
|
||||
},
|
||||
created() { this.getList(); },
|
||||
computed: {
|
||||
formTotal() {
|
||||
return (this.form.items || []).reduce((s, i) => s + (parseFloat(i.quantity || 0) * parseFloat(i.unitPrice || 0)), 0).toFixed(2);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.getStats();
|
||||
listRfq({ pageSize: 200 }).then(r => { this.rfqOptions = r.rows || []; });
|
||||
},
|
||||
methods: {
|
||||
// ===== 列表 =====
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listClientQuote(this.queryParams).then(res => {
|
||||
this.quoteList = res.rows || [];
|
||||
this.total = res.total || 0;
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
this.queryParams.params.beginTime = this.dateRange[0] + ' 00:00:00';
|
||||
this.queryParams.params.endTime = this.dateRange[1] + ' 23:59:59';
|
||||
} else {
|
||||
this.queryParams.params.beginTime = null;
|
||||
this.queryParams.params.endTime = null;
|
||||
}
|
||||
listClientQuote(this.queryParams).then(r => {
|
||||
this.list = r.rows || [];
|
||||
this.total = r.total || 0;
|
||||
this.loading = false;
|
||||
}).catch(() => { this.loading = false; });
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
|
||||
resetQuery() { this.resetForm("queryForm"); this.handleQuery(); },
|
||||
getStats() {
|
||||
getClientQuoteStatistics(this.queryParams).then(r => {
|
||||
this.stats = r.data || {};
|
||||
});
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList(); this.getStats(); },
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.dateRange = null;
|
||||
this.queryParams.params = { beginTime: null, endTime: null };
|
||||
this.handleQuery();
|
||||
},
|
||||
|
||||
// ===== 新建 =====
|
||||
handleAdd() {
|
||||
// Create draft then navigate
|
||||
addClientQuote({ status: "draft", currency: "CNY", items: [] }).then(res => {
|
||||
const id = res.data && res.data.quoteId;
|
||||
if (id) this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: id } });
|
||||
else this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: "__new__" } });
|
||||
}).catch(() => {
|
||||
this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: "__new__" } });
|
||||
this.form = { items: [], currency: "CNY", status: "draft", clientName: "", rfqId: null, validityDate: "", remark: "" };
|
||||
this.dialogTitle = "新建甲方报价单";
|
||||
this.dialogOpen = true;
|
||||
},
|
||||
|
||||
// ===== 编辑 =====
|
||||
handleUpdate(row) {
|
||||
getClientQuote(row.quoteId).then(r => {
|
||||
const data = r.data || {};
|
||||
this.form = {
|
||||
quoteId: data.quoteId,
|
||||
clientName: data.clientName,
|
||||
rfqId: data.rfqId,
|
||||
rfqNo: data.rfqNo,
|
||||
rfqTitle: data.rfqTitle,
|
||||
status: data.status,
|
||||
validityDate: data.validityDate,
|
||||
currency: data.currency,
|
||||
remark: data.remark,
|
||||
items: (data.items || []).map(i => ({ ...i }))
|
||||
};
|
||||
this.dialogTitle = "编辑报价单 - " + data.quoteNo;
|
||||
this.dialogOpen = true;
|
||||
});
|
||||
},
|
||||
goDetail(row) {
|
||||
this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: row.quoteId } });
|
||||
|
||||
// ===== 保存 =====
|
||||
submitForm() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return;
|
||||
this.saving = true;
|
||||
const action = this.form.quoteId ? updateClientQuote : addClientQuote;
|
||||
action(this.form).then(() => {
|
||||
this.$modal.msgSuccess("保存成功");
|
||||
this.dialogOpen = false;
|
||||
this.getList();
|
||||
this.getStats();
|
||||
}).finally(() => { this.saving = false; });
|
||||
});
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm("确认删除报价单【" + row.quoteNo + "】?").then(() => {
|
||||
return delClientQuote(row.quoteId);
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
|
||||
// ===== 详情查看 =====
|
||||
handleView(row) {
|
||||
getClientQuote(row.quoteId).then(r => {
|
||||
this.detailData = r.data || {};
|
||||
if (!this.detailData.items) this.detailData.items = [];
|
||||
this.detailOpen = true;
|
||||
});
|
||||
},
|
||||
editFromDetail() {
|
||||
this.detailOpen = false;
|
||||
if (this.detailData) {
|
||||
this.handleUpdate(this.detailData);
|
||||
}
|
||||
},
|
||||
quickCreateFromDetail() {
|
||||
this.detailOpen = false;
|
||||
if (this.detailData) this.handleQuickCreate(this.detailData);
|
||||
},
|
||||
|
||||
// ===== 快速新建 =====
|
||||
handleQuickCreate(row) {
|
||||
this.$modal.confirm("确认基于报价单【" + row.quoteNo + "】快速新建?").then(() => {
|
||||
return quickCreateFromQuote(row.quoteId);
|
||||
}).then(res => {
|
||||
this.$modal.msgSuccess("已创建新报价单草稿");
|
||||
this.getList();
|
||||
});
|
||||
this.getStats();
|
||||
// 打开编辑
|
||||
if (res.data) {
|
||||
this.form = {
|
||||
quoteId: res.data.quoteId,
|
||||
clientName: res.data.clientName,
|
||||
rfqId: res.data.rfqId,
|
||||
rfqNo: res.data.rfqNo,
|
||||
rfqTitle: res.data.rfqTitle,
|
||||
status: res.data.status,
|
||||
validityDate: res.data.validityDate,
|
||||
currency: res.data.currency,
|
||||
remark: res.data.remark,
|
||||
items: (res.data.items || []).map(i => ({ ...i }))
|
||||
};
|
||||
this.dialogTitle = "编辑新建报价单 - " + res.data.quoteNo;
|
||||
this.dialogOpen = true;
|
||||
}
|
||||
}).catch(() => {});
|
||||
},
|
||||
statusType(s) { return { draft: "info", sent: "primary", confirmed: "success", rejected: "danger" }[s] || "info"; },
|
||||
statusLabel(s) { return { draft: "草稿", sent: "已发送", confirmed: "已确认", rejected: "已拒绝" }[s] || s; }
|
||||
|
||||
// ===== 删除 =====
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm("确认删除报价单【" + row.quoteNo + "】?").then(() => delClientQuote(row.quoteId))
|
||||
.then(() => { this.$modal.msgSuccess("删除成功"); this.getList(); this.getStats(); });
|
||||
},
|
||||
|
||||
// ===== RFQ选择 =====
|
||||
onRfqSelect(rfqId) {
|
||||
const rfq = this.rfqOptions.find(r => r.rfqId === rfqId);
|
||||
if (rfq) { this.form.rfqNo = rfq.rfqNo; this.form.rfqTitle = rfq.rfqTitle; }
|
||||
},
|
||||
|
||||
// ===== 物料搜索 =====
|
||||
queryMaterialSearch(query, cb) {
|
||||
if (!query || query.length < 1) {
|
||||
cb(this.materialCache.slice(0, 20)); return;
|
||||
}
|
||||
listMaterial({ materialName: query, pageSize: 20 }).then(res => {
|
||||
const list = res.rows || [];
|
||||
this.materialCache = list.map(m => ({
|
||||
...m, value: m.materialName + (m.spec ? ' (' + m.spec + ')' : '')
|
||||
}));
|
||||
cb(this.materialCache.slice(0, 20));
|
||||
}).catch(() => cb([]));
|
||||
},
|
||||
onMaterialSelect(row, item) {
|
||||
if (!item) return;
|
||||
row.materialId = item.materialId;
|
||||
row.materialName = item.materialName;
|
||||
row.spec = item.spec || '';
|
||||
row.modelNo = item.modelNo || item.spec || '';
|
||||
row.unit = item.unit || '件';
|
||||
},
|
||||
|
||||
// ===== 明细行 =====
|
||||
addItem() {
|
||||
this.form.items.push({ materialId: null, materialName: "", spec: "", modelNo: "", unit: "件",
|
||||
quantity: 1, costPrice: 0, unitPrice: 0, totalPrice: "0.00", deliveryDays: null });
|
||||
},
|
||||
calcRow(row) {
|
||||
const q = parseFloat(row.quantity) || 0;
|
||||
const p = parseFloat(row.unitPrice) || 0;
|
||||
row.totalPrice = (q * p).toFixed(2);
|
||||
},
|
||||
itemTotal(row) {
|
||||
return ((parseFloat(row.quantity) || 0) * (parseFloat(row.unitPrice) || 0)).toFixed(2);
|
||||
},
|
||||
calcMargin(row) {
|
||||
const cost = parseFloat(row.costPrice) || 0;
|
||||
const price = parseFloat(row.unitPrice) || 0;
|
||||
if (!price) return "0.0";
|
||||
return (((price - cost) / price) * 100).toFixed(1);
|
||||
},
|
||||
marginColor(row) {
|
||||
const m = parseFloat(this.calcMargin(row));
|
||||
if (m >= 20) return "#67c23a";
|
||||
if (m >= 10) return "#e6a23c";
|
||||
return "#f56c6c";
|
||||
},
|
||||
|
||||
// ===== 状态辅助 =====
|
||||
statusType(s) { return { draft: "info", sent: "primary", confirmed: "success", rejected: "danger" }[s] || ""; },
|
||||
statusLabel(s) { return { draft: "草稿", sent: "已发送", confirmed: "已确认", rejected: "已拒绝" }[s] || s; },
|
||||
statusIcon(s) { return { draft: "el-icon-edit-outline", sent: "el-icon-upload2", confirmed: "el-icon-circle-check", rejected: "el-icon-circle-close" }[s] || "el-icon-document"; }
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.clientquote-page { padding-bottom: 30px; }
|
||||
|
||||
/* ── 统计卡片 ── */
|
||||
.stat-row { margin-bottom: 16px; }
|
||||
.stat-card {
|
||||
border-radius: 10px; padding: 18px 20px; position: relative;
|
||||
overflow: hidden; color: #fff; cursor: default;
|
||||
}
|
||||
.stat-num { font-size: 26px; font-weight: 700; line-height: 1; }
|
||||
.stat-lbl { font-size: 13px; margin-top: 6px; opacity: 0.9; }
|
||||
.stat-icon { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); font-size: 48px; opacity: 0.2; }
|
||||
.stat-all { background: linear-gradient(135deg, #1171c4, #22a4ff); }
|
||||
.stat-client { background: linear-gradient(135deg, #67c23a, #85ce61); }
|
||||
.stat-amount { background: linear-gradient(135deg, #e6a23c, #f0c040); }
|
||||
.stat-avg { background: linear-gradient(135deg, #909399, #b0b3b8); }
|
||||
|
||||
/* ── 搜索 ── */
|
||||
.search-card { ::v-deep .el-card__body { padding: 16px 20px 8px; } }
|
||||
|
||||
/* ── 状态芯片 ── */
|
||||
.status-chip {
|
||||
display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px;
|
||||
border-radius: 12px; font-size: 12px; font-weight: 600;
|
||||
i { font-size: 12px; }
|
||||
}
|
||||
.status-draft { background: #f4f4f5; color: #909399; }
|
||||
.status-sent { background: #e6f1ff; color: #409eff; border: 1px solid #b3d8ff; }
|
||||
.status-confirmed { background: #f0f9eb; color: #67c23a; border: 1px solid #c2e7b0; }
|
||||
.status-rejected { background: #fef0f0; color: #f56c6c; border: 1px solid #fbc4c4; }
|
||||
|
||||
/* ── 表单合计 ── */
|
||||
.items-table { margin-bottom: 0; }
|
||||
.form-total-bar {
|
||||
text-align: right; padding: 10px 16px;
|
||||
background: linear-gradient(90deg, #f9fbff, #f0f7ff);
|
||||
border: 1px solid #e4e7ed; border-top: none; border-radius: 0 0 4px 4px;
|
||||
font-size: 14px; color: #606266;
|
||||
strong { font-size: 20px; color: #409eff; margin-left: 6px; }
|
||||
}
|
||||
|
||||
/* ── 详情 - 状态流程 ── */
|
||||
.detail-steps {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 16px 0 20px; gap: 0;
|
||||
}
|
||||
.step-item {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||||
color: #c0c4cc; font-size: 12px;
|
||||
i { font-size: 22px; }
|
||||
&.active { color: #1171c4; }
|
||||
&.rejected { color: #f56c6c; }
|
||||
}
|
||||
.step-line {
|
||||
flex: 1; max-width: 80px; height: 2px; background: #e4e7ed; margin: 0 8px; margin-top: -12px;
|
||||
&.active { background: #1171c4; }
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px; font-weight: 700; color: #1a2c4e;
|
||||
padding-left: 8px; border-left: 4px solid #1171c4;
|
||||
}
|
||||
|
||||
/* ── 物料搜索下拉建议 ── */
|
||||
.material-suggestion {
|
||||
display: flex; flex-direction: column; padding: 4px 0; line-height: 1.5;
|
||||
}
|
||||
.ms-top { display: flex; align-items: center; gap: 8px; }
|
||||
.ms-name { font-size: 13px; font-weight: 600; color: #303133; }
|
||||
.ms-code { font-size: 11px; color: #909399; }
|
||||
.ms-detail { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-top: 3px; }
|
||||
.ms-tag {
|
||||
display: inline-block; font-size: 11px; color: #606266;
|
||||
background: #f5f7fa; padding: 0 6px; border-radius: 3px; line-height: 1.8;
|
||||
}
|
||||
.ms-brand-tag { color: #409EFF; background: #ecf5ff; }
|
||||
</style>
|
||||
|
||||
<!-- ── 全局样式:修复 autocomplete 下拉框被遮挡 ── -->
|
||||
<style lang="scss">
|
||||
.material-popper {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
.material-popper .el-autocomplete-suggestion {
|
||||
width: 420px !important;
|
||||
}
|
||||
.material-popper .el-autocomplete-suggestion li {
|
||||
padding: 6px 12px !important;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
.material-popper .el-autocomplete-suggestion li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.material-popper .el-autocomplete-suggestion li:hover {
|
||||
background: #f0f7ff !important;
|
||||
}
|
||||
|
||||
/* 编辑对话框中的表格容器,防止表格溢出 */
|
||||
.cq-edit-dialog .el-dialog__body {
|
||||
padding: 16px 20px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.items-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.items-table-wrap .el-table {
|
||||
border: none !important;
|
||||
}
|
||||
.items-table-wrap .el-table::before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,72 +3,100 @@
|
||||
<!-- 操作栏 -->
|
||||
<div class="tab-toolbar">
|
||||
<span class="tab-title">甲方报价记录</span>
|
||||
<el-button size="mini" icon="el-icon-download" @click="exportExcel">导出Excel</el-button>
|
||||
<div class="tab-actions">
|
||||
<el-button size="mini" icon="el-icon-plus" type="primary" @click="goToHistory">查看全部历史</el-button>
|
||||
<el-button size="mini" icon="el-icon-download" @click="exportExcel">导出Excel</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 报价表格 -->
|
||||
<el-table
|
||||
:data="list"
|
||||
v-loading="loading"
|
||||
border
|
||||
<el-table
|
||||
:data="list"
|
||||
v-loading="loading"
|
||||
border
|
||||
size="small"
|
||||
class="quote-table"
|
||||
:header-cell-style="headerStyle">
|
||||
<el-table-column label="报价日期" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="date-cell">{{ formatDate(scope.row.create_time) }}</span>
|
||||
<span class="date-cell">{{ formatDate(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="甲方信息" min-width="200">
|
||||
|
||||
<el-table-column label="甲方信息" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<div class="client-info">
|
||||
<div class="client-name">{{ scope.row.client_name }}</div>
|
||||
<div class="client-name">{{ scope.row.clientName }}</div>
|
||||
<div class="quote-ref" v-if="scope.row.quoteNo">单号:{{ scope.row.quoteNo }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="成本价(元)" width="120" align="right">
|
||||
|
||||
<el-table-column label="规格/型号" min-width="160">
|
||||
<template slot-scope="scope">
|
||||
<span class="price-cell cost-price">¥{{ formatPrice(scope.row.cost_price) }}</span>
|
||||
<div class="spec-info">
|
||||
<span v-if="scope.row.spec">规格:{{ scope.row.spec }}</span>
|
||||
<span v-if="scope.row.modelNo"> / {{ scope.row.modelNo }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="单价(元)" width="120" align="right">
|
||||
|
||||
<el-table-column label="数量" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="price-cell">¥{{ formatPrice(scope.row.unit_price) }}</span>
|
||||
{{ scope.row.quantity || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="成交价(元)" width="120" align="right">
|
||||
|
||||
<el-table-column label="成本价(元)" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span class="price-cell total-price">¥{{ formatPrice(scope.row.total_price) }}</span>
|
||||
<span class="price-cell cost-price">¥{{ formatPrice(scope.row.costPrice) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="报价单号" width="150" align="center">
|
||||
|
||||
<el-table-column label="单价(元)" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span class="quote-no">{{ scope.row.quote_no }}</span>
|
||||
<span class="price-cell">¥{{ formatPrice(scope.row.unitPrice) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
|
||||
<el-table-column label="成交价(元)" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-tag
|
||||
:type="getStatusType(scope.row.quote_status)"
|
||||
<span class="price-cell total-price">¥{{ formatPrice(scope.row.totalPrice) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag
|
||||
:type="getStatusType(scope.row.quoteStatus)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
class="status-tag">
|
||||
{{ getStatusText(scope.row.quote_status) }}
|
||||
{{ getStatusText(scope.row.quoteStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" icon="el-icon-top-right" @click="viewQuote(scope.row)">
|
||||
查看报价单
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && list.length === 0" class="empty-state">
|
||||
<i class="el-icon-document"></i>
|
||||
<p>暂无甲方报价记录</p>
|
||||
<el-button size="small" type="primary" icon="el-icon-plus" @click="goToHistory">去创建报价</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getClientQuotes } from "@/api/bid/material";
|
||||
import { getClientQuoteByMaterial } from "@/api/bid/clientquote";
|
||||
|
||||
export default {
|
||||
name: "ClientQuoteTab",
|
||||
@@ -79,7 +107,7 @@ export default {
|
||||
loadData() {
|
||||
if (!this.materialId) return;
|
||||
this.loading = true;
|
||||
getClientQuotes(this.materialId).then(res => {
|
||||
getClientQuoteByMaterial(this.materialId).then(res => {
|
||||
this.list = res.data || [];
|
||||
this.loading = false;
|
||||
}).catch(() => { this.loading = false; });
|
||||
@@ -94,21 +122,29 @@ export default {
|
||||
return Number(price).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
},
|
||||
getStatusType(status) {
|
||||
const map = { 'accepted': 'success', 'rejected': 'danger', 'pending': 'warning', 'draft': 'info' };
|
||||
const map = { 'confirmed': 'success', 'rejected': 'danger', 'sent': 'primary', 'draft': 'info' };
|
||||
return map[status] || 'info';
|
||||
},
|
||||
getStatusText(status) {
|
||||
const map = { 'accepted': '已接受', 'rejected': '已拒绝', 'pending': '待处理', 'draft': '草稿' };
|
||||
const map = { 'confirmed': '已确认', 'rejected': '已拒绝', 'sent': '已发送', 'draft': '草稿' };
|
||||
return map[status] || status;
|
||||
},
|
||||
headerStyle() {
|
||||
return {
|
||||
background: '#f5f7fa',
|
||||
color: '#606266',
|
||||
return {
|
||||
background: '#f5f7fa',
|
||||
color: '#606266',
|
||||
fontWeight: 600,
|
||||
fontSize: '13px'
|
||||
};
|
||||
},
|
||||
viewQuote(row) {
|
||||
if (row.quoteId) {
|
||||
this.$router.push({ path: "/quotemgr/clientquote/detail", query: { quoteId: row.quoteId } });
|
||||
}
|
||||
},
|
||||
goToHistory() {
|
||||
this.$router.push("/quotemgr/clientquote");
|
||||
},
|
||||
exportExcel() {
|
||||
this.$message.info('导出功能开发中');
|
||||
}
|
||||
@@ -121,7 +157,6 @@ export default {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.tab-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -139,7 +174,11 @@ export default {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quote-table {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
@@ -150,14 +189,12 @@ export default {
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* 日期单元格 */
|
||||
.date-cell {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 甲方信息 */
|
||||
.client-info {
|
||||
padding: 4px 0;
|
||||
}
|
||||
@@ -169,7 +206,17 @@ export default {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 价格单元格 */
|
||||
.quote-ref {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.spec-info {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.price-cell {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -185,22 +232,27 @@ export default {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 报价单号 */
|
||||
.quote-no {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-tag {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 行悬停效果 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
>>> .el-table__row:hover {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
@@ -157,12 +157,57 @@
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 3: 供货清单 -->
|
||||
<!-- Tab 3: 报价历史(按物料条目展开) -->
|
||||
<el-tab-pane label="报价历史" name="quotes">
|
||||
<div class="tab-content">
|
||||
<el-table :data="quoteList" v-loading="quoteLoading" border size="small" style="width:100%">
|
||||
<el-table-column type="index" width="44" label="#" />
|
||||
<el-table-column label="报价日期" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatDate(scope.row.submitTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价单号" prop="quoteNo" min-width="150" />
|
||||
<el-table-column label="物料名称" min-width="160" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<el-link v-if="scope.row.materialId" type="primary" :underline="false" @click="viewMaterial(scope.row)">
|
||||
{{ scope.row.materialName }}
|
||||
</el-link>
|
||||
<span v-else>{{ scope.row.materialName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" prop="spec" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="单位" prop="unit" width="60" align="center" />
|
||||
<el-table-column label="数量" prop="quantity" width="80" align="right" />
|
||||
<el-table-column label="单价(元)" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span class="price-text">{{ scope.row.unitPrice ? '¥' + Number(scope.row.unitPrice).toFixed(2) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额(元)" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span class="price-text">{{ scope.row.totalPrice ? '¥' + Number(scope.row.totalPrice).toFixed(2) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交期" prop="deliveryDays" width="70" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.deliveryDays || '-' }}天</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="quoteStatusType(scope.row.quoteStatus)" size="small">{{ quoteStatusText(scope.row.quoteStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
<div v-if="!quoteList.length && !quoteLoading" class="table-empty">暂无报价记录</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 4: 供货清单 -->
|
||||
<el-tab-pane label="供货清单" name="orders">
|
||||
<div class="tab-content">
|
||||
<el-table :data="orderList" v-loading="orderLoading" border size="small" style="width:100%">
|
||||
<el-table-column label="采购单号" prop="poNo" min-width="150" />
|
||||
<el-table-column label="供应商" prop="supplierName" min-width="140" />
|
||||
<el-table-column label="采购标题" prop="rfqTitle" min-width="160" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="总金额" prop="totalAmount" width="120" align="right">
|
||||
<template slot-scope="scope">
|
||||
@@ -180,7 +225,7 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 4: 订单异议 -->
|
||||
<!-- Tab 5: 订单异议 -->
|
||||
<el-tab-pane label="订单异议" name="objections">
|
||||
<div class="tab-content">
|
||||
<el-table :data="objectionList" v-loading="objectionLoading" border size="small" style="width:100%">
|
||||
@@ -251,6 +296,7 @@
|
||||
import { listSupplier, getSupplier, addSupplier, updateSupplier, delSupplier } from "@/api/bid/supplier";
|
||||
import { listPurchaseorder } from "@/api/bid/purchaseorder";
|
||||
import { listObjection } from "@/api/bid/objection";
|
||||
import { getSupplierQuoteItems } from "@/api/bid/quotation";
|
||||
|
||||
export default {
|
||||
name: "SupplierManage",
|
||||
@@ -288,7 +334,11 @@ export default {
|
||||
|
||||
// ---- 订单异议 ----
|
||||
objectionLoading: false,
|
||||
objectionList: []
|
||||
objectionList: [],
|
||||
|
||||
// ---- 报价历史 ----
|
||||
quoteLoading: false,
|
||||
quoteList: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -357,6 +407,13 @@ export default {
|
||||
// 清空子列表
|
||||
this.orderList = [];
|
||||
this.objectionList = [];
|
||||
this.quoteList = [];
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
||||
},
|
||||
|
||||
formatCode(id) {
|
||||
@@ -447,6 +504,39 @@ export default {
|
||||
this.$refs.editForm && this.$refs.editForm.resetFields();
|
||||
},
|
||||
|
||||
/* ========== Tab: 报价历史(按物料条目展开) ========== */
|
||||
loadQuotes() {
|
||||
if (!this.currentSupplier) return;
|
||||
this.quoteLoading = true;
|
||||
getSupplierQuoteItems(this.currentSupplier.supplierId)
|
||||
.then(res => {
|
||||
this.quoteList = res.data || [];
|
||||
this.quoteLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.quoteLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
quoteStatusType(status) {
|
||||
const map = { accepted: 'success', submitted: 'primary', draft: 'info', rejected: 'danger' };
|
||||
return map[status] || 'info';
|
||||
},
|
||||
|
||||
quoteStatusText(status) {
|
||||
const map = { accepted: '已采纳', submitted: '已提交', draft: '草稿', rejected: '已拒绝' };
|
||||
return map[status] || status || '-';
|
||||
},
|
||||
|
||||
/** 查看物料详情 */
|
||||
viewMaterial(row) {
|
||||
if (row.materialId) {
|
||||
this.$router.push({ path: '/bid/material/detail', query: { id: row.materialId } });
|
||||
} else {
|
||||
this.$message.warning('该报价条目未关联物料');
|
||||
}
|
||||
},
|
||||
|
||||
/* ========== Tab: 供货清单 ========== */
|
||||
loadOrders() {
|
||||
if (!this.currentSupplier) return;
|
||||
@@ -497,6 +587,9 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
activeTab(val) {
|
||||
if (val === "quotes" && !this.quoteList.length) {
|
||||
this.loadQuotes();
|
||||
}
|
||||
if (val === "orders" && !this.orderList.length) {
|
||||
this.loadOrders();
|
||||
}
|
||||
@@ -506,9 +599,13 @@ export default {
|
||||
},
|
||||
currentSupplier() {
|
||||
// 切换供应商时清除子列表
|
||||
this.quoteList = [];
|
||||
this.orderList = [];
|
||||
this.objectionList = [];
|
||||
// 如果当前在采购单或异议Tab,自动加载
|
||||
// 如果当前在报价、采购单或异议Tab,自动加载
|
||||
if (this.activeTab === "quotes") {
|
||||
this.$nextTick(() => this.loadQuotes());
|
||||
}
|
||||
if (this.activeTab === "orders") {
|
||||
this.$nextTick(() => this.loadOrders());
|
||||
}
|
||||
|
||||
58
sql/20260601/001_client_quote_optimize.sql
Normal file
58
sql/20260601/001_client_quote_optimize.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- ============================================================================
|
||||
-- 甲方报价系统优化 - 数据库迁移脚本
|
||||
-- 日期: 2026-06-01
|
||||
-- 说明:
|
||||
-- 1. biz_client_quote_item 表添加 material_id 字段,建立与物料表的正式关联
|
||||
-- 2. 添加必要的索引优化查询性能
|
||||
-- 3. 添加统计数据辅助字段
|
||||
-- 4. 修复历史数据中的 material_id 关联
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 1: 甲方报价明细表 - 添加 material_id 字段
|
||||
-- ============================================================================
|
||||
ALTER TABLE biz_client_quote_item
|
||||
ADD COLUMN material_id BIGINT DEFAULT NULL COMMENT '关联物料ID(biz_material.material_id)' AFTER quote_id;
|
||||
|
||||
-- 创建索引以加速按物料查询
|
||||
CREATE INDEX idx_cqi_quote_id ON biz_client_quote_item (quote_id);
|
||||
CREATE INDEX idx_cqi_material_id ON biz_client_quote_item (material_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 2: 甲方报价主表 - 添加统计优化索引
|
||||
-- ============================================================================
|
||||
CREATE INDEX idx_cq_create_time ON biz_client_quote (create_time);
|
||||
CREATE INDEX idx_cq_client_name ON biz_client_quote (client_name);
|
||||
CREATE INDEX idx_cq_status ON biz_client_quote (status);
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 3: 修复历史数据 - 通过物料名称匹配回填 material_id
|
||||
-- ============================================================================
|
||||
-- 注意: 此操作为一次性数据修复,基于 material_name 精确匹配。
|
||||
-- 如果存在同名的多个物料,只会关联第一个匹配到的物料ID。
|
||||
-- 请在执行前确认数据准确性。
|
||||
UPDATE biz_client_quote_item cqi
|
||||
JOIN biz_material m ON cqi.material_name = m.material_name
|
||||
SET cqi.material_id = m.material_id
|
||||
WHERE cqi.material_id IS NULL
|
||||
AND cqi.material_name IS NOT NULL
|
||||
AND cqi.material_name != '';
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 4: 验证数据修复结果
|
||||
-- ============================================================================
|
||||
-- 检查仍有未关联的记录数(如果 > 0,说明有些物料名称在 biz_material 表中不存在)
|
||||
-- SELECT COUNT(*) AS unlinked_count
|
||||
-- FROM biz_client_quote_item
|
||||
-- WHERE material_id IS NULL AND material_name IS NOT NULL AND material_name != '';
|
||||
|
||||
-- 查看关联情况统计
|
||||
-- SELECT
|
||||
-- CASE WHEN cqi.material_id IS NOT NULL THEN '已关联' ELSE '未关联' END AS status,
|
||||
-- COUNT(*) AS count
|
||||
-- FROM biz_client_quote_item cqi
|
||||
-- GROUP BY CASE WHEN cqi.material_id IS NOT NULL THEN '已关联' ELSE '未关联' END;
|
||||
|
||||
-- ============================================================================
|
||||
-- 完成
|
||||
-- ============================================================================
|
||||
3
sql/20260601/002_check_menus.sql
Normal file
3
sql/20260601/002_check_menus.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
SELECT menu_id, menu_name, parent_id, `path`, component, perms
|
||||
FROM sys_menu
|
||||
ORDER BY menu_id;
|
||||
50
sql/20260601/003_add_clientquote_menu.sql
Normal file
50
sql/20260601/003_add_clientquote_menu.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
-- ============================================================================
|
||||
-- 添加"甲方报价"菜单到智慧报价模块
|
||||
-- 说明:
|
||||
-- 1. 在智慧报价模块下添加"甲方报价"菜单(目录)
|
||||
-- 2. 添加"甲方报价历史"菜单(菜单)
|
||||
-- 3. 添加"甲方报价单详情"隐藏路由
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. 添加甲方报价目录菜单(parent_id = 2000 智慧报价)
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2060, '甲方报价', 2000, 5, 'clientquote', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'el-icon-document-copy', 'admin', NOW(), 'admin', NOW(), '甲方报价管理模块');
|
||||
|
||||
-- 2. 添加甲方报价历史页面菜单
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2061, '甲方报价历史', 2060, 1, 'history', 'bid/clientquote/history/index', NULL, 1, 0, 'C', '0', '0', 'bid:clientquote:history', 'el-icon-document-copy', 'admin', NOW(), 'admin', NOW(), '甲方报价历史记录');
|
||||
|
||||
-- 3. 添加甲方报价单详情隐藏路由(路径为 /quotemgr/clientquote/detail)
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2062, '甲方报价单详情', 2060, 2, 'detail', 'bid/clientquote/detail', NULL, 1, 0, 'C', '1', '0', 'bid:clientquote:detail', NULL, 'admin', NOW(), 'admin', NOW(), '甲方报价单详情编辑页');
|
||||
|
||||
-- 4. 添加甲方报价列表页(给现有的 clientquote/index.vue)
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2063, '甲方报价列表', 2060, 3, 'index', 'bid/clientquote/index', NULL, 1, 0, 'C', '0', '0', 'bid:clientquote:list', NULL, 'admin', NOW(), 'admin', NOW(), '甲方报价列表');
|
||||
|
||||
-- ============================================================================
|
||||
-- 添加权限按钮
|
||||
-- ============================================================================
|
||||
|
||||
-- 甲方报价历史:查询
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2064, '查询', 2061, 1, '#', NULL, NULL, 1, 0, 'F', '0', '0', 'bid:clientquote:query', '#', 'admin', NOW(), 'admin', NOW(), '');
|
||||
|
||||
-- 甲方报价历史:新增
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2065, '新增', 2061, 2, '#', NULL, NULL, 1, 0, 'F', '0', '0', 'bid:clientquote:add', '#', 'admin', NOW(), 'admin', NOW(), '');
|
||||
|
||||
-- 甲方报价历史:修改
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2066, '修改', 2061, 3, '#', NULL, NULL, 1, 0, 'F', '0', '0', 'bid:clientquote:edit', '#', 'admin', NOW(), 'admin', NOW(), '');
|
||||
|
||||
-- 甲方报价历史:删除
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2067, '删除', 2061, 4, '#', NULL, NULL, 1, 0, 'F', '0', '0', 'bid:clientquote:remove', '#', 'admin', NOW(), 'admin', NOW(), '');
|
||||
|
||||
-- ============================================================================
|
||||
-- 分配菜单给管理员角色 (role_id = 1)
|
||||
-- ============================================================================
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, menu_id FROM sys_menu WHERE menu_id BETWEEN 2060 AND 2067
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = 1 AND menu_id = sys_menu.menu_id);
|
||||
22
sql/20260601/004_fix_clientquote_menu.sql
Normal file
22
sql/20260601/004_fix_clientquote_menu.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- ============================================================================
|
||||
-- 修正甲方报价菜单结构
|
||||
-- 目标:甲方报价作为一个单页管理页面,类似供应商报价
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. 将 甲方报价历史(2061) 改为指向 index 主页面
|
||||
UPDATE sys_menu
|
||||
SET menu_name = '甲方报价',
|
||||
component = 'bid/clientquote/index',
|
||||
perms = 'bid:clientquote:list',
|
||||
path = 'index'
|
||||
WHERE menu_id = 2061;
|
||||
|
||||
-- 2. 删除多余的 甲方报价列表(2063) 和 甲方报价单详情(2062)
|
||||
DELETE FROM sys_role_menu WHERE menu_id IN (2062, 2063);
|
||||
DELETE FROM sys_menu WHERE menu_id IN (2062, 2063);
|
||||
|
||||
-- 3. 将权限按钮从历史页面移到主菜单下
|
||||
UPDATE sys_menu SET parent_id = 2061 WHERE menu_id = 2064; -- 查询
|
||||
UPDATE sys_menu SET parent_id = 2061 WHERE menu_id = 2065; -- 新增
|
||||
UPDATE sys_menu SET parent_id = 2061 WHERE menu_id = 2066; -- 修改
|
||||
UPDATE sys_menu SET parent_id = 2061 WHERE menu_id = 2067; -- 删除
|
||||
Reference in New Issue
Block a user