feat(bid): 完成招投标业务模块多需求迭代
本次提交包含多项功能改进与业务优化: 1. 全局主题色替换为#4A6FA5,统一前端UI风格 2. 新增客户报价单clientId字段,完善客户报价数据结构 3. 实现发货单状态流转功能,支持发货、完成、撤回、设置结单日期操作 4. 新增物料发货记录多表关联查询功能 5. 优化客户管理页面UI布局与交互体验 6. 修复客户报价表单自动补全逻辑,关联clientId与clientName 7. 补充租户ID自动填充逻辑,完善多租户数据隔离
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
**/
|
||||
|
||||
/* theme color */
|
||||
$--color-primary: #1890ff;
|
||||
$--color-primary: #4A6FA5;
|
||||
$--color-success: #13ce66;
|
||||
$--color-warning: #ffba00;
|
||||
$--color-danger: #ff4949;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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([]));
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
Reference in New Issue
Block a user