feat(bid): 完成招投标业务模块多需求迭代

本次提交包含多项功能改进与业务优化:
1.  全局主题色替换为#4A6FA5,统一前端UI风格
2.  新增客户报价单clientId字段,完善客户报价数据结构
3.  实现发货单状态流转功能,支持发货、完成、撤回、设置结单日期操作
4.  新增物料发货记录多表关联查询功能
5.  优化客户管理页面UI布局与交互体验
6.  修复客户报价表单自动补全逻辑,关联clientId与clientName
7.  补充租户ID自动填充逻辑,完善多租户数据隔离
This commit is contained in:
2026-06-10 20:47:14 +08:00
parent bbddcb494d
commit 93785be505
16 changed files with 671 additions and 727 deletions

View File

@@ -37,6 +37,11 @@ public class BizClientController extends BaseController {
@PostMapping
public AjaxResult add(@RequestBody BizClient record) {
record.setCreateBy(getUsername());
Long tenantId = getDeptId();
if (tenantId == null) {
tenantId = 1L;
}
record.setTenantId(tenantId);
return toAjax(service.insertBizClient(record));
}

View File

@@ -37,6 +37,9 @@ public class BizDeliveryOrderController extends BaseController {
@PostMapping
public AjaxResult add(@RequestBody BizDeliveryOrder record) {
record.setCreateBy(getUsername());
Long tenantId = getDeptId();
if (tenantId == null) tenantId = 1L;
record.setTenantId(tenantId);
return toAjax(service.insertBizDeliveryOrder(record));
}
@@ -54,4 +57,46 @@ public class BizDeliveryOrderController extends BaseController {
public AjaxResult remove(@PathVariable Long[] doIds) {
return toAjax(service.deleteBizDeliveryOrderByIds(doIds));
}
// ════════════════════════════════════════
// 状态流转
// ════════════════════════════════════════════
@PreAuthorize("@ss.hasPermi('bid:order:status')")
@Log(title = "发货管理", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/ship")
public AjaxResult ship(@PathVariable Long id) {
return toAjax(service.ship(id));
}
@PreAuthorize("@ss.hasPermi('bid:order:status')")
@Log(title = "发货管理", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/complete")
public AjaxResult complete(@PathVariable Long id) {
return toAjax(service.complete(id, getUsername()));
}
@PreAuthorize("@ss.hasPermi('bid:order:status')")
@Log(title = "发货管理", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/recall")
public AjaxResult recall(@PathVariable Long id) {
return toAjax(service.recall(id));
}
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit')")
@Log(title = "结单时间", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/closeDate")
public AjaxResult setCloseDate(@PathVariable Long id, @RequestParam String closeDate) {
return toAjax(service.setCloseDate(id, closeDate, getUsername()));
}
// ════════════════════════════════════════
// 物料发货记录
// ════════════════════════════════════════════
@PreAuthorize("@ss.hasPermi('bid:material:query')")
@GetMapping("/materialRecords/{materialId}")
public AjaxResult materialRecords(@PathVariable Long materialId) {
return success(service.selectMaterialRecords(materialId));
}
}

View File

@@ -9,6 +9,7 @@ public class BizClientQuote extends BaseEntity {
private Long quoteId;
private Long tenantId;
private String quoteNo;
private Long clientId;
private String clientName;
private Long rfqId;
private String rfqNo;
@@ -29,6 +30,8 @@ public class BizClientQuote extends BaseEntity {
public void setTenantId(Long v){tenantId=v;}
public String getQuoteNo(){return quoteNo;}
public void setQuoteNo(String v){quoteNo=v;}
public Long getClientId(){return clientId;}
public void setClientId(Long v){clientId=v;}
public String getClientName(){return clientName;}
public void setClientName(String v){clientName=v;}
public Long getRfqId(){return rfqId;}

View File

@@ -1,7 +1,10 @@
package com.ruoyi.system.mapper.bid;
import com.ruoyi.system.domain.bid.BizDeliveryOrder;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
public interface BizDeliveryOrderMapper {
List<BizDeliveryOrder> selectBizDeliveryOrderList(BizDeliveryOrder query);
@@ -10,4 +13,14 @@ public interface BizDeliveryOrderMapper {
int updateBizDeliveryOrder(BizDeliveryOrder record);
int deleteBizDeliveryOrderById(Long id);
int deleteBizDeliveryOrderByIds(Long[] ids);
// 状态流转直接更新不受动态SQL null判断影响
int updateDeliveryStatus(@Param("doId") Long doId,
@Param("deliveryStatus") String deliveryStatus,
@Param("delayDate") Date delayDate,
@Param("actualCloseDate") Date actualCloseDate,
@Param("closeDateSetBy") String closeDateSetBy);
// 物料发货记录
List<Map<String, Object>> selectMaterialRecords(@Param("materialId") Long materialId);
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.system.service.bid;
import com.ruoyi.system.domain.bid.BizDeliveryOrder;
import java.util.List;
import java.util.Map;
public interface IBizDeliveryOrderService {
List<BizDeliveryOrder> selectBizDeliveryOrderList(BizDeliveryOrder query);
@@ -10,4 +11,13 @@ public interface IBizDeliveryOrderService {
int updateBizDeliveryOrder(BizDeliveryOrder record);
int deleteBizDeliveryOrderById(Long id);
int deleteBizDeliveryOrderByIds(Long[] ids);
// 状态流转
int ship(Long id);
int complete(Long id, String username);
int recall(Long id);
int setCloseDate(Long id, String closeDate, String username);
// 物料发货记录
List<Map<String, Object>> selectMaterialRecords(Long materialId);
}

View File

@@ -12,6 +12,7 @@ import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Service
public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
@@ -86,4 +87,57 @@ public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
public int deleteBizDeliveryOrderByIds(Long[] ids) {
return mapper.deleteBizDeliveryOrderByIds(ids);
}
// ═══════════════════════════════════════════════
// 状态流转
// ═══════════════════════════════════════════════
@Override
@Transactional
public int ship(Long id) {
BizDeliveryOrder d = mapper.selectBizDeliveryOrderById(id);
if (d == null) throw new RuntimeException("发货单不存在");
if (!"pending".equals(d.getDeliveryStatus()))
throw new RuntimeException("当前状态不允许发货确认");
return mapper.updateDeliveryStatus(id, "transit", null, null, "");
}
@Override
@Transactional
public int complete(Long id, String username) {
BizDeliveryOrder d = mapper.selectBizDeliveryOrderById(id);
if (d == null) throw new RuntimeException("发货单不存在");
if (!"transit".equals(d.getDeliveryStatus()))
throw new RuntimeException("当前状态不允许收货完成");
return mapper.updateDeliveryStatus(id, "history", null, new java.sql.Date(System.currentTimeMillis()), username);
}
@Override
@Transactional
public int recall(Long id) {
BizDeliveryOrder d = mapper.selectBizDeliveryOrderById(id);
if (d == null) throw new RuntimeException("发货单不存在");
if (!"transit".equals(d.getDeliveryStatus()) && !"history".equals(d.getDeliveryStatus()))
throw new RuntimeException("当前状态不允许撤回");
return mapper.updateDeliveryStatus(id, "pending", null, null, "");
}
@Override
@Transactional
public int setCloseDate(Long id, String closeDate, String username) {
if (closeDate == null || closeDate.isEmpty()) {
return mapper.updateDeliveryStatus(id, null, null, null, "");
} else {
return mapper.updateDeliveryStatus(id, null, null, java.sql.Date.valueOf(closeDate), username);
}
}
// ═══════════════════════════════════════════════
// 物料发货记录
// ═══════════════════════════════════════════════
@Override
public List<Map<String, Object>> selectMaterialRecords(Long materialId) {
return mapper.selectMaterialRecords(materialId);
}
}

View File

@@ -7,7 +7,8 @@
<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,
client_id AS clientId, 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>
@@ -54,9 +55,9 @@
</select>
<insert id="insertClientQuote" useGeneratedKeys="true" keyProperty="quoteId">
INSERT INTO biz_client_quote (tenant_id,quote_no,client_name,rfq_id,rfq_no,rfq_title,
INSERT INTO biz_client_quote (tenant_id,quote_no,client_id,client_name,rfq_id,rfq_no,rfq_title,
status,validity_date,total_amount,currency,remark,create_by,create_time)
VALUES (1,#{quoteNo},#{clientName},#{rfqId},#{rfqNo},#{rfqTitle},
VALUES (1,#{quoteNo},#{clientId},#{clientName},#{rfqId},#{rfqNo},#{rfqTitle},
#{status},#{validityDate},#{totalAmount},#{currency},#{remark},#{createBy},NOW())
</insert>
@@ -68,7 +69,7 @@
</insert>
<update id="updateClientQuote">
UPDATE biz_client_quote SET client_name=#{clientName},status=#{status},
UPDATE biz_client_quote SET client_id=#{clientId},client_name=#{clientName},status=#{status},
validity_date=#{validityDate},total_amount=#{totalAmount},
currency=#{currency},remark=#{remark},update_time=NOW()
WHERE quote_id=#{quoteId}

View File

@@ -73,4 +73,36 @@
DELETE FROM biz_delivery_order WHERE do_id IN
<foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<!-- 状态流转更新直接设置字段不受动态SQL null判断影响 -->
<update id="updateDeliveryStatus">
UPDATE biz_delivery_order
SET delivery_status=#{deliveryStatus},
delay_date=#{delayDate},
actual_close_date=#{actualCloseDate},
close_date_set_by=#{closeDateSetBy},
update_time=NOW()
WHERE do_id=#{doId}
</update>
<!-- 物料发货记录多表JOIN追溯 -->
<select id="selectMaterialRecords" resultType="java.util.Map">
SELECT d.do_no,
s.supplier_name,
cl.client_name,
di.quantity,
di.unit_price,
di.total_price,
d.delivery_date,
d.actual_close_date,
d.delivery_status
FROM biz_delivery_order_item di
JOIN biz_delivery_order d ON di.do_id = d.do_id
LEFT JOIN biz_supplier s ON d.supplier_id = s.supplier_id
LEFT JOIN biz_rfq r ON d.rfq_id = r.rfq_id
LEFT JOIN biz_client_quote cq ON r.client_quote_id = cq.quote_id
LEFT JOIN biz_client cl ON cq.client_id = cl.client_id
WHERE di.material_id = #{materialId}
ORDER BY d.create_time DESC
</select>
</mapper>

View File

@@ -4,7 +4,7 @@
**/
/* theme color */
$--color-primary: #1890ff;
$--color-primary: #4A6FA5;
$--color-success: #13ce66;
$--color-warning: #ffba00;
$--color-danger: #ff4949;

View File

@@ -482,3 +482,37 @@
position: relative;
float: right;
}
/* ═══════ 全局按钮统一样式 ═══════ */
.el-button--primary {
background: #4A6FA5;
border-color: #4A6FA5;
border-radius: 4px;
}
.el-button--primary:hover,
.el-button--primary:focus {
background: #5a80b5;
border-color: #5a80b5;
}
.el-button--primary.is-disabled {
background: #8aa3c5;
border-color: #8aa3c5;
}
.el-button--danger {
border-radius: 4px;
}
.el-button--default {
border-radius: 4px;
}
/* 搜索按钮特殊色(浅蓝) */
.search-btn {
background: #409EFF;
border-color: #409EFF;
border-radius: 4px;
color: #fff;
}
.search-btn:hover {
background: #66b1ff;
border-color: #66b1ff;
}

View File

@@ -1,6 +1,6 @@
// base color - ERPNext inspired palette
$blue:#1171c4;
$light-blue:#409EFF;
$blue:#4A6FA5;
$light-blue:#6B8FB5;
$red:#C03639;
$pink: #E65D6E;
$green: #30B08F;
@@ -10,13 +10,13 @@ $panGreen: #30B08F;
// 默认菜单主题风格ERPNext风格白色边栏蓝色主色
$base-menu-color: #555c70;
$base-menu-color-active: #1171c4;
$base-menu-color-active: #4A6FA5;
$base-menu-background: #1a2332;
$base-logo-title-color: #ffffff;
$base-menu-light-color: rgba(0,0,0,.65);
$base-menu-light-background: #ffffff;
$base-logo-light-title-color: #1171c4;
$base-logo-light-title-color: #4A6FA5;
$base-sub-menu-background: #111a27;
$base-sub-menu-hover: rgba(17,113,196,.08);

View File

@@ -1,31 +1,33 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab">
<!-- Tab 1: 客户列表 -->
<div class="client-manage">
<!-- 标签页 -->
<el-tabs v-model="activeTab" class="client-tabs">
<el-tab-pane label="客户列表" name="list">
<div class="toolbar">
<el-input
v-model="queryParams.clientName"
placeholder="搜索名称/编号/联系人"
placeholder="搜索名称 / 编号 / 联系人"
size="small"
clearable
style="width:320px"
style="width:280px"
prefix-icon="el-icon-search"
@keyup.enter.native="handleSearch"
/>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增客户</el-button>
<div class="toolbar-right">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增客户</el-button>
</div>
</div>
<el-table v-loading="loading" :data="clientList" border size="small" style="width:100%">
<el-table-column label="编号" prop="clientNo" width="100" />
<el-table-column label="名称" prop="clientName" min-width="160" />
<el-table-column label="联系人" prop="contact" width="100" />
<el-table-column label="电话" prop="phone" width="130" />
<el-table-column label="城市" prop="city" width="110" />
<el-table-column label="订单数" prop="orderCount" width="80" align="center" />
<el-table-column label="备注" prop="remark" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<el-table v-loading="loading" :data="clientList" border size="small" stripe style="width:100%">
<el-table-column label="编号" prop="clientNo" width="90" />
<el-table-column label="客户名称" prop="clientName" min-width="160" show-overflow-tooltip />
<el-table-column label="联系人" prop="contact" width="90" />
<el-table-column label="电话" prop="phone" width="120" />
<el-table-column label="城市" prop="city" width="100" />
<el-table-column label="订单数" prop="orderCount" width="70" align="center" />
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="130" align="center" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
@@ -36,44 +38,49 @@
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-tab-pane>
<!-- Tab 2: 历史发货单 -->
<el-tab-pane label="历史发货单" name="orders">
<div class="toolbar">
<el-select v-model="orderClientId" filterable placeholder="选择甲方客户" style="width:400px" @change="loadClientOrders" clearable>
<el-select v-model="orderClientId" filterable placeholder="选择甲方客户" style="width:380px" @change="loadClientOrders" clearable>
<el-option v-for="c in clientOptions" :key="c.clientId" :label="c.clientNo + ' | ' + c.clientName" :value="c.clientId" />
</el-select>
<span v-if="orderClientName" style="margin-left:12px;color:#909399;font-size:12px"> {{ orderList.length }} 条记录</span>
<span v-if="orderClientName" class="order-hint"> {{ orderList.length }} 条记录</span>
</div>
<el-table v-loading="orderLoading" :data="orderList" border size="small" style="width:100%">
<el-table-column label="发货单号" prop="doNo" width="150" />
<el-table-column label="供应商" prop="supplierName" min-width="140" />
<el-table v-loading="orderLoading" :data="orderList" border size="small" stripe style="width:100%">
<el-table-column label="发货单号" prop="doNo" width="160" />
<el-table-column label="供应商" prop="supplierName" min-width="130" show-overflow-tooltip />
<el-table-column label="金额" width="130" align="right">
<template slot-scope="scope">¥{{ scope.row.totalAmount }}</template>
<template slot-scope="scope"><span class="amount">¥{{ scope.row.totalAmount }}</span></template>
</el-table-column>
<el-table-column label="交货期" prop="deliveryDate" width="100" />
<el-table-column label="结单日期" prop="actualCloseDate" width="100" />
<el-table-column label="物料数" prop="itemCount" width="70" align="center" />
<el-table-column label="状态" width="100">
<el-table-column label="物料数" prop="itemCount" width="65" align="center" />
<el-table-column label="状态" width="95">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.deliveryStatus)" size="small">{{ statusLabel(scope.row.deliveryStatus) }}</el-tag>
<el-tag :type="statusType(scope.row.deliveryStatus)" size="small" effect="dark">{{ statusLabel(scope.row.deliveryStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<el-table-column label="操作" width="65" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="showOrderDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!orderClientId && !orderLoading" description="请先选择甲方客户" />
<el-empty v-if="orderClientId && !orderList.length && !orderLoading" description="该客户暂无发货记录" />
<div v-if="!orderClientId && !orderLoading" class="empty-box">
<i class="el-icon-document" />
<p>请先选择甲方客户</p>
</div>
<div v-if="orderClientId && !orderList.length && !orderLoading" class="empty-box">
<i class="el-icon-document" />
<p>该客户暂无发货记录</p>
</div>
</el-tab-pane>
</el-tabs>
<!-- 新增/编辑客户弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="600px" append-to-body @close="cancelDialog">
<el-form ref="form" :model="form" :rules="rules" label-width="90px" size="small">
<!-- 新增/编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="620px" append-to-body class="client-dialog" @close="cancelDialog">
<el-form ref="form" :model="form" :rules="rules" label-width="85px" size="small">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户编号" prop="clientNo">
@@ -94,7 +101,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="手机号/固话" />
<el-input v-model="form.phone" placeholder="手机号 / 固话" />
</el-form-item>
</el-col>
</el-row>
@@ -106,7 +113,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="所在城市" prop="city">
<el-input v-model="form.city" placeholder="如 广东深圳" />
<el-input v-model="form.city" placeholder="如广东深圳" />
</el-form-item>
</el-col>
</el-row>
@@ -114,8 +121,8 @@
<el-input v-model="form.address" placeholder="详细地址" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户等级" prop="grade">
<el-col :span="8">
<el-form-item label="等级" prop="grade">
<el-select v-model="form.grade" style="width:100%">
<el-option label="A级" value="A" />
<el-option label="B级" value="B" />
@@ -123,7 +130,7 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" style="width:100%">
<el-option label="正常" value="0" />
@@ -143,45 +150,48 @@
</el-dialog>
<!-- 发货单详情弹窗 -->
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" append-to-body>
<div v-if="detailData" class="detail-card">
<div class="detail-section">
<table class="detail-table">
<tr>
<td class="dt-label">发货单号</td><td class="dt-value"><b>{{ detailData.doNo }}</b></td>
<td class="dt-label">供应商</td><td class="dt-value">{{ detailData.supplierName }}</td>
</tr>
<tr>
<td class="dt-label">总金额</td><td class="dt-value" style="color:#409EFF;font-weight:700">¥{{ detailData.totalAmount }}</td>
<td class="dt-label">状态</td>
<td class="dt-value">
<el-tag :type="statusType(detailData.deliveryStatus)" size="small">{{ statusLabel(detailData.deliveryStatus) }}</el-tag>
</td>
</tr>
<tr>
<td class="dt-label">交货期</td><td class="dt-value">{{ detailData.deliveryDate || '-' }}</td>
<td class="dt-label">结单日期</td><td class="dt-value">{{ detailData.actualCloseDate || '-' }}</td>
</tr>
<tr v-if="detailData.remark">
<td class="dt-label">备注</td><td class="dt-value" colspan="3">{{ detailData.remark }}</td>
</tr>
</table>
</div>
<div class="detail-section">
<div class="section-title">物料明细</div>
<el-table :data="detailData.items || []" border size="small" style="width:100%">
<el-table-column label="物料名称" prop="materialName" min-width="140" />
<el-table-column label="规格" prop="spec" width="120" />
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="数量" prop="quantity" width="80" align="right" />
<el-table-column label="单价" width="100" align="right">
<template slot-scope="s">¥{{ s.row.unitPrice }}</template>
</el-table-column>
<el-table-column label="小计" width="100" align="right">
<template slot-scope="s">¥{{ s.row.totalPrice }}</template>
</el-table-column>
</el-table>
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="800px" append-to-body class="detail-dialog">
<div v-if="detailData">
<div class="detail-grid">
<div class="detail-item">
<span class="dl">发货单号</span>
<span class="dv"><b>{{ detailData.doNo }}</b></span>
</div>
<div class="detail-item">
<span class="dl">供应商</span>
<span class="dv">{{ detailData.supplierName || '-' }}</span>
</div>
<div class="detail-item">
<span class="dl">总金额</span>
<span class="dv" style="color:#409EFF;font-weight:700">¥{{ detailData.totalAmount }}</span>
</div>
<div class="detail-item">
<span class="dl">状态</span>
<span class="dv"><el-tag :type="statusType(detailData.deliveryStatus)" size="small" effect="dark">{{ statusLabel(detailData.deliveryStatus) }}</el-tag></span>
</div>
<div class="detail-item">
<span class="dl">交货期</span>
<span class="dv">{{ detailData.deliveryDate || '-' }}</span>
</div>
<div class="detail-item">
<span class="dl">结单日期</span>
<span class="dv">{{ detailData.actualCloseDate || '-' }}</span>
</div>
</div>
<div v-if="detailData.remark" class="detail-remark">备注{{ detailData.remark }}</div>
<div class="section-bar">物料明细</div>
<el-table :data="detailData.items || []" border size="small" style="width:100%">
<el-table-column label="物料名称" prop="materialName" min-width="140" />
<el-table-column label="规格" prop="spec" width="120" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="数量" prop="quantity" width="80" align="right" />
<el-table-column label="单价" width="100" align="right">
<template slot-scope="s">¥{{ s.row.unitPrice }}</template>
</el-table-column>
<el-table-column label="小计" width="100" align="right">
<template slot-scope="s">¥{{ s.row.totalPrice }}</template>
</el-table-column>
</el-table>
</div>
<div slot="footer">
<el-button @click="detailOpen = false">关闭</el-button>
@@ -198,16 +208,11 @@ export default {
name: "Client",
data() {
return {
// ── Tab ──
activeTab: "list",
// ── 客户列表 ──
loading: false,
clientList: [],
total: 0,
queryParams: { pageNum: 1, pageSize: 20, clientName: "" },
// ── 新增/编辑 ──
dialogOpen: false,
dialogTitle: "",
form: { grade: "B", status: "0" },
@@ -216,135 +221,158 @@ export default {
clientName: [{ required: true, message: "客户名称不能为空", trigger: "blur" }]
},
editId: null,
// ── 历史发货单 ──
orderClientId: null,
orderClientName: "",
orderLoading: false,
orderList: [],
clientOptions: [],
// ── 发货单详情 ──
detailOpen: false,
detailData: null
}
},
created() {
this.getList()
this.loadClientOptions()
},
created() { this.getList(); this.loadClientOptions() },
methods: {
// ═══════════ 客户列表 ═══════════
getList() {
this.loading = true
listClient(this.queryParams).then(r => {
this.clientList = r.rows || []
this.total = r.total || 0
this.loading = false
}).catch(() => { this.loading = false })
},
handleSearch() {
this.queryParams.pageNum = 1
this.getList()
this.loadClientOptions()
},
// ═══════════ 新增/编辑 ═══════════
handleAdd() {
this.editId = null
this.form = { grade: "B", status: "0", clientNo: "", clientName: "", contact: "", phone: "", email: "", city: "", address: "", remark: "" }
this.dialogTitle = "新增客户"
this.dialogOpen = true
},
handleEdit(row) {
this.editId = row.clientId
this.form = { ...row }
this.dialogTitle = "编辑客户"
this.dialogOpen = true
},
cancelDialog() {
this.dialogOpen = false
this.$refs.form && this.$refs.form.clearValidate()
listClient(this.queryParams).then(r => { this.clientList = r.rows || []; this.total = r.total || 0; this.loading = false }).catch(() => { this.loading = false })
},
handleSearch() { this.queryParams.pageNum = 1; this.getList(); this.loadClientOptions() },
handleAdd() { this.editId = null; this.form = { grade: "B", status: "0", clientNo: "", clientName: "", contact: "", phone: "", email: "", city: "", address: "", remark: "" }; this.dialogTitle = "新增客户"; this.dialogOpen = true },
handleEdit(row) { this.editId = row.clientId; this.form = { ...row }; this.dialogTitle = "编辑客户"; this.dialogOpen = true },
cancelDialog() { this.dialogOpen = false; this.$refs.form && this.$refs.form.clearValidate() },
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
this.$refs.form.validate(v => {
if (!v) return
const action = this.editId ? updateClient(this.form) : addClient(this.form)
action.then(() => {
this.$modal.msgSuccess(this.editId ? "修改成功" : "新增成功")
this.dialogOpen = false
this.getList()
}).catch(() => {})
action.then(() => { this.$modal.msgSuccess(this.editId ? "修改成功" : "新增成功"); this.dialogOpen = false; this.getList() }).catch(() => {})
})
},
// ═══════════ 删除 ═══════════
handleDelete(row) {
this.$modal.confirm('确认删除客户 "' + row.clientName + '"').then(() => {
delClient(row.clientId).then(() => {
this.$modal.msgSuccess("删除成功")
this.getList()
})
}).catch(() => {})
},
// ═══════════ 历史发货单 ═══════════
loadClientOptions() {
listClient({ pageNum: 1, pageSize: 999 }).then(r => {
this.clientOptions = r.rows || []
}).catch(() => {})
this.$modal.confirm('确认删除客户 "' + row.clientName + '"').then(() => { delClient(row.clientId).then(() => { this.$modal.msgSuccess("删除成功"); this.getList() }) }).catch(() => {})
},
loadClientOptions() { listClient({ pageNum: 1, pageSize: 999 }).then(r => { this.clientOptions = r.rows || [] }).catch(() => {}) },
loadClientOrders(clientId) {
if (!clientId) {
this.orderList = []
this.orderClientName = ""
return
}
this.orderLoading = true
this.orderList = []
// 获取客户名用于显示
if (!clientId) { this.orderList = []; this.orderClientName = ""; return }
this.orderLoading = true; this.orderList = []
const c = this.clientOptions.find(o => o.clientId === clientId)
this.orderClientName = c ? c.clientName : ""
getClientOrders(clientId).then(r => {
this.orderList = (r.data || []).map(o => ({
...o,
totalAmount: o.totalAmount || o.total_amount,
deliveryDate: o.deliveryDate || o.delivery_date,
actualCloseDate: o.actualCloseDate || o.actual_close_date,
deliveryStatus: o.deliveryStatus || o.delivery_status,
itemCount: o.itemCount || o.item_count
...o, totalAmount: o.totalAmount || o.total_amount, deliveryDate: o.deliveryDate || o.delivery_date,
actualCloseDate: o.actualCloseDate || o.actual_close_date, deliveryStatus: o.deliveryStatus || o.delivery_status, itemCount: o.itemCount || o.item_count
}))
this.orderLoading = false
}).catch(() => { this.orderLoading = false })
},
// ═══════════ 发货单详情 ═══════════
showOrderDetail(row) {
getDelivery(row.doId || row.do_id).then(r => {
this.detailData = r.data
this.detailOpen = true
}).catch(() => {})
getDelivery(row.doId || row.do_id).then(r => { this.detailData = r.data; this.detailOpen = true }).catch(() => {})
},
// ═══════════ 工具方法 ═══════════
statusType(s) {
return { pending: "warning", transit: "primary", history: "success" }[s] || ""
},
statusLabel(s) {
return { pending: "待发", transit: "在途", history: "已收货" }[s] || s || "-"
}
statusType(s) { return { pending: "warning", transit: "primary", history: "success" }[s] || "" },
statusLabel(s) { return { pending: "待发", transit: "在途", history: "已收货" }[s] || s || "-" }
}
}
</script>
<style scoped>
.app-container { background: #fff; padding: 20px; border-radius: 4px; min-height: calc(100vh - 104px); }
.toolbar { margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.detail-card { padding: 0; }
.detail-section { margin-bottom: 20px; }
.section-title { font-size: 14px; font-weight: 700; color: #1a2c4e; margin-bottom: 10px; padding-left: 8px; border-left: 4px solid #1171c4; }
.detail-table { width: 100%; border-collapse: collapse; }
.detail-table td { padding: 8px 12px; border: 1px solid #e4e7ed; }
.dt-label { background: #f5f7fa; color: #606266; font-weight: 600; width: 90px; font-size: 12px; }
.dt-value { color: #303133; font-size: 13px; }
/* ═══════ 整体布局 ═══════ */
.client-manage {
padding: 12px;
background: #f5f7fa;
min-height: calc(100vh - 84px);
}
.client-manage ::v-deep .el-tabs__header {
background: #fff;
padding: 0 16px;
margin: 0;
border-radius: 4px 4px 0 0;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.client-manage ::v-deep .el-tabs__content {
background: #fff;
padding: 16px;
border-radius: 0 0 4px 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
/* ═══════ 工具栏 ═══════ */
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.toolbar-right { margin-left: auto; }
.order-hint {
margin-left: 12px;
font-size: 12px;
color: #909399;
}
/* ═══════ 金额 ═══════ */
.amount {
color: #409EFF;
font-weight: 700;
}
/* ═══════ 空状态 ═══════ */
.empty-box {
text-align: center;
padding: 60px 20px;
color: #c0c4cc;
}
.empty-box i { font-size: 48px; display: block; margin-bottom: 12px; }
.empty-box p { font-size: 14px; margin: 0; }
/* ═══════ 详情弹窗 ═══════ */
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 16px;
}
.detail-item {
display: flex;
border-bottom: 1px solid #ebeef5;
}
.detail-item:nth-last-child(-n+2) { border-bottom: none; }
.detail-item:nth-child(odd) { border-right: 1px solid #ebeef5; }
.dl {
width: 90px;
flex-shrink: 0;
background: #f5f7fa;
padding: 10px 12px;
font-size: 12px;
color: #606266;
font-weight: 600;
border-right: 1px solid #ebeef5;
}
.dv {
padding: 10px 12px;
font-size: 13px;
color: #303133;
flex: 1;
}
.detail-remark {
padding: 8px 12px;
background: #fdf6ec;
border: 1px solid #faecd8;
border-radius: 4px;
font-size: 12px;
color: #e6a23c;
margin-bottom: 16px;
}
.section-bar {
font-size: 13px;
font-weight: 700;
color: #1a2c4e;
padding: 8px 0;
margin-bottom: 10px;
border-bottom: 2px solid #1171c4;
padding-left: 8px;
}
/* ═══════ 弹窗统一样式 ═══════ */
.client-dialog ::v-deep .el-dialog__body { padding: 20px 30px; }
</style>

View File

@@ -28,7 +28,7 @@
size="small"
style="width:100%"
:trigger-on-focus="true"
@select="(item) => { if (item && item.value) form.clientName = item.value; }"
@select="(item) => { if (item && item.value) { form.clientName = item.value; form.clientId = item.clientId; } }"
/>
</el-form-item>
</el-col>
@@ -289,7 +289,8 @@
</template>
<script>
import { getClientQuote, addClientQuote, updateClientQuote, getClientQuoteHistory, getClientNames } from "@/api/bid/clientquote";
import { getClientQuote, addClientQuote, updateClientQuote, getClientQuoteHistory } from "@/api/bid/clientquote";
import { listClient } from "@/api/bid/client";
import { createRfqFromQuote } from "@/api/bid/rfq";
import { listMaterial, getSupplierQuoteReference } from "@/api/bid/material";
import logoImg from "@/assets/logo/logo.png";
@@ -316,7 +317,7 @@ export default {
refLoading: false,
selectedRow: null,
form: {
quoteId: null, quoteNo: "", clientName: "", rfqId: null, rfqNo: "", rfqTitle: "",
quoteId: null, quoteNo: "", clientId: null, clientName: "", rfqId: null, rfqNo: "", rfqTitle: "",
status: "draft", validityDate: "", currency: "CNY", remark: "", totalAmount: 0,
items: []
}
@@ -393,14 +394,14 @@ export default {
window.open(route.href, '_blank');
}).catch(() => {});
},
// 客户名称搜索(自动补全)
// 客户名称搜索(从甲方客户库自动补全)
queryClientSearch(query, cb) {
if (!query || query.length < 1) {
cb([]);
return;
}
getClientNames(query).then(r => {
const list = (r.data || []).map(n => ({ value: n }));
listClient({ clientName: query, pageSize: 20 }).then(r => {
const list = (r.rows || []).map(c => ({
value: c.clientName,
clientId: c.clientId,
clientNo: c.clientNo
}));
cb(list);
}).catch(() => cb([]));
},

View File

@@ -1,148 +1,124 @@
<template>
<div class="app-container clientquote-page">
<!-- 顶部统计卡片 -->
<el-row :gutter="14" class="stat-row">
<div class="cq-page">
<!-- 顶部统计卡片 -->
<el-row :gutter="12" 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 class="stat-card" style="border-top-color:#1171c4">
<div class="stat-body">
<div class="stat-num">{{ stats.total_count || 0 }}</div>
<div class="stat-lbl">报价单总数</div>
</div>
<i class="el-icon-document-copy stat-icon" style="color:#1171c4"></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 class="stat-card" style="border-top-color:#30B08F">
<div class="stat-body">
<div class="stat-num">{{ stats.client_count || 0 }}</div>
<div class="stat-lbl">客户数量</div>
</div>
<i class="el-icon-user stat-icon" style="color:#30B08F"></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 class="stat-card" style="border-top-color:#409EFF">
<div class="stat-body">
<div class="stat-num">¥{{ (stats.total_amount_sum || 0) | money }}</div>
<div class="stat-lbl">报价总金额</div>
</div>
<i class="el-icon-money stat-icon" style="color:#409EFF"></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 class="stat-card" style="border-top-color:#FEC171">
<div class="stat-body">
<div class="stat-num">¥{{ (stats.avg_amount || 0) | money }}</div>
<div class="stat-lbl">平均金额</div>
</div>
<i class="el-icon-s-data stat-icon" style="color:#FEC171"></i>
</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="客户名称">
<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>
<!-- 搜索栏 -->
<div class="search-bar">
<el-input v-model="queryParams.clientName" placeholder="客户名称" clearable size="small" style="width:140px" @keyup.enter.native="handleQuery" />
<el-input v-model="queryParams.quoteNo" placeholder="报价单号" clearable size="small" style="width:140px" @keyup.enter.native="handleQuery" />
<el-select v-model="queryParams.status" placeholder="状态" clearable size="small" style="width:100px">
<el-option label="草稿" value="draft" />
<el-option label="已发送" value="sent" />
<el-option label="已确认" value="confirmed" />
<el-option label="已拒绝" value="rejected" />
</el-select>
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始"
end-placeholder="结束" value-format="yyyy-MM-dd" size="small" style="width:210px" clearable />
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">搜索</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<div class="search-right">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新建报价单</el-button>
<el-button size="small" icon="el-icon-refresh" @click="getList">刷新</el-button>
</div>
</div>
<!-- 工具栏 -->
<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>
<!-- 报价单列表 -->
<el-table v-loading="loading" :data="list" border stripe size="small" highlight-current-row class="cq-table">
<el-table-column label="报价单号" prop="quoteNo" width="165" fixed>
<template slot-scope="s">
<span style="font-weight:600;color:#303133;cursor:pointer" @click="handleView(s.row)">{{ s.row.quoteNo }}</span>
<span class="link-text" @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="总金额" width="130" align="right">
<template slot-scope="s">
<strong style="color:#409EFF;font-size:15px">¥{{ s.row.totalAmount | money }}</strong>
</template>
<template slot-scope="s"><span class="amount">¥{{ s.row.totalAmount | money }}</span></template>
</el-table-column>
<el-table-column label="币种" prop="currency" width="70" align="center" />
<el-table-column label="有效期" width="110" align="center">
<el-table-column label="币种" prop="currency" width="60" align="center" />
<el-table-column label="有效期" width="105" align="center">
<template slot-scope="s">{{ s.row.validityDate | dateFmt }}</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="80" align="center">
<template slot-scope="s">
<div class="status-chip" :class="'status-' + s.row.status">
<i :class="statusIcon(s.row.status)"></i>
{{ statusLabel(s.row.status) }}
</div>
<el-tag :type="statusType(s.row.status)" size="small" 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="280" fixed="right">
<el-table-column label="创建人" prop="createBy" width="90" align="center" />
<el-table-column label="创建时间" prop="createTime" width="155" align="center" />
<el-table-column label="操作" width="260" align="center" fixed="right">
<template slot-scope="s">
<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-s-promotion" style="color:#67C23A" @click="handleCreateRfq(s.row)">生成RFQ</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>
<el-button size="mini" type="text" @click="handleView(s.row)">查看</el-button>
<el-button size="mini" type="text" @click="handleUpdate(s.row)">编辑</el-button>
<el-button size="mini" type="text" @click="handleQuickCreate(s.row)">快速新建</el-button>
<el-button size="mini" type="text" style="color:#67C23A" @click="handleCreateRfq(s.row)">生成RFQ</el-button>
<el-button size="mini" type="text" 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" />
<!-- 创建/编辑对话框 -->
<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">
<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-dialog">
<el-form ref="form" :model="form" :rules="rules" label-width="85px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-col :span="8">
<el-form-item label="客户名称" prop="clientName">
<el-input v-model="form.clientName" placeholder="请输入客户/甲方名称" />
<el-input v-model="form.clientName" placeholder="客户/甲方名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label=" ">
<span style="color:#909399;font-size:12px">RFQ 通过生成RFQ按钮创建自动关联此报价单</span>
</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-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-col :span="4">
<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-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-col :span="4">
<el-form-item label="状态">
<el-select v-model="form.status" style="width:100%">
<el-option label="草稿" value="draft" />
@@ -154,99 +130,51 @@
</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="section-bar">报价明细</div>
<div class="items-table-wrap">
<el-table :data="form.items" border size="small" class="items-table">
<el-table-column type="index" width="40" label="#" />
<el-table-column label="物料名称" width="170">
<el-table-column type="index" width="36" label="#" />
<el-table-column label="物料名称" width="160">
<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>
<el-autocomplete v-model="s.row.materialName" :fetch-suggestions="queryMaterialSearch" placeholder="搜索物料"
size="mini" style="width:100%" :popper-append-to-body="true"
@select="(item) => onMaterialSelect(s.row, item)" />
</template>
</el-table-column>
<el-table-column label="规格型号" width="130">
<template slot-scope="s">
<el-input v-model="s.row.spec" size="mini" placeholder="规格型号" @change="s.row.modelNo = s.row.spec" />
</template>
<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="50">
<template slot-scope="s">
<el-input v-model="s.row.unit" size="mini" />
</template>
<template slot-scope="s"><el-input v-model="s.row.unit" size="mini" /></template>
</el-table-column>
<el-table-column label="数量" width="70">
<template slot-scope="s">
<el-input v-model="s.row.quantity" size="mini" placeholder="0" @input="calcRow(s.row)" />
</template>
<template slot-scope="s"><el-input v-model="s.row.quantity" size="mini" @input="calcRow(s.row)" /></template>
</el-table-column>
<el-table-column label="成本价" width="80">
<template slot-scope="s">
<el-input v-model="s.row.costPrice" size="mini" placeholder="0.00" @input="calcRow(s.row)" />
</template>
<template slot-scope="s"><el-input v-model="s.row.costPrice" size="mini" @input="calcRow(s.row)" /></template>
</el-table-column>
<el-table-column label="报价" width="80">
<template slot-scope="s">
<el-input v-model="s.row.unitPrice" size="mini" placeholder="0.00" @input="calcRow(s.row)" />
</template>
<template slot-scope="s"><el-input v-model="s.row.unitPrice" size="mini" @input="calcRow(s.row)" /></template>
</el-table-column>
<el-table-column label="金额" width="75" align="right">
<template slot-scope="s">
<strong style="color:#409EFF">¥{{ itemTotal(s.row) }}</strong>
</template>
<el-table-column label="金额" width="80" align="right">
<template slot-scope="s"><span class="amount">¥{{ itemTotal(s.row) }}</span></template>
</el-table-column>
<el-table-column label="毛利率" width="60" align="center">
<template slot-scope="s">
<span :style="{ color: marginColor(s.row) }">{{ calcMargin(s.row) }}%</span>
</template>
<template slot-scope="s"><span :style="{ color: marginColor(s.row) }">{{ calcMargin(s.row) }}%</span></template>
</el-table-column>
<el-table-column label="交期" width="95">
<template slot-scope="s">
<el-input v-model="s.row.deliveryDays" size="mini" placeholder="0" />
</template>
<el-table-column label="交期" width="90">
<template slot-scope="s"><el-input v-model="s.row.deliveryDays" size="mini" /></template>
</el-table-column>
<el-table-column label="操作" width="55" align="center">
<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 label="操作" width="50" align="center">
<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>
<span class="form-total-meta">{{ 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-item label="备注" style="margin-top:12px"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogOpen = false">取消</el-button>
@@ -254,18 +182,13 @@
</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="steps-bar">
<div class="step-item" :class="{ active: detailData.status !== 'draft' }"><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-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>
@@ -273,12 +196,11 @@
</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-tag :type="statusType(detailData.status)" size="small" 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>
@@ -287,61 +209,47 @@
<span v-else style="color:#c0c4cc">-</span>
</el-descriptions-item>
<el-descriptions-item label="总金额" :span="3">
<strong style="color:#409EFF;font-size:18px">¥{{ detailData.totalAmount | money }}</strong>
<span class="amount" style="font-size:18px">¥{{ detailData.totalAmount | money }}</span>
</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">
<div class="section-bar">报价明细</div>
<el-table :data="detailData.items || []" border size="small" style="margin-top:10px">
<el-table-column type="index" width="46" label="#" />
<el-table-column label="物料名称" prop="materialName" min-width="150" />
<el-table-column label="规格型号" prop="spec" width="130" />
<el-table-column label="规格" prop="spec" width="130" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="55" align="center" />
<el-table-column label="数量" prop="quantity" width="70" align="right" />
<el-table-column label="成本价" width="90" align="right">
<template slot-scope="s">¥{{ s.row.costPrice | money }}</template>
</el-table-column>
<el-table-column label="单价" width="90" align="right">
<template slot-scope="s">¥{{ s.row.unitPrice | money }}</template>
</el-table-column>
<el-table-column label="金额" width="90" 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="65" align="center" />
<el-table-column label="成本价" width="90" align="right"><template slot-scope="s">¥{{ s.row.costPrice | money }}</template></el-table-column>
<el-table-column label="单价" width="90" align="right"><template slot-scope="s">¥{{ s.row.unitPrice | money }}</template></el-table-column>
<el-table-column label="金额" width="90" align="right"><template slot-scope="s"><span class="amount">¥{{ s.row.totalPrice | money }}</span></template></el-table-column>
<el-table-column label="交期" prop="deliveryDays" width="60" align="center" />
</el-table>
<!-- 关联的RFQ列表 -->
<div style="margin-top:20px">
<div class="section-title" style="margin-bottom:12px">
已生成的采购计划RFQ
<el-button size="mini" type="success" icon="el-icon-s-promotion" style="margin-left:12px"
@click="handleCreateRfq(detailData)">生成RFQ</el-button>
<div class="section-bar" style="margin-bottom:12px">
已生成的RFQ
<el-button size="mini" type="success" style="margin-left:12px" @click="handleCreateRfq(detailData)">生成RFQ</el-button>
</div>
<el-table :data="detailRfqList" v-loading="detailRfqLoading" border size="small">
<el-table-column label="RFQ编号" prop="rfqNo" width="150" />
<el-table-column label="标题" prop="rfqTitle" min-width="160" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center">
<template slot-scope="s">
<el-tag :type="rfqStatusType(s.row.status)" size="small">{{ rfqStatusLabel(s.row.status) }}</el-tag>
</template>
<template slot-scope="s"><el-tag :type="rfqStatusType(s.row.status)" size="small">{{ rfqStatusLabel(s.row.status) }}</el-tag></template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="160" align="center" />
<el-table-column label="操作" width="80" align="center">
<template slot-scope="s">
<el-button type="text" size="small" @click="viewRfqDetail(s.row)">查看</el-button>
</template>
<template slot-scope="s"><el-button type="text" size="small" @click="viewRfqDetail(s.row)">查看</el-button></template>
</el-table-column>
</el-table>
<div v-if="!detailRfqLoading && detailRfqList.length === 0" style="text-align:center;padding:16px;color:#c0c4cc;font-size:13px">
暂未生成采购计划点击上方生成RFQ按钮创建
</div>
<div v-if="!detailRfqLoading && !detailRfqList.length" class="empty-tip">暂未生成采购计划</div>
</div>
</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>
<el-button type="primary" @click="editFromDetail" v-if="detailData">编辑</el-button>
<el-button @click="quickCreateFromDetail" v-if="detailData">快速新建</el-button>
</div>
</el-dialog>
</div>
@@ -361,267 +269,121 @@ export default {
},
data() {
return {
// 列表
loading: false,
list: [],
total: 0,
stats: {},
dateRange: null,
queryParams: {
pageNum: 1, pageSize: 10,
clientName: null, quoteNo: null, status: null,
params: { beginTime: null, endTime: null }
},
// 创建/编辑
dialogOpen: false,
dialogTitle: "",
saving: false,
materialCache: [],
loading: false, list: [], total: 0, stats: {}, dateRange: null,
queryParams: { pageNum: 1, pageSize: 10, clientName: null, quoteNo: null, status: null, params: { beginTime: null, endTime: null } },
dialogOpen: false, dialogTitle: "", saving: false, materialCache: [],
form: { items: [], currency: "CNY", status: "draft" },
rules: {
clientName: [{ required: true, message: "请输入客户名称", trigger: "blur" }]
},
// 详情
detailOpen: false,
detailData: null,
detailRfqList: [],
detailRfqLoading: false
rules: { clientName: [{ required: true, message: "请输入客户名称", trigger: "blur" }] },
detailOpen: false, detailData: null, detailRfqList: [], detailRfqLoading: false
};
},
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();
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(); },
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(r => {
this.list = r.rows || [];
this.total = r.total || 0;
this.loading = false;
}).catch(() => { this.loading = false; });
},
getStats() {
getClientQuoteStatistics(this.queryParams).then(r => {
this.stats = r.data || {};
});
} 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; });
},
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() {
// 跳转到 detail.vue 页面进行新增
this.$router.push('/bid/clientquote/detail');
},
// ===== 编辑 =====
handleUpdate(row) {
// 跳转到 detail.vue 页面进行编辑,传递 quoteId
this.$router.push({ path: '/bid/clientquote/detail', query: { quoteId: row.quoteId } });
},
// ===== 保存 =====
resetQuery() { this.resetForm("queryForm"); this.dateRange = null; this.queryParams.params = { beginTime: null, endTime: null }; this.handleQuery(); },
handleAdd() { this.$router.push('/bid/clientquote/detail'); },
handleUpdate(row) { this.$router.push({ path: '/bid/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; });
});
this.$refs.form.validate(v => { if (!v) return; this.saving = true;
(this.form.quoteId ? updateClientQuote : addClientQuote)(this.form).then(() => { this.$modal.msgSuccess("保存成功"); this.dialogOpen = false; this.getList(); this.getStats(); }).finally(() => { this.saving = false; }); });
},
// ===== 详情查看 =====
handleView(row) {
getClientQuote(row.quoteId).then(r => {
this.detailData = r.data || {};
if (!this.detailData.items) this.detailData.items = [];
this.detailOpen = true;
this.loadRfqForDetail(row.quoteId);
});
getClientQuote(row.quoteId).then(r => { this.detailData = r.data || {}; if (!this.detailData.items) this.detailData.items = []; this.detailOpen = true; this.loadRfqForDetail(row.quoteId); });
},
editFromDetail() {
this.detailOpen = false;
if (this.detailData) {
this.handleUpdate(this.detailData);
}
},
quickCreateFromDetail() {
this.detailOpen = false;
if (this.detailData) this.handleQuickCreate(this.detailData);
},
// ===== 快速新建 =====
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();
// 跳转到 detail.vue 编辑新创建的报价单
if (res.data && res.data.quoteId) {
this.$router.push({ path: '/bid/clientquote/detail', query: { quoteId: res.data.quoteId } });
}
}).catch(() => {});
this.$modal.confirm("确认基于【" + row.quoteNo + "】快速新建?").then(() => quickCreateFromQuote(row.quoteId)).then(r => { this.$modal.msgSuccess("已创建草稿"); this.getList(); this.getStats(); if (r.data && r.data.quoteId) this.$router.push({ path: '/bid/clientquote/detail', query: { quoteId: r.data.quoteId } }); }).catch(() => {});
},
// ===== 生成RFQ =====
handleCreateRfq(row) {
this.$modal.confirm("确认基于报价单【" + row.quoteNo + "】生成采购询价(RFQ").then(() => {
return createRfqFromQuote(row.quoteId);
}).then(res => {
this.detailOpen = false;
this.$modal.msgSuccess("RFQ已创建");
this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: res.data.rfqId, rfqNo: res.data.rfqNo, edit: '1' } });
}).catch(() => {});
},
// ===== 删除 =====
handleDelete(row) {
this.$modal.confirm("确认删除报价单【" + row.quoteNo + "】?").then(() => delClientQuote(row.quoteId))
.then(() => { this.$modal.msgSuccess("删除成功"); this.getList(); this.getStats(); });
},
// ===== RFQ列表详情弹窗中展示 =====
loadRfqForDetail(quoteId) {
this.detailRfqLoading = true;
listRfq({ clientQuoteId: quoteId, pageSize: 50 }).then(r => {
this.detailRfqList = r.rows || [];
this.detailRfqLoading = false;
}).catch(() => { this.detailRfqLoading = false; });
},
viewRfqDetail(rfq) {
this.detailOpen = false;
this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: rfq.rfqId } });
this.$modal.confirm("确认基于【" + row.quoteNo + "】生成RFQ").then(() => createRfqFromQuote(row.quoteId)).then(r => { this.detailOpen = false; this.$modal.msgSuccess("RFQ已创建"); this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: r.data.rfqId, rfqNo: r.data.rfqNo, edit: '1' } }); }).catch(() => {});
},
handleDelete(row) { this.$modal.confirm("确认删除【" + row.quoteNo + "】?").then(() => delClientQuote(row.quoteId)).then(() => { this.$modal.msgSuccess("删除成功"); this.getList(); this.getStats(); }); },
loadRfqForDetail(quoteId) { this.detailRfqLoading = true; listRfq({ clientQuoteId: quoteId, pageSize: 50 }).then(r => { this.detailRfqList = r.rows || []; this.detailRfqLoading = false; }).catch(() => { this.detailRfqLoading = false; }); },
viewRfqDetail(rfq) { this.detailOpen = false; this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: rfq.rfqId } }); },
rfqStatusType(s) { return { draft:"info", published:"warning", closed:"", completed:"success" }[s] || ""; },
rfqStatusLabel(s) { return { draft:"草稿", published:"已发布", closed:"已关闭", completed:"已完成" }[s] || s; },
// ===== 物料搜索 =====
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([]));
if (!query || query.length < 1) { cb(this.materialCache.slice(0, 20)); return; }
listMaterial({ materialName: query, pageSize: 20 }).then(r => { const list = (r.rows || []).map(m => ({ ...m, value: m.materialName + (m.spec ? ' (' + m.spec + ')' : '') })); this.materialCache = list; cb(list.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.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"; }
onMaterialSelect(row, item) { if (!item) return; row.materialId = item.materialId; row.materialName = item.materialName; row.spec = item.spec || ''; row.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 c = parseFloat(row.costPrice)||0; const p = parseFloat(row.unitPrice)||0; if (!p) return "0.0"; return (((p-c)/p)*100).toFixed(1); },
marginColor(row) { const m = parseFloat(this.calcMargin(row)); return m >= 20 ? "#67c23a" : m >= 10 ? "#e6a23c" : "#f56c6c"; },
statusType(s) { return { draft:"info", sent:"primary", confirmed:"success", rejected:"danger" }[s] || ""; },
statusLabel(s) { return { draft:"草稿", sent:"已发送", confirmed:"已确认", rejected:"已拒绝" }[s] || s; }
}
};
</script>
<style lang="scss" scoped>
.clientquote-page { padding-bottom: 30px; }
<style scoped>
/* ═══════ 页面容器 ═══════ */
.cq-page { background: #f5f7fa; padding: 12px; min-height: calc(100vh - 84px); }
/* ── 统计卡片 ── */
.stat-row { margin-bottom: 16px; }
/* ═══════ 统计卡片 ═══════ */
.stat-row { margin-bottom: 12px !important; }
.stat-card {
border-radius: 10px; padding: 18px 20px; position: relative;
overflow: hidden; color: #fff; cursor: default;
background: #fff; border-radius: 4px; border-top: 3px solid #1171c4;
padding: 16px 20px; display: flex; align-items: center; justify-content: space-between;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.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); }
.stat-num { font-size: 26px; font-weight: 700; color: #1a2c4e; line-height: 1.2; }
.stat-lbl { font-size: 12px; color: #8c97a8; margin-top: 4px; }
.stat-icon { font-size: 28px; opacity: 0.5; }
/* ── 搜索 ── */
.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; }
/* ═══════ 搜索栏 ═══════ */
.search-bar {
background: #fff; padding: 12px 16px; border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 12px;
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.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; }
.search-right { margin-left: auto; display: flex; gap: 8px; }
/* ── 表单合计 ── */
/* ═══════ 表格 ═══════ */
.cq-table { box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.link-text { font-weight: 600; color: #303133; cursor: pointer; }
.link-text:hover { color: #1171c4; }
.amount { color: #409EFF; font-weight: 700; }
/* ═══════ 标题装饰条 ═══════ */
.section-bar {
font-size: 13px; font-weight: 700; color: #1a2c4e;
padding: 6px 0 6px 10px; margin-bottom: 10px;
border-left: 4px solid #1171c4;
}
/* ═══════ 弹窗样式 ═══════ */
.cq-dialog ::v-deep .el-dialog__body { padding: 16px 24px; max-height: 70vh; overflow-y: auto; }
.items-table-wrap { border: 1px solid #e4e7ed; border-radius: 4px; overflow-x: auto; }
.items-table-wrap .el-table { border: none !important; }
.items-table-wrap .el-table::before { display: none; }
.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;
text-align: right; padding: 10px 16px; background: #f9fbff;
border: 1px solid #e4e7ed; border-top: none; font-size: 14px; color: #606266;
strong { font-size: 20px; color: #409eff; margin-left: 6px; }
}
.form-total-meta { margin-left: 16px; font-size: 12px; color: #909399; }
/* ── 详情 - 状态流程 ── */
.detail-steps {
/* ═══════ 详情状态流程 ═══════ */
.steps-bar {
display: flex; align-items: center; justify-content: center;
padding: 16px 0 20px; gap: 0;
padding: 12px 0 20px; gap: 0;
}
.step-item {
display: flex; flex-direction: column; align-items: center; gap: 4px;
@@ -635,60 +397,5 @@ export default {
&.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;
}
.empty-tip { text-align: center; padding: 16px; color: #c0c4cc; font-size: 13px; }
</style>

View File

@@ -41,31 +41,26 @@
stripe
style="width:100%"
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 700, fontSize: '13px' }"
:cell-style="{ fontSize: '13px', color: '#606266' }">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="物料编码" prop="materialCode" width="140" header-align="center" align="center" />
<el-table-column label="物料名称" prop="materialName" min-width="150" :show-overflow-tooltip="true" />
<el-table-column label="所属分类" prop="categoryName" width="130" :show-overflow-tooltip="true" />
<el-table-column label="厂家/品牌" prop="brand" width="130" :show-overflow-tooltip="true" />
<el-table-column label="规格型号" prop="spec" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="材质" prop="material" width="90" header-align="center" align="center" />
<el-table-column label="用途" prop="purpose" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="性能参数" width="200" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span v-if="scope.row.performanceParams">{{ parsePerfParams(scope.row.performanceParams) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
:cell-style="{ fontSize: '12px', color: '#606266' }"
size="small">
<el-table-column type="selection" width="44" align="center" />
<el-table-column label="物料编码" prop="materialCode" width="120" header-align="center" align="center" />
<el-table-column label="物料名称" prop="materialName" min-width="130" :show-overflow-tooltip="true" />
<el-table-column label="分类" prop="categoryName" width="100" :show-overflow-tooltip="true" />
<el-table-column label="品牌" prop="brand" width="120" :show-overflow-tooltip="true" />
<el-table-column label="规格型号" prop="spec" min-width="140" :show-overflow-tooltip="true" />
<el-table-column label="材质" prop="material" width="80" :show-overflow-tooltip="true" />
<el-table-column label="用途" prop="purpose" min-width="100" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" width="80">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="210" fixed="right">
<el-table-column label="操作" align="center" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-document" @click="handleDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
<el-button size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -298,6 +293,17 @@ export default {
border-radius: 4px;
}
/* 紧凑表格行 */
.el-table td { padding: 4px 4px !important; }
.el-table th { padding: 6px 4px !important; }
/* 圆角按钮 */
.el-button--mini { border-radius: 4px !important; }
/* 搜索按钮浅蓝 */
.search-btn { background: #409EFF; color: #fff; border: none; border-radius: 4px; }
.search-btn:hover { background: #66b1ff; }
/* 搜索表单样式 */
.el-form--inline .el-form-item {
margin-bottom: 16px;

View File

@@ -1,33 +1,41 @@
<template>
<div class="app-container quotation-page">
<!-- 顶部统计卡片 -->
<el-row :gutter="14" class="stat-row">
<!-- 顶部统计卡片 -->
<el-row :gutter="12" class="stat-row">
<el-col :span="6">
<div class="stat-card stat-all">
<div class="stat-num">{{ stats.total }}</div>
<div class="stat-lbl">全部报价</div>
<i class="el-icon-document stat-icon"></i>
<div class="stat-card" style="border-top-color:#1171c4">
<div class="stat-body">
<div class="stat-num">{{ stats.total || 0 }}</div>
<div class="stat-lbl">全部报价</div>
</div>
<i class="el-icon-document stat-icon" style="color:#1171c4"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-draft">
<div class="stat-num">{{ stats.draft }}</div>
<div class="stat-lbl">草稿</div>
<i class="el-icon-edit-outline stat-icon"></i>
<div class="stat-card" style="border-top-color:#909399">
<div class="stat-body">
<div class="stat-num">{{ stats.draft || 0 }}</div>
<div class="stat-lbl">草稿</div>
</div>
<i class="el-icon-edit-outline stat-icon" style="color:#909399"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-submitted">
<div class="stat-num">{{ stats.submitted }}</div>
<div class="stat-lbl">待处理</div>
<i class="el-icon-time stat-icon"></i>
<div class="stat-card" style="border-top-color:#e6a23c">
<div class="stat-body">
<div class="stat-num">{{ stats.submitted || 0 }}</div>
<div class="stat-lbl">待处理</div>
</div>
<i class="el-icon-time stat-icon" style="color:#e6a23c"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-accepted">
<div class="stat-num">{{ stats.accepted }}</div>
<div class="stat-lbl">已采纳</div>
<i class="el-icon-circle-check stat-icon"></i>
<div class="stat-card" style="border-top-color:#67c23a">
<div class="stat-body">
<div class="stat-num">{{ stats.accepted || 0 }}</div>
<div class="stat-lbl">已采纳</div>
</div>
<i class="el-icon-circle-check stat-icon" style="color:#67c23a"></i>
</div>
</el-col>
</el-row>
@@ -550,18 +558,15 @@ export default {
.quotation-page { padding-bottom: 30px; }
/* ── 顶部统计卡片 ── */
.stat-row { margin-bottom: 16px; }
.stat-row { margin-bottom: 12px !important; }
.stat-card {
border-radius: 10px; padding: 18px 20px; position: relative;
overflow: hidden; color: #fff; cursor: default;
background: #fff; border-radius: 4px; border-top: 3px solid #1171c4;
padding: 16px 20px; display: flex; align-items: center; justify-content: space-between;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.stat-num { font-size: 32px; 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-draft { background: linear-gradient(135deg, #909399, #b0b3b8); }
.stat-submitted{ background: linear-gradient(135deg, #e6a23c, #f0c040); }
.stat-accepted { background: linear-gradient(135deg, #67c23a, #85ce61); }
.stat-num { font-size: 26px; font-weight: 700; color: #1a2c4e; line-height: 1.2; }
.stat-lbl { font-size: 12px; color: #8c97a8; margin-top: 4px; }
.stat-icon { font-size: 28px; opacity: 0.5; }
/* ── 搜索 ── */
.search-card { ::v-deep .el-card__body { padding: 16px 20px 8px; } }