feat(bid): 完成批量业务优化与功能完善
1. 统一所有表格操作列样式,移除固定宽度避免布局溢出 2. 新增报价单自动编号与脏数据清理功能 3. 优化订单状态筛选与展示逻辑,新增closed状态支持 4. 完善操作日志管理,新增统计分析与详情查看功能 5. 优化报价单流程,调整提交审批逻辑与权限控制 6. 修复客户端订单查询SQL,优化关联查询逻辑 7. 新增报价单提交时自动更新提交时间的功能
This commit is contained in:
@@ -7,6 +7,7 @@ import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.system.mapper.bid.BizApprovalPendingMapper;
|
||||
import com.ruoyi.system.service.bid.IBizApprovalActionService;
|
||||
import com.ruoyi.system.service.bid.IBizQuotationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -18,6 +19,7 @@ public class BizApprovalActionController extends BaseController {
|
||||
|
||||
@Autowired private IBizApprovalActionService service;
|
||||
@Autowired private BizApprovalPendingMapper pendingMapper;
|
||||
@Autowired private IBizQuotationService quotationService;
|
||||
|
||||
@GetMapping("/pending")
|
||||
public AjaxResult pending() {
|
||||
@@ -27,7 +29,12 @@ public class BizApprovalActionController extends BaseController {
|
||||
@Log(title = "提交审批", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/submit/{bizType}/{id}")
|
||||
public AjaxResult submit(@PathVariable String bizType, @PathVariable Long id) {
|
||||
return toAjax(service.submit(bizType, id, getUsername()));
|
||||
int rows = service.submit(bizType, id, getUsername());
|
||||
// 报价单提交审批时同步更新 submit_time
|
||||
if ("QUOTATION".equals(bizType) && rows > 0) {
|
||||
quotationService.updateSubmitTime(id);
|
||||
}
|
||||
return toAjax(rows);
|
||||
}
|
||||
|
||||
@Log(title = "审批通过", businessType = BusinessType.UPDATE)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
package com.ruoyi.web.controller.bid;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.SysOperLog;
|
||||
import com.ruoyi.system.service.ISysOperLogService;
|
||||
|
||||
@@ -18,6 +27,67 @@ public class BizOperationLogController extends BaseController {
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysOperLog log) {
|
||||
startPage();
|
||||
return getDataTable(operLogService.selectOperLogList(log));
|
||||
List<SysOperLog> list = operLogService.selectOperLogList(log);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:operationlog:query')")
|
||||
@GetMapping("/{operId}")
|
||||
public AjaxResult getInfo(@PathVariable("operId") Long operId) {
|
||||
return success(operLogService.selectOperLogById(operId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:operationlog:remove')")
|
||||
@Log(title = "操作日志", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{operIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] operIds) {
|
||||
return toAjax(operLogService.deleteOperLogByIds(operIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:operationlog:remove')")
|
||||
@Log(title = "操作日志", businessType = BusinessType.CLEAN)
|
||||
@DeleteMapping("/clean")
|
||||
public AjaxResult clean() {
|
||||
operLogService.cleanOperLog();
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:operationlog:export')")
|
||||
@Log(title = "操作日志", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, SysOperLog operLog) {
|
||||
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
|
||||
ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
|
||||
util.exportExcel(response, list, "操作日志");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:operationlog:list')")
|
||||
@GetMapping("/stats")
|
||||
public AjaxResult stats(SysOperLog log) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Map<String, Object> basic = operLogService.selectOperLogStats(log);
|
||||
if (basic != null) {
|
||||
result.put("basic", basic);
|
||||
} else {
|
||||
Map<String, Object> empty = new HashMap<>();
|
||||
empty.put("total_count", 0);
|
||||
empty.put("error_count", 0);
|
||||
empty.put("today_count", 0);
|
||||
empty.put("user_count", 0);
|
||||
empty.put("avg_cost_time", 0);
|
||||
empty.put("module_count", 0);
|
||||
result.put("basic", empty);
|
||||
}
|
||||
result.put("modules", operLogService.selectModuleStats(log));
|
||||
result.put("businessTypes", operLogService.selectBusinessTypeStats(log));
|
||||
result.put("trend", operLogService.selectDailyTrend(log));
|
||||
result.put("errors", operLogService.selectErrorStats(log));
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:operationlog:list')")
|
||||
@GetMapping("/modules")
|
||||
public AjaxResult getModules() {
|
||||
return success(operLogService.selectModuleStats(new SysOperLog()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ public class BizQuotationController extends BaseController {
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody BizQuotation q) {
|
||||
q.setCreateBy(getUsername());
|
||||
Long tenantId = getDeptId();
|
||||
q.setTenantId(tenantId != null ? tenantId : 1L);
|
||||
// 供应商新建报价时自动设置 supplier_id
|
||||
if (SecurityUtils.hasRole("supplier")) {
|
||||
BizSupplier supplier = supplierService.selectBizSupplierByUserId(SecurityUtils.getUserId());
|
||||
@@ -86,13 +88,24 @@ public class BizQuotationController extends BaseController {
|
||||
return toAjax(service.rejectQuotation(quotationId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:quotation:remove') || @ss.hasRole('supplier')")
|
||||
@PreAuthorize("@ss.hasPermi('bid:quotation:remove')")
|
||||
@Log(title = "报价单", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{quotationIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] quotationIds) {
|
||||
return toAjax(service.deleteBizQuotationByIds(quotationIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理无编号的脏数据(历史遗留数据,quote_no IS NULL 或空字符串)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('bid:quotation:remove')")
|
||||
@Log(title = "报价单", businessType = BusinessType.CLEAN)
|
||||
@DeleteMapping("/clean-null-quote")
|
||||
public AjaxResult cleanNullQuote() {
|
||||
int n = service.deleteBizQuotationByNoQuoteNo();
|
||||
return success(n > 0 ? "已清理 " + n + " 条无编号报价单" : "没有需要清理的无编号报价单");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按供应商ID查询报价明细(展开为每行一条物料,支持搜索过滤)
|
||||
* 用于供应商管理页面的"报价历史"Tab
|
||||
|
||||
@@ -45,4 +45,44 @@ public interface SysOperLogMapper
|
||||
* 清空操作日志
|
||||
*/
|
||||
public void cleanOperLog();
|
||||
|
||||
/**
|
||||
* 查询操作日志统计信息
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 统计结果
|
||||
*/
|
||||
public java.util.Map<String, Object> selectOperLogStats(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 按模块统计操作日志
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 各模块统计列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectModuleStats(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 按操作类型统计
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 各操作类型统计列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectBusinessTypeStats(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 查询每日操作趋势
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 每日趋势列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectDailyTrend(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 按模块统计异常日志
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 各模块异常统计列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectErrorStats(SysOperLog operLog);
|
||||
}
|
||||
|
||||
@@ -10,4 +10,8 @@ public interface BizQuotationMapper {
|
||||
int updateBizQuotation(BizQuotation record);
|
||||
int deleteBizQuotationById(Long id);
|
||||
int deleteBizQuotationByIds(Long[] ids);
|
||||
/** 自动生成报价单号:Q-YYYYMM-NNN */
|
||||
String selectNextQuoteNo();
|
||||
/** 查询所有无编号报价单ID(用于清理脏数据) */
|
||||
List<Long> selectIdsByQuoteNoNull();
|
||||
}
|
||||
|
||||
@@ -45,4 +45,44 @@ public interface ISysOperLogService
|
||||
* 清空操作日志
|
||||
*/
|
||||
public void cleanOperLog();
|
||||
|
||||
/**
|
||||
* 查询操作日志统计信息
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 统计结果
|
||||
*/
|
||||
public java.util.Map<String, Object> selectOperLogStats(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 按模块统计
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 各模块统计列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectModuleStats(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 按操作类型统计
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 各类型统计列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectBusinessTypeStats(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 查询每日操作趋势
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 每日趋势列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectDailyTrend(SysOperLog operLog);
|
||||
|
||||
/**
|
||||
* 按模块统计异常日志
|
||||
*
|
||||
* @param operLog 操作日志对象(带筛选条件)
|
||||
* @return 各模块异常统计列表
|
||||
*/
|
||||
public java.util.List<java.util.Map<String, Object>> selectErrorStats(SysOperLog operLog);
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ public interface IBizQuotationService {
|
||||
int rejectQuotation(Long quotationId);
|
||||
int deleteBizQuotationById(Long id);
|
||||
int deleteBizQuotationByIds(Long[] ids);
|
||||
int deleteBizQuotationByNoQuoteNo();
|
||||
void updateSubmitTime(Long quotationId);
|
||||
List<BizQuotationItem> selectItemsByQuotationId(Long quotationId);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ public class BizQuotationServiceImpl implements IBizQuotationService {
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertBizQuotation(BizQuotation q) {
|
||||
if (q.getQuoteNo() == null || q.getQuoteNo().isBlank()) {
|
||||
q.setQuoteNo(mapper.selectNextQuoteNo());
|
||||
}
|
||||
if (q.getStatus() == null) {
|
||||
q.setStatus("draft");
|
||||
}
|
||||
int rows = mapper.insertBizQuotation(q);
|
||||
saveItems(q);
|
||||
return rows;
|
||||
@@ -38,6 +44,13 @@ public class BizQuotationServiceImpl implements IBizQuotationService {
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateBizQuotation(BizQuotation q) {
|
||||
// 对历史遗留的空编号记录,编辑时自动补号
|
||||
if (q.getQuoteNo() == null || q.getQuoteNo().isBlank()) {
|
||||
BizQuotation existing = mapper.selectBizQuotationById(q.getQuotationId());
|
||||
if (existing != null && (existing.getQuoteNo() == null || existing.getQuoteNo().isBlank())) {
|
||||
q.setQuoteNo(mapper.selectNextQuoteNo());
|
||||
}
|
||||
}
|
||||
itemMapper.deleteByQuotationId(q.getQuotationId());
|
||||
saveItems(q);
|
||||
return mapper.updateBizQuotation(q);
|
||||
@@ -48,6 +61,7 @@ public class BizQuotationServiceImpl implements IBizQuotationService {
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (BizQuotationItem item : q.getItems()) {
|
||||
item.setQuotationId(q.getQuotationId());
|
||||
if (item.getRfqItemId() == null) item.setRfqItemId(0L);
|
||||
if (item.getUnitPrice() != null && item.getQuantity() != null) {
|
||||
item.setTotalPrice(item.getUnitPrice().multiply(item.getQuantity()));
|
||||
total = total.add(item.getTotalPrice());
|
||||
@@ -85,12 +99,31 @@ public class BizQuotationServiceImpl implements IBizQuotationService {
|
||||
return mapper.updateBizQuotation(q);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSubmitTime(Long quotationId) {
|
||||
BizQuotation q = new BizQuotation();
|
||||
q.setQuotationId(quotationId);
|
||||
q.setSubmitTime(new Date());
|
||||
mapper.updateBizQuotation(q);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteBizQuotationById(Long id) { return mapper.deleteBizQuotationById(id); }
|
||||
|
||||
@Override
|
||||
public int deleteBizQuotationByIds(Long[] ids) { return mapper.deleteBizQuotationByIds(ids); }
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int deleteBizQuotationByNoQuoteNo() {
|
||||
List<Long> ids = mapper.selectIdsByQuoteNoNull();
|
||||
if (ids.isEmpty()) return 0;
|
||||
for (Long id : ids) {
|
||||
itemMapper.deleteByQuotationId(id);
|
||||
}
|
||||
return mapper.deleteBizQuotationByIds(ids.toArray(new Long[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizQuotationItem> selectItemsByQuotationId(Long quotationId) {
|
||||
return itemMapper.selectItemsByQuotationId(quotationId);
|
||||
|
||||
@@ -73,4 +73,34 @@ public class SysOperLogServiceImpl implements ISysOperLogService
|
||||
{
|
||||
operLogMapper.cleanOperLog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Map<String, Object> selectOperLogStats(SysOperLog operLog)
|
||||
{
|
||||
return operLogMapper.selectOperLogStats(operLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<java.util.Map<String, Object>> selectModuleStats(SysOperLog operLog)
|
||||
{
|
||||
return operLogMapper.selectModuleStats(operLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<java.util.Map<String, Object>> selectBusinessTypeStats(SysOperLog operLog)
|
||||
{
|
||||
return operLogMapper.selectBusinessTypeStats(operLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<java.util.Map<String, Object>> selectDailyTrend(SysOperLog operLog)
|
||||
{
|
||||
return operLogMapper.selectDailyTrend(operLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<java.util.Map<String, Object>> selectErrorStats(SysOperLog operLog)
|
||||
{
|
||||
return operLogMapper.selectErrorStats(operLog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,19 +76,21 @@
|
||||
</delete>
|
||||
|
||||
<select id="selectClientDeliveryOrders" resultType="java.util.Map">
|
||||
SELECT d.do_no,
|
||||
d.delivery_date,
|
||||
d.delay_date,
|
||||
d.actual_close_date,
|
||||
d.delivery_status,
|
||||
d.total_amount,
|
||||
s.supplier_name,
|
||||
(SELECT COUNT(*) FROM biz_delivery_order_item WHERE do_id = d.do_id) AS item_count
|
||||
FROM biz_client_quote cq
|
||||
JOIN biz_rfq r ON r.client_quote_id = cq.quote_id
|
||||
JOIN biz_delivery_order d ON d.rfq_id = r.rfq_id
|
||||
SELECT d.do_id AS doId,
|
||||
d.do_no AS doNo,
|
||||
d.delivery_date AS deliveryDate,
|
||||
d.delay_date AS delayDate,
|
||||
d.actual_close_date AS actualCloseDate,
|
||||
d.delivery_status AS deliveryStatus,
|
||||
d.total_amount AS totalAmount,
|
||||
d.remark,
|
||||
s.supplier_name AS supplierName,
|
||||
(SELECT COUNT(*) FROM biz_delivery_order_item WHERE do_id = d.do_id) AS itemCount
|
||||
FROM biz_delivery_order d
|
||||
LEFT JOIN biz_supplier s ON d.supplier_id = s.supplier_id
|
||||
WHERE cq.client_id = #{clientId}
|
||||
WHERE d.client_quote_id IN (
|
||||
SELECT cq.quote_id FROM biz_client_quote cq WHERE cq.client_id = #{clientId}
|
||||
)
|
||||
ORDER BY d.create_time DESC
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<if test="query.type != null and query.type != ''"> AND d.type=#{query.type}</if>
|
||||
<if test="query.doNo != null and query.doNo != ''"> AND d.do_no LIKE CONCAT('%',#{query.doNo},'%')</if>
|
||||
<if test="query.supplierId != null"> AND d.supplier_id=#{query.supplierId}</if>
|
||||
<if test="query.deliveryStatus != null and query.deliveryStatus != ''"> AND d.delivery_status=#{query.deliveryStatus}</if>
|
||||
<if test="query.deliveryStatus != null and query.deliveryStatus != ''"> AND FIND_IN_SET(d.delivery_status, #{query.deliveryStatus})</if>
|
||||
<if test="query.supplierName != null and query.supplierName != ''"> AND s.supplier_name LIKE CONCAT('%',#{query.supplierName},'%')</if>
|
||||
<if test="query.clientName != null and query.clientName != ''"> AND cl.client_name LIKE CONCAT('%',#{query.clientName},'%')</if>
|
||||
</where>
|
||||
|
||||
@@ -66,11 +66,21 @@
|
||||
<if test="status != null">status=#{status},</if>
|
||||
<if test="note != null">note=#{note},</if>
|
||||
<if test="submitTime != null">submit_time=#{submitTime},</if>
|
||||
<if test="quoteNo != null and quoteNo != ''">quote_no=#{quoteNo},</if>
|
||||
update_time=NOW()
|
||||
</set>
|
||||
WHERE quotation_id=#{quotationId}
|
||||
</update>
|
||||
|
||||
<select id="selectNextQuoteNo" resultType="String">
|
||||
SELECT CONCAT('Q-', DATE_FORMAT(NOW(),'%Y%m'), '-', LPAD(IFNULL(MAX(CAST(SUBSTRING_INDEX(quote_no,'-',-1) AS UNSIGNED)),0)+1,3,'0'))
|
||||
FROM biz_quotation WHERE quote_no LIKE CONCAT('Q-', DATE_FORMAT(NOW(),'%Y%m'), '%')
|
||||
</select>
|
||||
|
||||
<select id="selectIdsByQuoteNoNull" resultType="Long">
|
||||
SELECT quotation_id FROM biz_quotation WHERE quote_no IS NULL OR quote_no = ''
|
||||
</select>
|
||||
|
||||
<delete id="deleteBizQuotationById">DELETE FROM biz_quotation WHERE quotation_id=#{id}</delete>
|
||||
<delete id="deleteBizQuotationByIds">
|
||||
DELETE FROM biz_quotation WHERE quotation_id IN
|
||||
|
||||
@@ -82,6 +82,110 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<update id="cleanOperLog">
|
||||
truncate table sys_oper_log
|
||||
</update>
|
||||
</update>
|
||||
|
||||
<select id="selectOperLogStats" resultType="java.util.Map">
|
||||
SELECT
|
||||
COUNT(*) AS total_count,
|
||||
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS error_count,
|
||||
SUM(CASE WHEN DATE(oper_time) = CURDATE() THEN 1 ELSE 0 END) AS today_count,
|
||||
COUNT(DISTINCT oper_name) AS user_count,
|
||||
ROUND(AVG(cost_time), 0) AS avg_cost_time,
|
||||
COUNT(DISTINCT title) AS module_count
|
||||
FROM sys_oper_log
|
||||
<where>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND (title LIKE CONCAT('%', #{title}, '%') OR oper_name LIKE CONCAT('%', #{operName}, '%'))
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<select id="selectModuleStats" resultType="java.util.Map">
|
||||
SELECT title AS module_name, COUNT(*) AS count
|
||||
FROM sys_oper_log
|
||||
<where>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
AND oper_time >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
AND oper_time <= #{params.endTime}
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY title
|
||||
ORDER BY count DESC
|
||||
LIMIT 20
|
||||
</select>
|
||||
|
||||
<select id="selectBusinessTypeStats" resultType="java.util.Map">
|
||||
SELECT business_type AS type, COUNT(*) AS count
|
||||
FROM sys_oper_log
|
||||
<where>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
AND oper_time >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
AND oper_time <= #{params.endTime}
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY business_type
|
||||
ORDER BY count DESC
|
||||
</select>
|
||||
|
||||
<select id="selectDailyTrend" resultType="java.util.Map">
|
||||
SELECT DATE_FORMAT(oper_time, '%Y-%m-%d') AS day, COUNT(*) AS count
|
||||
FROM sys_oper_log
|
||||
<where>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
AND oper_time >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
AND oper_time <= #{params.endTime}
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY DATE_FORMAT(oper_time, '%Y-%m-%d')
|
||||
ORDER BY day DESC
|
||||
LIMIT 14
|
||||
</select>
|
||||
|
||||
<select id="selectErrorStats" resultType="java.util.Map">
|
||||
SELECT title AS module_name, COUNT(*) AS error_count
|
||||
FROM sys_oper_log
|
||||
WHERE status = 1
|
||||
<if test="operName != null and operName != ''">
|
||||
AND oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
AND oper_time >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
AND oper_time <= #{params.endTime}
|
||||
</if>
|
||||
GROUP BY title
|
||||
ORDER BY error_count DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -1,3 +1,16 @@
|
||||
import request from '@/utils/request'
|
||||
const baseUrl = '/bid/operationLog'
|
||||
|
||||
export const listOperationLog = (params) => request({ url: baseUrl + '/list', method: 'get', params })
|
||||
|
||||
export const getOperationLog = (operId) => request({ url: baseUrl + '/' + operId, method: 'get' })
|
||||
|
||||
export const deleteOperationLog = (operIds) => request({ url: baseUrl + '/' + operIds, method: 'delete' })
|
||||
|
||||
export const cleanOperationLog = () => request({ url: baseUrl + '/clean', method: 'delete' })
|
||||
|
||||
export const exportOperationLog = (params) => request({ url: baseUrl + '/export', method: 'post', data: params, responseType: 'blob' })
|
||||
|
||||
export const getOperationLogStats = (params) => request({ url: baseUrl + '/stats', method: 'get', params })
|
||||
|
||||
export const getOperationLogModules = () => request({ url: baseUrl + '/modules', method: 'get' })
|
||||
|
||||
@@ -9,6 +9,9 @@ export const acceptQuotation = (id) => request({ url: baseUrl + '/accept/' + id,
|
||||
export const rejectQuotation = (id) => request({ url: baseUrl + '/reject/' + id, method: 'put' })
|
||||
export const delQuotation = (ids) => request({ url: baseUrl + '/' + ids, method: 'delete' })
|
||||
|
||||
// 清理无报价单号的脏数据(admin用)
|
||||
export const cleanNullQuoteNo = () => request({ url: baseUrl + '/clean-null-quote', method: 'delete' })
|
||||
|
||||
// 按供应商ID查询报价明细(展开为每行一条物料,支持搜索过滤参数:materialName, quoteNo, quoteStatus, beginTime, endTime)
|
||||
export const getSupplierQuoteItems = (params) => {
|
||||
const { supplierId, ...query } = params;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="操作" align="center" width="160">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['bid:approval:edit']">编辑</el-button>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template><el-tag type="warning">审批中</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-button size="mini" type="text" style="color:#67C23A" @click="handleApprove(s.row)">通过</el-button>
|
||||
<el-button size="mini" type="text" style="color:#F56C6C" @click="handleReject(s.row)">驳回</el-button>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="200">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)">新增子类</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<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">
|
||||
<el-table-column label="操作" class-name="col-ops" 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>
|
||||
@@ -274,18 +274,15 @@ export default {
|
||||
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
|
||||
}))
|
||||
this.orderList = r.data || []
|
||||
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(() => {})
|
||||
},
|
||||
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", closed: "info" }[s] || "" },
|
||||
statusLabel(s) { return { pending: "待发", transit: "在途", history: "已签收", closed: "已结单" }[s] || s || "-" }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" align="center">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-button size="mini" type="text" @click="handleView(s.row)">详情</el-button>
|
||||
<el-button size="mini" type="text" style="color:#E6A23C" @click="handleSubmitApproval(s.row)" v-if="s.row.deliveryStatus==='pending' || s.row.deliveryStatus==='rejected'">提交审批</el-button>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<el-table-column label="配送差异" width="90" align="center"><template slot-scope="s"><span :class="diffClass(s)">{{ diffLabel(s.row) }}</span></template></el-table-column>
|
||||
<el-table-column label="物料" prop="itemCount" width="55" align="center" />
|
||||
<el-table-column label="状态" width="85" align="center"><el-tag type="success" size="small" effect="dark">已签收</el-tag></el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center"><template slot-scope="s"><el-button size="mini" type="text" @click="handleView(s.row)">详情</el-button><el-button size="mini" type="text" @click="handleRecall(s.row)">撤回</el-button></template></el-table-column>
|
||||
<el-table-column label="操作" class-name="col-ops" align="center"><template slot-scope="s"><el-button size="mini" type="text" @click="handleView(s.row)">详情</el-button><el-button size="mini" type="text" @click="handleRecall(s.row)">撤回</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total>0" :total="total" :page.sync="q.pageNum" :limit.sync="q.pageSize" @pagination="getList" />
|
||||
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="780px" append-to-body>
|
||||
@@ -47,7 +47,7 @@
|
||||
import { listDelivery, getDelivery, recallDelivery } from "@/api/bid/delivery"
|
||||
export default {
|
||||
name: "ClientDeliverySigned",
|
||||
data() { return { loading: false, list: [], total: 0, q: { pageNum: 1, pageSize: 20, type: "client", deliveryStatus: "history", doNo: "", clientName: "" }, detailOpen: false, detailData: null }},
|
||||
data() { return { loading: false, list: [], total: 0, q: { pageNum: 1, pageSize: 20, type: "client", deliveryStatus: "history,closed", doNo: "", clientName: "" }, detailOpen: false, detailData: null }},
|
||||
created() { this.getList() },
|
||||
methods: {
|
||||
getList() { this.loading=true; listDelivery(this.q).then(r=>{this.list=(r.rows||[]).map(d=>({...d,deliveryDate:d.deliveryDate?d.deliveryDate.substring(0,10):'',actualCloseDate:d.actualCloseDate?d.actualCloseDate.substring(0,10):''}));this.total=r.total||0;this.loading=false}).catch(()=>{this.loading=false}) },
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<el-table-column label="交货期" prop="deliveryDate" width="95" align="center" />
|
||||
<el-table-column label="物料" prop="itemCount" width="55" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center"><el-tag type="primary" size="small" effect="dark">运输中</el-tag></el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-button size="mini" type="text" @click="handleView(s.row)">详情</el-button>
|
||||
<el-button size="mini" type="text" style="color:#67C23A" @click="handleSign(s.row)">甲方签收</el-button>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" width="160" align="center" />
|
||||
<el-table-column label="操作" align="center" width="180">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-button type="primary" size="mini" icon="el-icon-data-analysis"
|
||||
@click="goCompare(s.row)">进入比价</el-button>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="处理结果" prop="resolution" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="提交时间" prop="createTime" width="160" />
|
||||
<el-table-column label="操作" align="center" width="150">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleResolve(scope.row)" v-if="scope.row.status==='pending'||scope.row.status==='processing'" style="color:#67C23A">处理</el-button>
|
||||
<el-button size="mini" type="text" @click="handleSubmitApproval(scope.row)" v-if="scope.row.status==='pending'" style="color:#E6A23C">提交审批</el-button>
|
||||
|
||||
180
ruoyi-ui/src/views/bid/operationLog/detail.vue
Normal file
180
ruoyi-ui/src/views/bid/operationLog/detail.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<el-dialog title="操作日志详情" :visible.sync="visible" width="780px" append-to-body>
|
||||
<div v-if="form.operId" class="detail-wrap">
|
||||
<!-- 基本信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-info" style="color:#409EFF"></i> 基本信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">日志编号:</span><span class="detail-value">{{ form.operId }}</span></div>
|
||||
<div class="detail-item"><span class="detail-label">操作模块:</span><span class="detail-value"><el-tag size="mini">{{ form.title }}</el-tag></span></div>
|
||||
<div class="detail-item"><span class="detail-label">业务类型:</span><span class="detail-value"><span :style="{color:typeColor(form.businessType),fontWeight:600}">{{ typeLabel(form.businessType) }}</span></span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">操作时间:</span><span class="detail-value">{{ formatDate(form.operTime) }}</span></div>
|
||||
<div class="detail-item"><span class="detail-label">执行状态:</span>
|
||||
<el-tag :type="form.status === 0 ? 'success' : 'danger'" size="mini">{{ form.status === 0 ? '正常' : '异常' }}</el-tag>
|
||||
</div>
|
||||
<div class="detail-item"><span class="detail-label">请求耗时:</span><span class="detail-value">{{ form.costTime }} 毫秒</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 操作人员 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-user" style="color:#E6A23C"></i> 操作人员</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">用户名:</span><span class="detail-value">{{ form.operName || '-' }}</span></div>
|
||||
<div class="detail-item" v-if="form.deptName"><span class="detail-label">所属部门:</span><span class="detail-value">{{ form.deptName }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">IP地址:</span><span class="detail-value">{{ form.operIp || '-' }}</span></div>
|
||||
<div class="detail-item"><span class="detail-label">访问地点:</span><span class="detail-value">{{ form.operLocation || '-' }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 请求信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-sort" style="color:#67C23A"></i> 请求信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">请求地址:</span>
|
||||
<span class="detail-value">
|
||||
<el-tag :type="methodType(form.requestMethod)" size="mini">{{ form.requestMethod || '-' }}</el-tag>
|
||||
{{ form.operUrl || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div class="detail-item"><span class="detail-label">执行方法:</span><span class="detail-value mono">{{ form.method || '-' }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 请求参数 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-upload2" style="color:#9B59B6"></i> 请求参数</div>
|
||||
<div class="code-body">
|
||||
<div class="code-action">
|
||||
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.operParam)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(form.operParam) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回参数 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-download" style="color:#1890FF"></i> 返回参数</div>
|
||||
<div class="code-body">
|
||||
<div class="code-action">
|
||||
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.jsonResult)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(form.jsonResult) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 异常信息 -->
|
||||
<div class="detail-card" v-if="form.status !== 0 && form.errorMsg">
|
||||
<div class="detail-card-title error-title"><i class="el-icon-warning"></i> 异常信息</div>
|
||||
<div class="error-body">
|
||||
<div class="error-msg">{{ form.errorMsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">
|
||||
<i class="el-icon-document" style="font-size:48px;color:#c0c4cc"></i>
|
||||
<p>暂无详细数据</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<el-button size="mini" @click="visible = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LogDetailDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
row: { type: Object, default: () => ({}) }
|
||||
},
|
||||
computed: {
|
||||
form() { return this.row || {} }
|
||||
},
|
||||
methods: {
|
||||
formatDate(t) {
|
||||
if (!t) return '-'
|
||||
return this.parseTime(t)
|
||||
},
|
||||
typeLabel(t) {
|
||||
const map = { 0: '其它', 1: '新增', 2: '修改', 3: '删除', 4: '授权', 5: '导出', 6: '导入', 7: '强退', 8: '生成代码', 9: '清空' }
|
||||
return map[t] || '类型' + t
|
||||
},
|
||||
typeColor(t) {
|
||||
const map = { 0: '#909399', 1: '#67C23A', 2: '#E6A23C', 3: '#F56C6C', 5: '#409EFF', 6: '#9B59B6' }
|
||||
return map[t] || '#909399'
|
||||
},
|
||||
methodType(m) {
|
||||
const map = { GET: 'success', POST: 'primary', PUT: 'warning', DELETE: 'danger' }
|
||||
return map[m] || 'info'
|
||||
},
|
||||
formatJson(str) {
|
||||
if (!str) return '(无数据)'
|
||||
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }
|
||||
},
|
||||
copyText(str) {
|
||||
const text = this.formatJson(str)
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(() => this.$message({ message: '已复制', type: 'success', duration: 1500 }))
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
this.$message({ message: '已复制', type: 'success', duration: 1500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-wrap { padding: 4px; }
|
||||
.detail-card {
|
||||
border: 1px solid #ebeef5; border-radius: 4px;
|
||||
margin-bottom: 12px; padding: 12px 14px; background: #fafafa;
|
||||
}
|
||||
.detail-card-title {
|
||||
font-size: 13px; font-weight: 600; color: #303133;
|
||||
margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.detail-row { margin-bottom: 0; }
|
||||
.detail-item {
|
||||
display: flex; align-items: center; padding: 4px 0;
|
||||
font-size: 12px; line-height: 1.6;
|
||||
}
|
||||
.detail-label { color: #909399; min-width: 70px; flex-shrink: 0; }
|
||||
.detail-value { color: #303133; flex: 1; word-break: break-all; }
|
||||
.detail-value.mono { font-family: 'Courier New', monospace; font-size: 11px; }
|
||||
|
||||
.code-body { background: #2d2d2d; border-radius: 4px; overflow: hidden; }
|
||||
.code-action { background: #3a3a3a; padding: 6px 10px; display: flex; justify-content: flex-end; }
|
||||
.code-pre {
|
||||
margin: 0; padding: 12px; color: #e0e0e0; font-size: 11px;
|
||||
font-family: 'Consolas', monospace; line-height: 1.6; white-space: pre-wrap;
|
||||
max-height: 300px; overflow: auto;
|
||||
}
|
||||
|
||||
.error-title { color: #f56c6c; }
|
||||
.error-body { background: #fef0f0; padding: 12px; border-radius: 4px; border: 1px solid #fde2e2; }
|
||||
.error-msg { color: #f56c6c; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; }
|
||||
|
||||
.empty-tip { padding: 40px 0; text-align: center; color: #909399; }
|
||||
</style>
|
||||
@@ -1,143 +1,472 @@
|
||||
<template>
|
||||
<div class="log-page">
|
||||
<!-- ═══ 标题栏 ═══ -->
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="page-header">
|
||||
<span class="page-title">操作记录</span>
|
||||
<span class="page-subtitle">OPERATION AUDIT LOG</span>
|
||||
<div class="header-left">
|
||||
<span class="page-title">
|
||||
<i class="el-icon-document-checked" style="color:#409EFF"></i>
|
||||
操作记录
|
||||
</span>
|
||||
<span class="page-subtitle">OPERATION AUDIT LOG</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="total-info">共 <b>{{ total }}</b> 条记录</span>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="refreshAll">刷新数据</el-button>
|
||||
<el-button size="mini" icon="el-icon-download" @click="handleExport" v-hasPermi="['bid:operationlog:export']">导出</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="handleClean" v-hasPermi="['bid:operationlog:remove']">清空</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ 筛选栏 ═══ -->
|
||||
<!-- KPI 统计卡片 -->
|
||||
<el-row :gutter="12" class="kpi-row" v-if="stats.basic">
|
||||
<el-col :xs="12" :sm="6" :md="4" v-for="card in kpiCards" :key="card.label">
|
||||
<div class="kpi-card" :style="{ borderTop: '3px solid ' + card.color }">
|
||||
<div class="kpi-label">{{ card.label }}</div>
|
||||
<div class="kpi-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="kpi-icon"><i :class="card.icon" :style="{ color: card.color }"></i></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="12" class="chart-row">
|
||||
<el-col :xs="24" :sm="12" :md="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">
|
||||
<i class="el-icon-pie-chart" style="color:#67C23A"></i>
|
||||
<span>模块分布</span>
|
||||
</div>
|
||||
<div ref="moduleChart" class="chart-container" style="height:280px"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">
|
||||
<i class="el-icon-data-analysis" style="color:#E6A23C"></i>
|
||||
<span>操作类型分布</span>
|
||||
</div>
|
||||
<div ref="typeChart" class="chart-container" style="height:280px"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">
|
||||
<i class="el-icon-warning" style="color:#F56C6C"></i>
|
||||
<span>异常模块 Top5</span>
|
||||
</div>
|
||||
<div ref="errorChart" class="chart-container" style="height:280px"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 趋势图表 -->
|
||||
<div class="chart-card trend-card">
|
||||
<div class="chart-title">
|
||||
<i class="el-icon-data-line" style="color:#409EFF"></i>
|
||||
<span>近 14 日操作趋势</span>
|
||||
</div>
|
||||
<div ref="trendChart" class="chart-container" style="height:280px"></div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input v-model="q.searchKey" placeholder="搜索目标 / 详情" clearable size="small" style="width:200px"
|
||||
prefix-icon="el-icon-search" @keyup.enter.native="handleSearch" />
|
||||
<el-select v-model="q.title" placeholder="全部模块" clearable size="small" style="width:130px" @change="handleSearch">
|
||||
<el-option label="全部模块" value="" />
|
||||
<el-option label="甲方客户" value="甲方客户" />
|
||||
<el-option label="物料管理" value="物料管理" />
|
||||
<el-option label="供应商管理" value="供应商管理" />
|
||||
<el-option label="报价请求" value="报价请求" />
|
||||
<el-option label="供应商报价" value="报价单" />
|
||||
<el-option label="采购单" value="采购单" />
|
||||
<el-option label="发货管理" value="发货管理" />
|
||||
<el-option label="结单时间" value="结单时间" />
|
||||
<el-option label="订单异议" value="订单异议" />
|
||||
<el-option label="供应商评价" value="供应商评价" />
|
||||
<el-option label="租户管理" value="租户管理" />
|
||||
<el-input
|
||||
v-model="queryParams.operParam"
|
||||
placeholder="搜索关键词(模块/操作人/请求URL/参数)"
|
||||
clearable
|
||||
size="small"
|
||||
prefix-icon="el-icon-search"
|
||||
style="width:260px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-select
|
||||
v-model="queryParams.title"
|
||||
placeholder="模块筛选"
|
||||
clearable
|
||||
size="small"
|
||||
filterable
|
||||
style="width:140px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="m in moduleOptions"
|
||||
:key="m.module_name"
|
||||
:label="m.module_name"
|
||||
:value="m.module_name"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="q.businessType" placeholder="全部操作" clearable size="small" style="width:120px" @change="handleSearch">
|
||||
<el-option label="全部操作" value="" />
|
||||
<el-option label="新增" value="1" />
|
||||
<el-option label="修改" value="2" />
|
||||
<el-option label="删除" value="3" />
|
||||
<el-option label="导出" value="5" />
|
||||
<el-select
|
||||
v-model="queryParams.businessType"
|
||||
placeholder="操作类型"
|
||||
clearable
|
||||
size="small"
|
||||
style="width:120px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option label="新增" :value="1" />
|
||||
<el-option label="修改" :value="2" />
|
||||
<el-option label="删除" :value="3" />
|
||||
<el-option label="授权" :value="4" />
|
||||
<el-option label="导出" :value="5" />
|
||||
<el-option label="导入" :value="6" />
|
||||
<el-option label="强退" :value="7" />
|
||||
<el-option label="生成代码" :value="8" />
|
||||
<el-option label="清空" :value="9" />
|
||||
<el-option label="其它" :value="0" />
|
||||
</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="handleSearch">搜索</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh" @click="resetSearch">重置</el-button>
|
||||
<div class="search-right">
|
||||
<el-button size="small" icon="el-icon-refresh" @click="getList">刷新</el-button>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="操作状态"
|
||||
clearable
|
||||
size="small"
|
||||
style="width:110px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option label="正常" :value="0" />
|
||||
<el-option label="异常" :value="1" />
|
||||
</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:240px"
|
||||
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>
|
||||
|
||||
<!-- ═══ 表格 ═══ -->
|
||||
<el-table v-loading="loading" :data="list" border stripe size="small" class="log-table" style="width:100%">
|
||||
<el-table-column label="时间" prop="operTime" width="155" />
|
||||
<el-table-column label="操作人" prop="operName" width="90" />
|
||||
<el-table-column label="模块" width="100">
|
||||
<template slot-scope="s">
|
||||
<el-tag :type="moduleType(s.row.title)" size="small" effect="plain">{{ s.row.title }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作类型" width="85" align="center">
|
||||
<template slot-scope="s">
|
||||
<span :style="{ color: actionColor(s.row.businessType), fontWeight:600 }">{{ actionLabel(s.row.businessType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请求地址" prop="operUrl" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="参数" min-width="180" show-overflow-tooltip>
|
||||
<template slot-scope="s">{{ formatParam(s.row.operParam) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="65" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-tag :type="s.row.status === 0 ? 'success' : 'danger'" size="mini" effect="dark">
|
||||
{{ s.row.status === 0 ? '正常' : '异常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时" prop="costTime" width="70" align="center">
|
||||
<template slot-scope="s">{{ s.row.costTime || '-' }}ms</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 日志列表 -->
|
||||
<div v-loading="loading" class="log-table-wrap">
|
||||
<el-table
|
||||
ref="logTable"
|
||||
:data="list"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
height="500"
|
||||
style="width:100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="45" align="center" />
|
||||
<el-table-column label="编号" prop="operId" width="70" align="center" />
|
||||
<el-table-column label="时间" prop="operTime" width="155" align="center">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.operTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模块" min-width="110">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" effect="plain">{{ scope.row.title }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: typeColor(scope.row.businessType), fontWeight: 600 }">{{ typeLabel(scope.row.businessType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" prop="operName" width="100" align="center" />
|
||||
<el-table-column label="请求路径" prop="operUrl" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="75" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'" size="mini">
|
||||
{{ scope.row.status === 0 ? '正常' : '异常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时" prop="costTime" width="75" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.costTime }}ms</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" class-name="col-ops" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
style="color:#F56C6C"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['bid:operationlog:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total>0" :total="total" :page.sync="q.pageNum" :limit.sync="q.pageSize"
|
||||
@pagination="getList" />
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<log-detail-dialog :visible.sync="detailVisible" :row="detailRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listOperationLog } from "@/api/bid/operation-log"
|
||||
import * as echarts from 'echarts'
|
||||
require('echarts/theme/macarons')
|
||||
import {
|
||||
listOperationLog,
|
||||
deleteOperationLog,
|
||||
cleanOperationLog,
|
||||
getOperationLogStats
|
||||
} from '@/api/bid/operation-log'
|
||||
import LogDetailDialog from './detail'
|
||||
|
||||
export default {
|
||||
name: "OperationLog",
|
||||
name: 'OperationLog',
|
||||
components: { LogDetailDialog },
|
||||
data() {
|
||||
return {
|
||||
loading: false, list: [], total: 0,
|
||||
dateRange: null,
|
||||
q: {
|
||||
pageNum: 1, pageSize: 20,
|
||||
title: "", businessType: "", searchKey: "",
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
dateRange: [],
|
||||
moduleOptions: [],
|
||||
stats: { basic: null, modules: [], businessTypes: [], trend: [], errors: [] },
|
||||
charts: { module: null, type: null, trend: null, error: null },
|
||||
detailVisible: false,
|
||||
detailRow: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
operParam: undefined,
|
||||
title: undefined,
|
||||
businessType: undefined,
|
||||
status: undefined,
|
||||
operName: undefined,
|
||||
params: { beginTime: null, endTime: null }
|
||||
}
|
||||
}
|
||||
},
|
||||
created() { this.getList() },
|
||||
computed: {
|
||||
kpiCards() {
|
||||
const b = this.stats.basic || {}
|
||||
return [
|
||||
{ label: '总操作数', value: b.total_count || 0, color: '#409EFF', icon: 'el-icon-document' },
|
||||
{ label: '今日操作', value: b.today_count || 0, color: '#67C23A', icon: 'el-icon-time' },
|
||||
{ label: '异常次数', value: b.error_count || 0, color: '#F56C6C', icon: 'el-icon-warning' },
|
||||
{ label: '操作人数', value: b.user_count || 0, color: '#E6A23C', icon: 'el-icon-user-solid' },
|
||||
{ label: '平均耗时', value: (b.avg_cost_time || 0) + 'ms', color: '#909399', icon: 'el-icon-alarm-clock' },
|
||||
{ label: '模块总数', value: b.module_count || 0, color: '#9B59B6', icon: 'el-icon-menu' }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadStats()
|
||||
this.getList()
|
||||
this.initCharts()
|
||||
const that = this
|
||||
this.$nextTick(() => {
|
||||
window.addEventListener('resize', that.resizeAll)
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resizeAll)
|
||||
this.disposeAll()
|
||||
},
|
||||
methods: {
|
||||
loadStats() {
|
||||
const p = this.buildQueryParams()
|
||||
delete p.pageNum
|
||||
delete p.pageSize
|
||||
getOperationLogStats(p).then(res => {
|
||||
if (res.data) {
|
||||
this.stats = res.data
|
||||
this.moduleOptions = (res.data.modules || []).slice(0, 30)
|
||||
this.$nextTick(() => this.renderCharts())
|
||||
}
|
||||
})
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
const p = {
|
||||
pageNum: this.q.pageNum, pageSize: this.q.pageSize,
|
||||
title: this.q.title || undefined,
|
||||
businessType: this.q.businessType || undefined,
|
||||
params: {}
|
||||
}
|
||||
const p = this.buildQueryParams()
|
||||
listOperationLog(p).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
buildQueryParams() {
|
||||
const p = JSON.parse(JSON.stringify(this.queryParams))
|
||||
p.params = {}
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
p.params.beginTime = this.dateRange[0] + ' 00:00:00'
|
||||
p.params.endTime = this.dateRange[1] + ' 23:59:59'
|
||||
}
|
||||
if (this.q.searchKey) p.operParam = this.q.searchKey
|
||||
listOperationLog(p).then(r => {
|
||||
this.list = r.rows || []; this.total = r.total || 0; this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
return p
|
||||
},
|
||||
handleSearch() { this.q.pageNum = 1; this.getList() },
|
||||
resetSearch() {
|
||||
this.q.title = ""; this.q.businessType = ""; this.q.searchKey = ""
|
||||
this.dateRange = null; this.q.params = {}
|
||||
this.handleSearch()
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.loadStats()
|
||||
this.getList()
|
||||
},
|
||||
|
||||
moduleType(title) {
|
||||
const map = { "物料管理": "", "供应商管理": "", "报价单": "warning", "报价请求": "warning",
|
||||
"采购单": "", "发货管理": "success", "甲方客户": "primary", "结单时间": "warning",
|
||||
"订单异议": "danger", "供应商评价": "success", "租户管理": "info" }
|
||||
return map[title] || ""
|
||||
resetQuery() {
|
||||
this.dateRange = []
|
||||
this.queryParams = {
|
||||
pageNum: 1, pageSize: 20,
|
||||
operParam: undefined, title: undefined,
|
||||
businessType: undefined, status: undefined,
|
||||
params: { beginTime: null, endTime: null }
|
||||
}
|
||||
this.handleQuery()
|
||||
},
|
||||
actionLabel(b) {
|
||||
const map = { 1: "新增", 2: "修改", 3: "删除", 4: "授权", 5: "导出", 6: "导入" }
|
||||
return map[b] || ("类型" + b)
|
||||
refreshAll() {
|
||||
this.loadStats()
|
||||
this.getList()
|
||||
},
|
||||
actionColor(b) {
|
||||
const map = { 1: "#67c23a", 2: "#e6a23c", 3: "#f56c6c", 5: "#e4393c" }
|
||||
return map[b] || "#909399"
|
||||
handleSelectionChange(sel) {
|
||||
this.ids = sel.map(x => x.operId)
|
||||
},
|
||||
formatParam(param) {
|
||||
if (!param) return "-"
|
||||
if (param.length > 80) return param.substring(0, 80) + "..."
|
||||
return param
|
||||
handleDetail(row) {
|
||||
this.detailRow = row
|
||||
this.detailVisible = true
|
||||
},
|
||||
handleDelete(row) {
|
||||
const ids = row.operId ? [row.operId] : this.ids
|
||||
if (!ids || ids.length === 0) {
|
||||
this.$message.warning('请选择要删除的日志')
|
||||
return
|
||||
}
|
||||
this.$modal.confirm('确定删除选中的日志记录吗?').then(() => {
|
||||
return deleteOperationLog(ids.join(','))
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.loadStats()
|
||||
this.$message.success('删除成功')
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleClean() {
|
||||
this.$modal.confirm('确定清空所有操作日志吗?此操作不可恢复。').then(() => {
|
||||
return cleanOperationLog()
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.loadStats()
|
||||
this.$message.success('清空成功')
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleExport() {
|
||||
const p = this.buildQueryParams()
|
||||
delete p.pageNum
|
||||
delete p.pageSize
|
||||
this.download('/bid/operationLog/export', p, '操作日志_' + Date.now())
|
||||
},
|
||||
formatDate(t) {
|
||||
if (!t) return '-'
|
||||
return this.parseTime(t)
|
||||
},
|
||||
typeLabel(t) {
|
||||
const map = { 0: '其它', 1: '新增', 2: '修改', 3: '删除', 4: '授权', 5: '导出', 6: '导入', 7: '强退', 8: '生成代码', 9: '清空' }
|
||||
return map[t] || '类型' + t
|
||||
},
|
||||
typeColor(t) {
|
||||
const map = { 0: '#909399', 1: '#67C23A', 2: '#E6A23C', 3: '#F56C6C', 5: '#409EFF', 6: '#9B59B6' }
|
||||
return map[t] || '#909399'
|
||||
},
|
||||
initCharts() {
|
||||
if (this.$refs.moduleChart) this.charts.module = echarts.init(this.$refs.moduleChart, 'macarons')
|
||||
if (this.$refs.typeChart) this.charts.type = echarts.init(this.$refs.typeChart, 'macarons')
|
||||
if (this.$refs.trendChart) this.charts.trend = echarts.init(this.$refs.trendChart, 'macarons')
|
||||
if (this.$refs.errorChart) this.charts.error = echarts.init(this.$refs.errorChart, 'macarons')
|
||||
},
|
||||
renderCharts() {
|
||||
this.renderModuleChart()
|
||||
this.renderTypeChart()
|
||||
this.renderTrendChart()
|
||||
this.renderErrorChart()
|
||||
},
|
||||
renderModuleChart() {
|
||||
const data = (this.stats.modules || []).slice(0, 10)
|
||||
if (!this.charts.module || data.length === 0) return
|
||||
const option = {
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { type: 'scroll', bottom: 0, textStyle: { fontSize: 10 } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['35%', '65%'],
|
||||
center: ['50%', '40%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: { show: true, formatter: '{b}\n{c}', fontSize: 10 },
|
||||
data: data.map(d => ({ name: d.module_name, value: d.count }))
|
||||
}]
|
||||
}
|
||||
this.charts.module.setOption(option)
|
||||
},
|
||||
renderTypeChart() {
|
||||
const data = this.stats.businessTypes || []
|
||||
if (!this.charts.type || data.length === 0) return
|
||||
const option = {
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { type: 'scroll', bottom: 0, textStyle: { fontSize: 10 } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
center: ['50%', '40%'],
|
||||
label: { show: true, formatter: '{b}\n{c}', fontSize: 10 },
|
||||
data: data.map(d => ({ name: this.typeLabel(d.type), value: d.count }))
|
||||
}]
|
||||
}
|
||||
this.charts.type.setOption(option)
|
||||
},
|
||||
renderTrendChart() {
|
||||
const data = (this.stats.trend || []).slice().reverse()
|
||||
if (!this.charts.trend || data.length === 0) return
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '10%', top: '10%', containLabel: true },
|
||||
xAxis: { type: 'category', data: data.map(d => d.day), axisLabel: { fontSize: 10, rotate: 30 } },
|
||||
yAxis: { type: 'value', name: '次数' },
|
||||
series: [{
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: { opacity: 0.3 },
|
||||
lineStyle: { width: 2 },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: { color: '#409EFF' },
|
||||
data: data.map(d => d.count)
|
||||
}]
|
||||
}
|
||||
this.charts.trend.setOption(option)
|
||||
},
|
||||
renderErrorChart() {
|
||||
const data = (this.stats.errors || []).slice(0, 5)
|
||||
if (!this.charts.error || data.length === 0) return
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '8%', bottom: '10%', top: '10%', containLabel: true },
|
||||
xAxis: { type: 'value', name: '次数' },
|
||||
yAxis: { type: 'category', data: data.map(d => d.module_name), axisLabel: { fontSize: 10 } },
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: data.map(d => d.count),
|
||||
itemStyle: { color: '#F56C6C' },
|
||||
label: { show: true, position: 'right', fontSize: 10 }
|
||||
}]
|
||||
}
|
||||
this.charts.error.setOption(option)
|
||||
},
|
||||
resizeAll() {
|
||||
Object.keys(this.charts).forEach(k => {
|
||||
if (this.charts[k] && this.charts[k].resize) this.charts[k].resize()
|
||||
})
|
||||
},
|
||||
disposeAll() {
|
||||
Object.keys(this.charts).forEach(k => {
|
||||
if (this.charts[k] && this.charts[k].dispose) this.charts[k].dispose()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,20 +476,42 @@ export default {
|
||||
.log-page { background: #f5f7fa; padding: 12px; min-height: calc(100vh - 84px); }
|
||||
|
||||
.page-header {
|
||||
background: #fff; padding: 12px 16px; border-radius: 4px; margin-bottom: 12px;
|
||||
background: #fff; padding: 14px 18px; border-radius: 4px; margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06); display: flex; align-items: center; gap: 12px;
|
||||
}
|
||||
.page-title { font-size: 16px; font-weight: 700; color: #333333; }
|
||||
.header-left { display: flex; align-items: center; gap: 10px; flex: 1; }
|
||||
.page-title { font-size: 16px; font-weight: 700; color: #333; }
|
||||
.page-subtitle { font-size: 11px; color: #c0c4cc; letter-spacing: 1px; }
|
||||
.header-right { margin-left: auto; }
|
||||
.total-info { font-size: 12px; color: #909399; }
|
||||
.header-right { display: flex; gap: 8px; }
|
||||
|
||||
.kpi-row { margin-bottom: 12px; }
|
||||
.kpi-card {
|
||||
background: #fff; border-radius: 4px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
position: relative; overflow: hidden; margin-bottom: 12px;
|
||||
}
|
||||
.kpi-label { font-size: 12px; color: #909399; margin-bottom: 8px; }
|
||||
.kpi-value { font-size: 26px; font-weight: 700; }
|
||||
.kpi-icon { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); font-size: 36px; opacity: 0.15; }
|
||||
|
||||
.chart-row { margin-bottom: 12px; }
|
||||
.chart-card {
|
||||
background: #fff; border-radius: 4px; padding: 14px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 14px; font-weight: 600; color: #303133; margin-bottom: 10px;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.chart-container { width: 100%; }
|
||||
|
||||
.trend-card { margin-bottom: 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;
|
||||
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||
}
|
||||
.search-right { margin-left: auto; }
|
||||
|
||||
.log-table { box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.log-table-wrap { background: #fff; border-radius: 4px; padding: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
</style>
|
||||
|
||||
@@ -131,7 +131,7 @@ export default {
|
||||
{ key: 'transit', label: '在途', count: 0 },
|
||||
{ key: 'history', label: '历史', count: 0 },
|
||||
],
|
||||
q: { pageNum: 1, pageSize: 20, deliveryStatus: "history", doNo: "", supplierName: "" },
|
||||
q: { pageNum: 1, pageSize: 20, deliveryStatus: "history,closed", doNo: "", supplierName: "" },
|
||||
detailOpen: false, detailData: null,
|
||||
statCards: [
|
||||
{ key: "totalHistory", label: "历史订单总数", icon: "el-icon-document-copy", color: "#e4393c" },
|
||||
|
||||
@@ -328,33 +328,37 @@ export default {
|
||||
}
|
||||
|
||||
this.chart.setOption(option)
|
||||
// 点击跳转到对应详情(self 已在上方定义)
|
||||
// 点击甘特条或节点 → 跳转到对应订单详情
|
||||
this.chart.on('click', function(params) {
|
||||
// custom series: params.value = [startDate, endDate, idx]
|
||||
// scatter series: params.data.order 或 params.value = [date, idx]
|
||||
let o = null
|
||||
// scatter节点:order 保存在 data 中
|
||||
if (params.data && params.data.order) {
|
||||
o = params.data.order
|
||||
} else if (params.value && params.value.length >= 3) {
|
||||
const idx = Math.round(params.value[2])
|
||||
o = self.orders[idx]
|
||||
} else if (params.value && params.value.length >= 2) {
|
||||
// scatter的坐标: [date, idx]
|
||||
const idx = Math.round(params.value[1])
|
||||
o = self.orders[idx]
|
||||
}
|
||||
// custom甘特条:通过 dataIndex 映射到 orders
|
||||
if (!o && params.dataIndex != null) {
|
||||
if (params.seriesIndex === 0) {
|
||||
o = self.orders[params.dataIndex]
|
||||
} else if (params.seriesIndex === 1 && params.data && params.data.order) {
|
||||
o = params.data.order
|
||||
}
|
||||
}
|
||||
if (!o) return
|
||||
// 根据类型和状态跳转
|
||||
// 根据类型和状态跳转到对应的订单列表页
|
||||
if (o.type === 'client') {
|
||||
const path = o.deliveryStatus === 'pending' ? '/bid/clientDelivery/pending'
|
||||
: o.deliveryStatus === 'transit' ? '/bid/clientDelivery/transit'
|
||||
: '/bid/clientDelivery/signed'
|
||||
self.$router.push({ path, query: { doNo: o.doNo } })
|
||||
self.$router.push({
|
||||
path: o.deliveryStatus === 'pending' ? '/bid/clientDelivery/pending'
|
||||
: o.deliveryStatus === 'transit' ? '/bid/clientDelivery/transit'
|
||||
: '/bid/clientDelivery/signed',
|
||||
query: { doNo: o.doNo }
|
||||
})
|
||||
} else {
|
||||
const path = o.deliveryStatus === 'pending' ? '/bid/order/pending'
|
||||
: o.deliveryStatus === 'transit' ? '/bid/order/transit'
|
||||
: '/bid/order/history'
|
||||
self.$router.push({ path, query: { doNo: o.doNo } })
|
||||
self.$router.push({
|
||||
path: o.deliveryStatus === 'pending' ? '/bid/order/pending'
|
||||
: o.deliveryStatus === 'transit' ? '/bid/order/transit'
|
||||
: '/bid/order/history',
|
||||
query: { doNo: o.doNo }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,11 +72,20 @@
|
||||
<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" v-if="!isSupplier">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" @click="handleCleanNullQuote">清理无编号数据</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- ── 报价列表 ── -->
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column label="报价单号" prop="quoteNo" width="155" />
|
||||
<el-table-column label="报价单号" width="155">
|
||||
<template slot-scope="s">
|
||||
<span :style="{ color: s.row.quoteNo ? '#333' : '#c0c4cc' }">
|
||||
{{ s.row.quoteNo || '(待编号)' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联询价单" width="200">
|
||||
<template slot-scope="s">
|
||||
<div style="font-weight:600;color:#333">{{ s.row.rfqNo }}</div>
|
||||
@@ -113,27 +122,42 @@
|
||||
<span v-else style="color:#c0c4cc;font-size:12px">未提交</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="220" fixed="right">
|
||||
<el-table-column label="操作" align="center" width="260" fixed="right" class-name="col-ops">
|
||||
<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)"
|
||||
v-if="s.row.status==='draft'">编辑</el-button>
|
||||
<el-button size="mini" type="text" style="color:#67C23A" icon="el-icon-upload2"
|
||||
@click="handleSubmit(s.row)" v-if="s.row.status==='draft'">提交</el-button>
|
||||
<el-button size="mini" type="text" style="color:#E6A23C" icon="el-icon-s-check"
|
||||
@click="handleSubmitApproval(s.row)" v-if="s.row.status==='draft' || s.row.status==='submitted'">提交审批</el-button>
|
||||
<el-button size="mini" type="text" style="color:#67C23A"
|
||||
@click="handleApprove(s.row)" v-if="s.row.status==='10'">审批通过</el-button>
|
||||
<el-button size="mini" type="text" style="color:#F56C6C"
|
||||
@click="handleApprovalReject(s.row)" v-if="s.row.status==='10'">审批驳回</el-button>
|
||||
<el-button size="mini" type="text" style="color:#67C23A" icon="el-icon-check"
|
||||
@click="handleAccept(s.row)" v-if="s.row.status==='submitted' && !isSupplier">采纳</el-button>
|
||||
<el-button size="mini" type="text" style="color:#F56C6C" icon="el-icon-close"
|
||||
@click="handleReject(s.row)" v-if="s.row.status==='submitted' && !isSupplier">拒绝</el-button>
|
||||
<el-button size="mini" type="text" style="color:#4A6FA5" icon="el-icon-s-order"
|
||||
@click="handleCreateDelivery(s.row)" v-if="s.row.status==='accepted'">生成发货单</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c"
|
||||
@click="handleDelete(s.row)" v-if="s.row.status==='draft'">删除</el-button>
|
||||
|
||||
<!-- ═══ 草稿:提交审批是唯一送审入口 ═══ -->
|
||||
<template v-if="s.row.status==='draft'">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(s.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" style="color:#E6A23C" icon="el-icon-s-check"
|
||||
@click="handleSubmitApproval(s.row)">提交审批</el-button>
|
||||
<el-dropdown trigger="click" @command="c => handleDropdown(c, s.row)">
|
||||
<el-button size="mini" type="text">更多<i class="el-icon-arrow-down el-icon--right" /></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<!-- ═══ 已提交(历史遗留):允许补交审批 ═══ -->
|
||||
<template v-if="s.row.status==='submitted'">
|
||||
<el-dropdown trigger="click" @command="c => handleDropdown(c, s.row)">
|
||||
<el-button size="mini" type="text">更多<i class="el-icon-arrow-down el-icon--right" /></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="submitApproval" icon="el-icon-s-check">提交审批</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<!-- ═══ 审批中:审批人操作 ═══ -->
|
||||
<template v-if="s.row.status==='10'">
|
||||
<el-button size="mini" type="text" style="color:#67C23A" @click="handleApprove(s.row)">审批通过</el-button>
|
||||
<el-button size="mini" type="text" style="color:#F56C6C" @click="handleApprovalReject(s.row)">审批驳回</el-button>
|
||||
</template>
|
||||
|
||||
<!-- ═══ 已采纳:生成发货单 ═══ -->
|
||||
<el-button v-if="s.row.status==='accepted'" size="mini" type="text" style="color:#4A6FA5"
|
||||
icon="el-icon-s-order" @click="handleCreateDelivery(s.row)">生成发货单</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -273,7 +297,7 @@
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogOpen = false">取消</el-button>
|
||||
<el-button type="success" @click="submitForm('draft')" :loading="saving">保存草稿</el-button>
|
||||
<el-button type="primary" @click="submitForm('submit')" :loading="submitting">保存并提交</el-button>
|
||||
<el-button type="primary" @click="submitForm('approval')" :loading="submitting">保存并提交审批</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
@@ -304,12 +328,12 @@
|
||||
<div class="pdf-company">福安德综合报价系统</div>
|
||||
<div class="pdf-doc-type">供应商报价单</div>
|
||||
</div>
|
||||
<div class="pdf-header-no">{{ detailData.quoteNo }}</div>
|
||||
<div class="pdf-header-no">{{ detailData.quoteNo || '(待编号)' }}</div>
|
||||
</div>
|
||||
<div class="pdf-divider"></div>
|
||||
<table class="pdf-meta-table">
|
||||
<tr>
|
||||
<td class="meta-label">报价单号</td><td class="meta-val">{{ detailData.quoteNo }}</td>
|
||||
<td class="meta-label">报价单号</td><td class="meta-val">{{ detailData.quoteNo || '(待编号)' }}</td>
|
||||
<td class="meta-label">供应商</td><td class="meta-val"><strong>{{ detailData.supplierName }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -374,7 +398,7 @@
|
||||
|
||||
<script>
|
||||
import { listQuotation, getQuotation, addQuotation, updateQuotation,
|
||||
submitQuotation, acceptQuotation, rejectQuotation, delQuotation } from "@/api/bid/quotation";
|
||||
delQuotation, cleanNullQuoteNo } from "@/api/bid/quotation";
|
||||
import { listRfq, getRfqItems } from "@/api/bid/rfq";
|
||||
import { listSupplier } from "@/api/bid/supplier";
|
||||
import { addDelivery } from "@/api/bid/delivery";
|
||||
@@ -426,6 +450,10 @@ export default {
|
||||
listSupplier({ pageSize: 200 }).then(r => { this.supplierOptions = r.rows || []; });
|
||||
}
|
||||
},
|
||||
/** keep-alive 缓存激活时自动刷新(解决跨页面审批后状态不更新问题) */
|
||||
activated() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true;
|
||||
@@ -451,6 +479,13 @@ export default {
|
||||
this.dialogTitle = "新建报价单";
|
||||
this.dialogOpen = true;
|
||||
},
|
||||
/** 清理历史遗留的无编号报价单脏数据 */
|
||||
handleCleanNullQuote() {
|
||||
this.$modal.confirm("确认清理所有无报价单号的脏数据?此操作不可恢复!", "危险操作", { type: "warning" })
|
||||
.then(() => cleanNullQuoteNo())
|
||||
.then(r => { this.$modal.msgSuccess(r.msg || "清理成功"); this.getList(); })
|
||||
.catch(() => {});
|
||||
},
|
||||
handleUpdate(row) {
|
||||
getQuotation(row.quotationId).then(r => {
|
||||
this.form = { ...r.data, items: r.data.items || [] };
|
||||
@@ -498,10 +533,6 @@ export default {
|
||||
itemTotal(row) {
|
||||
return ((parseFloat(row.quantity) || 0) * (parseFloat(row.unitPrice) || 0)).toFixed(2);
|
||||
},
|
||||
handleSubmit(row) {
|
||||
this.$modal.confirm("确认提交报价?提交后不可修改").then(() => submitQuotation(row.quotationId))
|
||||
.then(() => { this.$modal.msgSuccess("提交成功"); this.getList(); });
|
||||
},
|
||||
handleSubmitApproval(row) {
|
||||
this.$modal.confirm("确认提交审批?").then(() => submitApproval("QUOTATION", row.quotationId))
|
||||
.then(() => { this.$modal.msgSuccess("已提交审批"); this.getList(); });
|
||||
@@ -516,18 +547,18 @@ export default {
|
||||
.then(() => { this.$modal.msgSuccess("已驳回"); this.getList(); })
|
||||
.catch(() => {});
|
||||
},
|
||||
handleAccept(row) {
|
||||
this.$modal.confirm("确认采纳此报价?").then(() => acceptQuotation(row.quotationId))
|
||||
.then(() => { this.$modal.msgSuccess("已采纳"); this.getList(); });
|
||||
},
|
||||
handleReject(row) {
|
||||
this.$modal.confirm("确认拒绝此报价?").then(() => rejectQuotation(row.quotationId))
|
||||
.then(() => { this.$modal.msgSuccess("已拒绝"); this.getList(); });
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm("确认删除?").then(() => delQuotation(row.quotationId))
|
||||
.then(() => { this.$modal.msgSuccess("删除成功"); this.getList(); });
|
||||
},
|
||||
/** 「更多」下拉菜单统一路由 */
|
||||
handleDropdown(command, row) {
|
||||
const map = {
|
||||
submitApproval:() => this.handleSubmitApproval(row),
|
||||
delete: () => this.handleDelete(row),
|
||||
};
|
||||
if (map[command]) map[command]();
|
||||
},
|
||||
handleCreateDelivery(row) {
|
||||
this.$modal.confirm("确认基于此报价单生成发货单?").then(() => {
|
||||
return getQuotation(row.quotationId);
|
||||
@@ -572,14 +603,14 @@ export default {
|
||||
submitForm(mode) {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return;
|
||||
if (mode === "submit") this.submitting = true;
|
||||
if (mode === "approval") this.submitting = true;
|
||||
else this.saving = true;
|
||||
const action = this.form.quotationId ? updateQuotation : addQuotation;
|
||||
action(this.form).then(res => {
|
||||
const id = (res.data && res.data.quotationId) || this.form.quotationId;
|
||||
if (mode === "submit" && id) {
|
||||
return submitQuotation(id).then(() => {
|
||||
this.$modal.msgSuccess("提交成功");
|
||||
if (mode === "approval" && id) {
|
||||
return submitApproval("QUOTATION", id).then(() => {
|
||||
this.$modal.msgSuccess("已提交审批");
|
||||
this.dialogOpen = false;
|
||||
this.getList();
|
||||
});
|
||||
@@ -700,4 +731,11 @@ export default {
|
||||
.amount-cell { color: #e4393c; font-weight: 600; }
|
||||
.total-cell { font-size: 15px; background: #fafafa !important; font-weight: 700; }
|
||||
.pdf-footer { text-align: right; font-size: 11px; color: #aaa; margin-top: 10px; border-top: 1px solid #f0f2f5; padding-top: 8px; }
|
||||
|
||||
/* 操作列:禁止溢出省略,确保所有按钮完整显示 */
|
||||
::v-deep .col-ops .cell {
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参与供应商" width="100" align="center" prop="supplierCount" />
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" size="mini" icon="el-icon-data-analysis"
|
||||
@click="goCompare(s.row.rfqId)">比价</el-button>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" width="160" />
|
||||
<el-table-column label="操作" align="center" width="160">
|
||||
<el-table-column label="操作" class-name="col-ops" align="center">
|
||||
<template slot-scope="scope">
|
||||
<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" @click="handleDelete(scope.row)">删除</el-button>
|
||||
|
||||
Reference in New Issue
Block a user