按需求调整

This commit is contained in:
wanglongjie 2026-02-03 15:37:25 +08:00
parent 8aa3af986f
commit daacd7d742
17 changed files with 602 additions and 165 deletions

View File

@ -55,6 +55,11 @@ public class Constants {
*/
public static final int REPORT_STATUS_EXPIRED = 3;
/**
* 报备状态 - 已作废
*/
public static final int REPORT_STATUS_CANCELED = 4;
/**
* 用户角色 - 管理员
*/

View File

@ -5,6 +5,7 @@ import com.bycrm.common.Result;
import com.bycrm.dto.PageQuery;
import com.bycrm.dto.ReportAuditDTO;
import com.bycrm.dto.ReportDTO;
import com.bycrm.dto.ReportUpdateDTO;
import com.bycrm.service.ReportService;
import com.bycrm.vo.ReportVO;
import io.swagger.annotations.Api;
@ -72,6 +73,20 @@ public class ReportController {
return Result.success();
}
/**
* 更新报备
*/
@ApiOperation("更新报备")
@PutMapping("/{id}")
public Result<Void> updateReport(
@ApiParam("报备ID") @PathVariable Long id,
@RequestBody ReportUpdateDTO updateDTO,
HttpServletRequest request) {
Long currentUserId = (Long) request.getAttribute("currentUserId");
reportService.updateReport(id, updateDTO, currentUserId);
return Result.success();
}
/**
* 审核报备
*/

View File

@ -2,7 +2,7 @@ package com.bycrm.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
@ -14,10 +14,27 @@ public class ReportDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 客户ID
* 学校ID可选从数据源中选择时提供
*/
@NotNull(message = "客户ID不能为空")
private Long customerId;
private Long schoolId;
/**
* 学校名称必填
*/
@NotBlank(message = "学校名称不能为空")
private String schoolName;
/**
* 所属产品
*/
@NotBlank(message = "所属产品不能为空")
private String product;
/**
* 项目类型
*/
@NotBlank(message = "项目类型不能为空")
private String projectType;
/**
* 报备说明

View File

@ -0,0 +1,44 @@
package com.bycrm.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 报备更新 DTO
*/
@Data
public class ReportUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 所属产品
*/
@NotBlank(message = "所属产品不能为空")
private String product;
/**
* 项目类型
*/
@NotBlank(message = "项目类型不能为空")
private String projectType;
/**
* 报备说明
*/
private String description;
/**
* 状态
*/
@NotNull(message = "状态不能为空")
private Integer status;
/**
* 作废原因当状态为作废时必填
*/
private String cancelReason;
}

View File

@ -26,9 +26,24 @@ public class Report implements Serializable {
private Long dealerId;
/**
* 客户ID
* 学校ID crm_school 选择
*/
private Long customerId;
private Long schoolId;
/**
* 学校名称冗余存储方便查询
*/
private String schoolName;
/**
* 所属产品
*/
private String product;
/**
* 项目类型
*/
private String projectType;
/**
* 报备说明
@ -36,7 +51,7 @@ public class Report implements Serializable {
private String description;
/**
* 状态0-待审核 1-已通过 2-已驳回 3-已失效
* 状态0-待审核 1-已通过 2-已驳回 3-已失效 4-已作废
*/
private Integer status;
@ -45,6 +60,11 @@ public class Report implements Serializable {
*/
private String rejectReason;
/**
* 作废原因
*/
private String cancelReason;
/**
* 保护期开始日期
*/
@ -73,14 +93,4 @@ public class Report implements Serializable {
* 关联查询字段 - 经销商名称
*/
private String dealerName;
/**
* 关联查询字段 - 客户名称
*/
private String customerName;
/**
* 关联查询字段 - 客户电话
*/
private String customerPhone;
}

View File

@ -19,9 +19,18 @@ public interface ReportMapper {
Report selectById(@Param("id") Long id);
/**
* 查询客户的有效报备
* 查询学校+产品+项目类型的有效报备根据学校ID
*/
Report selectValidByCustomerId(@Param("customerId") Long customerId);
Report selectValidBySchoolAndProduct(@Param("schoolId") Long schoolId,
@Param("product") String product,
@Param("projectType") String projectType);
/**
* 查询学校名称+产品+项目类型的有效报备根据学校名称
*/
Report selectValidBySchoolNameAndProduct(@Param("schoolName") String schoolName,
@Param("product") String product,
@Param("projectType") String projectType);
/**
* 分页查询报备

View File

@ -12,6 +12,11 @@ import java.util.List;
@Mapper
public interface SchoolMapper {
/**
* 根据ID查询学校
*/
School selectById(@Param("id") Long id);
/**
* 插入学校
*/

View File

@ -4,6 +4,7 @@ import com.bycrm.common.PageResult;
import com.bycrm.dto.PageQuery;
import com.bycrm.dto.ReportAuditDTO;
import com.bycrm.dto.ReportDTO;
import com.bycrm.dto.ReportUpdateDTO;
import com.bycrm.vo.ReportVO;
/**
@ -27,6 +28,11 @@ public interface ReportService {
*/
void createReport(ReportDTO reportDTO, Long currentUserId);
/**
* 更新报备
*/
void updateReport(Long id, ReportUpdateDTO updateDTO, Long currentUserId);
/**
* 审核报备
*/

View File

@ -5,12 +5,13 @@ import com.bycrm.common.PageResult;
import com.bycrm.dto.PageQuery;
import com.bycrm.dto.ReportAuditDTO;
import com.bycrm.dto.ReportDTO;
import com.bycrm.dto.ReportUpdateDTO;
import com.bycrm.entity.Report;
import com.bycrm.entity.Customer;
import com.bycrm.entity.School;
import com.bycrm.entity.User;
import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.CustomerMapper;
import com.bycrm.mapper.ReportMapper;
import com.bycrm.mapper.SchoolMapper;
import com.bycrm.mapper.UserMapper;
import com.bycrm.service.ReportService;
import com.bycrm.service.SystemConfigService;
@ -32,16 +33,16 @@ import java.util.stream.Collectors;
public class ReportServiceImpl implements ReportService {
private final ReportMapper reportMapper;
private final CustomerMapper customerMapper;
private final SchoolMapper schoolMapper;
private final UserMapper userMapper;
private final SystemConfigService systemConfigService;
public ReportServiceImpl(ReportMapper reportMapper,
CustomerMapper customerMapper,
SchoolMapper schoolMapper,
UserMapper userMapper,
SystemConfigService systemConfigService) {
this.reportMapper = reportMapper;
this.customerMapper = customerMapper;
this.schoolMapper = schoolMapper;
this.userMapper = userMapper;
this.systemConfigService = systemConfigService;
}
@ -86,23 +87,47 @@ public class ReportServiceImpl implements ReportService {
throw new BusinessException("您未关联经销商,无法提交报备");
}
// 检查客户是否存在
Customer customer = customerMapper.selectById(reportDTO.getCustomerId());
if (customer == null) {
throw new BusinessException("客户不存在");
// 防撞单校验根据是否有 schoolId 选择不同的校验方式
Report existingReport = null;
if (reportDTO.getSchoolId() != null) {
// 有学校ID按学校ID+产品+项目类型校验
existingReport = reportMapper.selectValidBySchoolAndProduct(
reportDTO.getSchoolId(),
reportDTO.getProduct(),
reportDTO.getProjectType()
);
} else {
// 没有学校ID按学校名称+产品+项目类型校验
existingReport = reportMapper.selectValidBySchoolNameAndProduct(
reportDTO.getSchoolName(),
reportDTO.getProduct(),
reportDTO.getProjectType()
);
}
// 防撞单校验检查该客户是否已存在有效报备
Report existingReport = reportMapper.selectValidByCustomerId(reportDTO.getCustomerId());
Boolean allowOverlap = systemConfigService.getBooleanConfigValue("report.allow.overlap", false);
if (existingReport != null && !allowOverlap) {
throw new BusinessException("该客户已被其他经销商报备,无法重复报备");
throw new BusinessException("该项目已报备");
}
// 如果有学校ID查询学校信息
String schoolName = reportDTO.getSchoolName();
Long schoolId = reportDTO.getSchoolId();
if (schoolId != null) {
School school = schoolMapper.selectById(schoolId);
if (school != null) {
schoolName = school.getSchoolName();
}
}
// 创建报备
Report report = new Report();
report.setDealerId(currentUser.getDealerId());
report.setCustomerId(reportDTO.getCustomerId());
report.setSchoolId(schoolId); // 可以为 NULL
report.setSchoolName(schoolName);
report.setProduct(reportDTO.getProduct());
report.setProjectType(reportDTO.getProjectType());
report.setDescription(reportDTO.getDescription());
report.setStatus(Constants.REPORT_STATUS_PENDING);
report.setCreatedAt(LocalDateTime.now());
@ -138,11 +163,6 @@ public class ReportServiceImpl implements ReportService {
report.setProtectStartDate(today);
Integer protectDays = systemConfigService.getIntegerConfigValue("report.protect.days", Constants.DEFAULT_PROTECT_DAYS);
report.setProtectEndDate(today.plusDays(protectDays));
// 更新客户状态为保护中
Customer customer = customerMapper.selectById(report.getCustomerId());
customer.setStatus(Constants.CUSTOMER_STATUS_PROTECTED);
customerMapper.update(customer);
} else {
// 审核驳回
report.setStatus(Constants.REPORT_STATUS_REJECTED);
@ -153,6 +173,43 @@ public class ReportServiceImpl implements ReportService {
reportMapper.update(report);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateReport(Long id, ReportUpdateDTO updateDTO, Long currentUserId) {
// 获取当前用户
User currentUser = userMapper.selectById(currentUserId);
if (currentUser.getRole() != Constants.USER_ROLE_ADMIN) {
throw new BusinessException("只有管理员才能编辑报备");
}
Report report = reportMapper.selectById(id);
if (report == null) {
throw new BusinessException("报备不存在");
}
// 只有已通过的报备才能编辑
if (report.getStatus() != Constants.REPORT_STATUS_APPROVED) {
throw new BusinessException("只能编辑已通过的报备");
}
// 如果状态改为作废必须填写作废原因
if (updateDTO.getStatus() == Constants.REPORT_STATUS_CANCELED) {
if (updateDTO.getCancelReason() == null || updateDTO.getCancelReason().trim().isEmpty()) {
throw new BusinessException("作废时必须填写作废原因");
}
}
// 更新报备信息
report.setProduct(updateDTO.getProduct());
report.setProjectType(updateDTO.getProjectType());
report.setDescription(updateDTO.getDescription());
report.setStatus(updateDTO.getStatus());
report.setCancelReason(updateDTO.getCancelReason());
report.setUpdatedAt(LocalDateTime.now());
reportMapper.update(report);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void withdrawReport(Long id, Long currentUserId) {
@ -189,15 +246,6 @@ public class ReportServiceImpl implements ReportService {
// 批量更新报备状态
List<Long> ids = expiringReports.stream().map(Report::getId).collect(Collectors.toList());
reportMapper.batchUpdateExpired(ids);
// 批量更新客户状态为可报备
expiringReports.forEach(report -> {
Customer customer = customerMapper.selectById(report.getCustomerId());
if (customer != null && customer.getStatus() == Constants.CUSTOMER_STATUS_PROTECTED) {
customer.setStatus(Constants.CUSTOMER_STATUS_AVAILABLE);
customerMapper.update(customer);
}
});
}
@Override
@ -236,6 +284,9 @@ public class ReportServiceImpl implements ReportService {
case Constants.REPORT_STATUS_EXPIRED:
vo.setStatusDesc("已失效");
break;
case Constants.REPORT_STATUS_CANCELED:
vo.setStatusDesc("已作废");
break;
default:
vo.setStatusDesc("未知");
}

View File

@ -31,19 +31,24 @@ public class ReportVO implements Serializable {
private String dealerName;
/**
* 客户ID
* 学校ID
*/
private Long customerId;
private Long schoolId;
/**
* 客户名称
* 学校名称
*/
private String customerName;
private String schoolName;
/**
* 客户电话
* 所属产品
*/
private String customerPhone;
private String product;
/**
* 项目类型
*/
private String projectType;
/**
* 报备说明
@ -51,7 +56,7 @@ public class ReportVO implements Serializable {
private String description;
/**
* 状态0-待审核 1-已通过 2-已驳回 3-已失效
* 状态0-待审核 1-已通过 2-已驳回 3-已失效 4-已作废
*/
private Integer status;
@ -65,6 +70,11 @@ public class ReportVO implements Serializable {
*/
private String rejectReason;
/**
* 作废原因
*/
private String cancelReason;
/**
* 保护期开始日期
*/

View File

@ -6,45 +6,52 @@
<resultMap id="BaseResultMap" type="com.bycrm.entity.Report">
<id column="id" property="id"/>
<result column="dealer_id" property="dealerId"/>
<result column="customer_id" property="customerId"/>
<result column="school_id" property="schoolId"/>
<result column="school_name" property="schoolName"/>
<result column="product" property="product"/>
<result column="project_type" property="projectType"/>
<result column="description" property="description"/>
<result column="status" property="status"/>
<result column="reject_reason" property="rejectReason"/>
<result column="cancel_reason" property="cancelReason"/>
<result column="protect_start_date" property="protectStartDate"/>
<result column="protect_end_date" property="protectEndDate"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
<result column="dealer_name" property="dealerName"/>
<result column="customer_name" property="customerName"/>
<result column="customer_phone" property="customerPhone"/>
</resultMap>
<select id="selectById" resultMap="BaseResultMap">
SELECT r.*,
d.name AS dealer_name,
c.name AS customer_name,
c.phone AS customer_phone
d.name AS dealer_name
FROM crm_report r
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
LEFT JOIN crm_customer c ON r.customer_id = c.id
WHERE r.id = #{id}
</select>
<select id="selectValidByCustomerId" resultMap="BaseResultMap">
<select id="selectValidBySchoolAndProduct" resultMap="BaseResultMap">
SELECT * FROM crm_report
WHERE customer_id = #{customerId}
WHERE school_id = #{schoolId}
AND product = #{product}
AND project_type = #{projectType}
AND status IN (0, 1)
LIMIT 1
</select>
<select id="selectValidBySchoolNameAndProduct" resultMap="BaseResultMap">
SELECT * FROM crm_report
WHERE school_name = #{schoolName}
AND product = #{product}
AND project_type = #{projectType}
AND status IN (0, 1)
LIMIT 1
</select>
<select id="selectPage" resultMap="BaseResultMap">
SELECT r.*,
d.name AS dealer_name,
c.name AS customer_name,
c.phone AS customer_phone
d.name AS dealer_name
FROM crm_report r
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
LEFT JOIN crm_customer c ON r.customer_id = c.id
<where>
<if test="dealerId != null">
AND r.dealer_id = #{dealerId}
@ -53,7 +60,7 @@
AND d.name LIKE CONCAT('%', #{dealerName}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND c.name LIKE CONCAT('%', #{customerName}, '%')
AND r.school_name LIKE CONCAT('%', #{customerName}, '%')
</if>
<if test="status != null">
AND r.status = #{status}
@ -67,7 +74,6 @@
SELECT COUNT(*)
FROM crm_report r
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
LEFT JOIN crm_customer c ON r.customer_id = c.id
<where>
<if test="dealerId != null">
AND r.dealer_id = #{dealerId}
@ -76,7 +82,7 @@
AND d.name LIKE CONCAT('%', #{dealerName}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND c.name LIKE CONCAT('%', #{customerName}, '%')
AND r.school_name LIKE CONCAT('%', #{customerName}, '%')
</if>
<if test="status != null">
AND r.status = #{status}
@ -85,17 +91,22 @@
</select>
<insert id="insert" parameterType="com.bycrm.entity.Report" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_report (dealer_id, customer_id, description, status, protect_start_date, protect_end_date)
VALUES (#{dealerId}, #{customerId}, #{description}, #{status}, #{protectStartDate}, #{protectEndDate})
INSERT INTO crm_report (dealer_id, school_id, school_name, product, project_type, description, status, protect_start_date, protect_end_date)
VALUES (#{dealerId}, #{schoolId}, #{schoolName}, #{product}, #{projectType}, #{description}, #{status}, #{protectStartDate}, #{protectEndDate})
</insert>
<update id="update" parameterType="com.bycrm.entity.Report">
UPDATE crm_report
<set>
<if test="product != null">product = #{product},</if>
<if test="projectType != null">project_type = #{projectType},</if>
<if test="description != null">description = #{description},</if>
<if test="status != null">status = #{status},</if>
<if test="rejectReason != null">reject_reason = #{rejectReason},</if>
<if test="cancelReason != null">cancel_reason = #{cancelReason},</if>
<if test="protectStartDate != null">protect_start_date = #{protectStartDate},</if>
<if test="protectEndDate != null">protect_end_date = #{protectEndDate},</if>
updated_at = NOW()
</set>
WHERE id = #{id}
</update>

View File

@ -12,6 +12,12 @@
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="selectById" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.bycrm.entity.School" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_school (school_code, school_name, location, created_at, updated_at)
VALUES (#{schoolCode}, #{schoolName}, #{location}, #{createdAt}, #{updatedAt})

View File

@ -1,5 +1,5 @@
import { http } from '@/utils/request'
import type { Report, ReportForm, ReportAuditForm, PageQuery, PageResult } from '@/types'
import type { Report, ReportForm, ReportAuditForm, ReportUpdateForm, PageQuery, PageResult } from '@/types'
// 分页查询报备
export const getReportPage = (params: PageQuery & {
@ -21,6 +21,11 @@ export const createReport = (data: ReportForm) => {
return http.post('/report', data)
}
// 更新报备
export const updateReport = (id: number, data: ReportUpdateForm) => {
return http.put(`/report/${id}`, data)
}
// 审核报备
export const auditReport = (id: number, data: ReportAuditForm) => {
return http.put(`/report/${id}/audit`, data)

View File

@ -60,13 +60,15 @@ export interface Report {
id: number
dealerId: number
dealerName: string
customerId: number
customerName: string
customerPhone?: string
schoolId?: number
schoolName?: string
product?: string
projectType?: string
description?: string
status: number
statusDesc: string
rejectReason?: string
cancelReason?: string
protectStartDate?: string
protectEndDate?: string
remainDays?: number
@ -75,7 +77,10 @@ export interface Report {
}
export interface ReportForm {
customerId: number | null
schoolId?: number
schoolName: string
product: string
projectType: string
description?: string
}
@ -84,6 +89,14 @@ export interface ReportAuditForm {
rejectReason?: string
}
export interface ReportUpdateForm {
product: string
projectType: string
description?: string
status: number
cancelReason?: string
}
// 分页相关类型
export interface PageQuery {
current: number
@ -104,22 +117,6 @@ export interface DictItem {
value: string | number
}
// 报备进展记录相关类型
export interface ReportProgress {
id: number
reportId: number
progressContent: string
createdBy: number
creatorName?: string
createdAt: string
updatedAt: string
}
export interface ReportProgressForm {
reportId: number
progressContent: string
}
// 系统配置相关类型
export interface SystemConfig {
id: number

View File

@ -1,7 +1,7 @@
<template>
<div class="dashboard">
<el-row :gutter="20">
<el-col :span="6">
<!-- <el-col :span="userStore.isAdmin ? 6 : 8">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background-color: #409eff">
@ -14,8 +14,9 @@
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
-->
<el-col :span="userStore.isAdmin ? 8 : 12">
<el-card class="stat-card" @click="handleReportCountClick">
<div class="stat-content">
<div class="stat-icon" style="background-color: #67c23a">
<el-icon :size="30"><Document /></el-icon>
@ -27,8 +28,8 @@
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<el-col :span="userStore.isAdmin ? 8 : 12">
<el-card class="stat-card" @click="handlePendingClick">
<div class="stat-content">
<div class="stat-icon" style="background-color: #e6a23c">
<el-icon :size="30"><Clock /></el-icon>
@ -40,8 +41,8 @@
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<el-col :span="8" v-if="userStore.isAdmin">
<el-card class="stat-card" @click="handleDealerCountClick">
<div class="stat-content">
<div class="stat-icon" style="background-color: #f56c6c">
<el-icon :size="30"><Shop /></el-icon>
@ -57,18 +58,20 @@
<el-card class="welcome-card" style="margin-top: 20px">
<h3>欢迎使用泊云智销通</h3>
<p v-if="userStore.isAdmin">您是管理员可以管理所有客户报备和经销商信息</p>
<p v-else>您是经销商用户可以管理客户和提交报备申请</p>
<p v-if="userStore.isAdmin">您是管理员可以管理所有报备和经销商信息</p>
<p v-else>您是经销商用户可以提交报备申请</p>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { getStatistics } from '@/api/dashboard'
const router = useRouter()
const userStore = useUserStore()
const stats = ref({
@ -78,6 +81,32 @@ const stats = ref({
dealerCount: 0
})
//
const handleReportCountClick = () => {
router.push({
path: '/report',
query: {}
})
}
//
const handlePendingClick = () => {
router.push({
path: '/report',
query: {
status: '0'
}
})
}
//
const handleDealerCountClick = () => {
router.push({
path: '/dealer',
query: {}
})
}
//
const fetchStatistics = async () => {
try {

View File

@ -13,10 +13,10 @@
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/customer">
<!-- <el-menu-item index="/customer">
<el-icon><User /></el-icon>
<span>客户管理</span>
</el-menu-item>
</el-menu-item> -->
<el-menu-item index="/report">
<el-icon><Document /></el-icon>
<span>报备管理</span>
@ -43,11 +43,11 @@
<el-dropdown>
<span class="user-name">
<el-icon><Avatar /></el-icon>
{{ userStore.userInfo?.realName }}
{{ userStore.userInfo?.dealerName }}
<el-tag v-if="userStore.isAdmin" type="danger" size="small" style="margin-left: 8px">
管理员
</el-tag>
<el-tag v-else type="success" size="small" style="margin-left: 8px">经销商</el-tag>
<!-- <el-tag v-else type="success" size="small" style="margin-left: 8px">经销商</el-tag> -->
</span>
<template #dropdown>
<el-dropdown-menu>

View File

@ -18,8 +18,8 @@
<!-- 查询表单 -->
<el-form :inline="true" :model="queryForm" class="query-form">
<el-form-item label="客户名称">
<el-input v-model="queryForm.customerName" placeholder="请输入客户名称" clearable />
<el-form-item label="学校名称">
<el-input v-model="queryForm.customerName" placeholder="请输入学校名称" clearable />
</el-form-item>
<el-form-item label="状态" style="width: 180px;">
<el-select v-model="queryForm.status" placeholder="请选择状态" clearable>
@ -27,6 +27,7 @@
<el-option label="已通过" :value="1" />
<el-option label="已驳回" :value="2" />
<el-option label="已失效" :value="3" />
<el-option label="已作废" :value="4" />
</el-select>
</el-form-item>
<el-form-item>
@ -40,16 +41,18 @@
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="dealerName" label="经销商" />
<el-table-column prop="customerName" label="客户名称" />
<el-table-column prop="customerPhone" label="联系电话" />
<el-table-column prop="dealerName" label="经销商" v-if="userStore.isAdmin"/>
<el-table-column prop="schoolName" label="学校名称" />
<el-table-column prop="product" label="所属产品" show-overflow-tooltip />
<el-table-column prop="projectType" label="项目类型" show-overflow-tooltip />
<el-table-column prop="description" label="报备说明" show-overflow-tooltip />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag v-if="row.status === 0" type="warning">待审核</el-tag>
<el-tag v-else-if="row.status === 1" type="success">已通过</el-tag>
<el-tag v-else-if="row.status === 2" type="danger">已驳回</el-tag>
<el-tag v-else type="info">已失效</el-tag>
<el-tag v-else-if="row.status === 3" type="info">已失效</el-tag>
<el-tag v-else-if="row.status === 4" type="warning">已作废</el-tag>
</template>
</el-table-column>
<el-table-column prop="protectEndDate" label="保护期截止" width="180">
@ -58,7 +61,7 @@
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="250" fixed="right">
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleView(row)">查看</el-button>
<el-button
@ -77,6 +80,14 @@
>
审核
</el-button>
<el-button
v-if="userStore.isAdmin && row.status === 1"
link
type="warning"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="!userStore.isAdmin && row.status === 0"
link
@ -109,23 +120,36 @@
<!-- 提交报备对话框 -->
<el-dialog v-model="dialogVisible" title="提交报备" width="600px" @close="handleDialogClose">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="客户" prop="customerId">
<el-select
v-model="formData.customerId"
placeholder="请选择客户"
filterable
remote
:remote-method="searchCustomer"
:loading="searchLoading"
<el-form-item label="学校" prop="schoolName">
<el-autocomplete
v-model="formData.schoolName"
:fetch-suggestions="searchSchool"
placeholder="请输入学校名称"
:trigger-on-focus="false"
style="width: 100%"
@select="handleSchoolSelect"
value-key="schoolName"
clearable
>
<el-option
v-for="item in customerOptions"
:key="item.id"
:label="item.name"
:value="item.id"
<template #default="{ item }">
<div>{{ item.schoolName }}</div>
<div style="font-size: 12px; color: #999;">{{ item.location || '无地址信息' }}</div>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item label="所属产品" prop="product">
<el-input
v-model="formData.product"
placeholder="请输入所属产品"
clearable
/>
</el-form-item>
<el-form-item label="项目类型" prop="projectType">
<el-input
v-model="formData.projectType"
placeholder="请输入项目类型"
clearable
/>
</el-select>
</el-form-item>
<el-form-item label="报备说明" prop="description">
<el-input
@ -166,6 +190,109 @@
</template>
</el-dialog>
<!-- 编辑报备对话框 -->
<el-dialog v-model="editDialogVisible" title="编辑报备" width="600px" @close="handleEditDialogClose">
<el-form ref="editFormRef" :model="editFormData" :rules="editRules" label-width="100px">
<el-form-item label="学校名称">
<el-input :model-value="currentReport?.schoolName" disabled />
</el-form-item>
<el-form-item label="所属产品" prop="product">
<el-input
v-model="editFormData.product"
placeholder="请输入所属产品"
clearable
/>
</el-form-item>
<el-form-item label="项目类型" prop="projectType">
<el-input
v-model="editFormData.projectType"
placeholder="请输入项目类型"
clearable
/>
</el-form-item>
<el-form-item label="报备说明" prop="description">
<el-input
v-model="editFormData.description"
type="textarea"
:rows="4"
placeholder="请输入报备说明"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="editFormData.status" placeholder="请选择状态" style="width: 100%">
<el-option label="已通过" :value="1" />
<el-option label="已作废" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="作废原因" prop="cancelReason" v-if="editFormData.status === 4">
<el-input
v-model="editFormData.cancelReason"
type="textarea"
:rows="4"
placeholder="请填写作废原因"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleEditSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 报备详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="报备详情" width="700px">
<el-descriptions :column="2" border>
<el-descriptions-item label="经销商">
{{ currentReport?.dealerName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="学校名称">
{{ currentReport?.schoolName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="所属产品">
{{ currentReport?.product || '-' }}
</el-descriptions-item>
<el-descriptions-item label="项目类型">
{{ currentReport?.projectType || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag v-if="currentReport?.status === 0" type="warning">待审核</el-tag>
<el-tag v-else-if="currentReport?.status === 1" type="success">已通过</el-tag>
<el-tag v-else-if="currentReport?.status === 2" type="danger">已驳回</el-tag>
<el-tag v-else-if="currentReport?.status === 3" type="info">已失效</el-tag>
<el-tag v-else-if="currentReport?.status === 4" type="warning">已作废</el-tag>
</el-descriptions-item>
<el-descriptions-item label="保护期开始" :span="1">
{{ formatDate(currentReport?.protectStartDate) }}
</el-descriptions-item>
<el-descriptions-item label="保护期结束" :span="1">
{{ formatDate(currentReport?.protectEndDate) }}
</el-descriptions-item>
<el-descriptions-item label="剩余保护天数" v-if="currentReport?.status === 1 && currentReport?.remainDays !== undefined">
<el-tag :type="currentReport.remainDays > 7 ? 'success' : currentReport.remainDays > 0 ? 'warning' : 'danger'">
{{ currentReport.remainDays }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="驳回原因" v-if="currentReport?.status === 2" :span="2">
<span style="color: #f56c6c;">{{ currentReport?.rejectReason || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="作废原因" v-if="currentReport?.status === 4" :span="2">
<span style="color: #e6a23c;">{{ currentReport?.cancelReason || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="报备说明" :span="2">
<div style="white-space: pre-wrap;">{{ currentReport?.description || '-' }}</div>
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ currentReport?.createdAt || '-' }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ currentReport?.updatedAt || '-' }}
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="primary" @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 进展记录对话框 -->
<el-dialog v-model="progressDialogVisible" title="报备进展记录" width="1000px" @close="handleProgressDialogClose">
<el-card>
@ -215,15 +342,17 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import { getReportPage, createReport, auditReport, withdrawReport } from '@/api/report'
import { getReportPage, createReport, auditReport, withdrawReport, updateReport } from '@/api/report'
import { getProgressByReportId, createProgress, updateProgress } from '@/api/report-progress'
import { searchCustomerByName } from '@/api/customer'
import { searchSchoolByName } from '@/api/school'
import { getConfigValue } from '@/api/system'
import type { Report, ReportForm, ReportProgress, ReportProgressForm } from '@/types'
import type { Report, ReportForm, ReportProgress, ReportProgressForm, ReportUpdateForm } from '@/types'
const route = useRoute()
const userStore = useUserStore()
const protectDays = ref<number>(90)
@ -233,10 +362,14 @@ const tableData = ref<Report[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const auditDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const editDialogVisible = ref(false)
const auditReportId = ref<number>()
const editingReportId = ref<number>()
const formRef = ref<FormInstance>()
const searchLoading = ref(false)
const customerOptions = ref<Array<{ id: number; name: string }>>([])
const editFormRef = ref<FormInstance>()
const selectedSchool = ref<any>(null)
const currentReport = ref<Report | null>(null)
//
const progressDialogVisible = ref(false)
@ -261,7 +394,10 @@ const queryForm = reactive({
})
const formData = reactive<ReportForm>({
customerId: null,
schoolId: undefined,
schoolName: '',
product: '',
projectType: '',
description: ''
})
@ -270,8 +406,25 @@ const auditForm = reactive({
rejectReason: ''
})
const editFormData = reactive<ReportUpdateForm>({
product: '',
projectType: '',
description: '',
status: 1,
cancelReason: ''
})
const rules: FormRules = {
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }]
schoolName: [{ required: true, message: '请输入学校名称', trigger: 'blur' }],
product: [{ required: true, message: '请输入所属产品', trigger: 'blur' }],
projectType: [{ required: true, message: '请输入项目类型', trigger: 'blur' }]
}
const editRules: FormRules = {
product: [{ required: true, message: '请输入所属产品', trigger: 'blur' }],
projectType: [{ required: true, message: '请输入项目类型', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
cancelReason: [{ required: true, message: '请填写作废原因', trigger: 'blur' }]
}
//
@ -365,18 +518,29 @@ const fetchData = async () => {
}
}
//
const searchCustomer = async (query: string) => {
if (!query) return
searchLoading.value = true
try {
const res = await searchCustomerByName(query)
customerOptions.value = res
} catch (error) {
console.error('搜索客户失败', error)
} finally {
searchLoading.value = false
//
const searchSchool = async (queryString: string, cb: any) => {
if (!queryString || queryString.trim().length < 1) {
cb([])
return
}
try {
const schools = await searchSchoolByName(queryString)
const suggestions = schools.map((school: any) => ({
...school,
value: school.schoolName
}))
cb(suggestions)
} catch (error) {
console.error('搜索学校失败', error)
cb([])
}
}
//
const handleSchoolSelect = (item: any) => {
selectedSchool.value = item
formData.schoolId = item.id
}
//
@ -400,24 +564,8 @@ const handleAdd = () => {
//
const handleView = (row: Report) => {
ElMessageBox.alert(
`
<div style="line-height: 2;">
<p><strong>经销商</strong>${row.dealerName}</p>
<p><strong>客户名称</strong>${row.customerName}</p>
<p><strong>联系电话</strong>${row.customerPhone || '-'}</p>
<p><strong>报备说明</strong>${row.description || '-'}</p>
<p><strong>状态</strong>${row.statusDesc}</p>
<p><strong>驳回原因</strong>${row.rejectReason || '-'}</p>
<p><strong>保护期</strong>${formatDate(row.protectStartDate)} ~ ${formatDate(row.protectEndDate)}</p>
</div>
`,
'报备详情',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '关闭'
}
)
currentReport.value = row
detailDialogVisible.value = true
}
//
@ -428,6 +576,51 @@ const handleAudit = (row: Report) => {
auditDialogVisible.value = true
}
//
const handleEdit = (row: Report) => {
editingReportId.value = row.id
currentReport.value = row
editFormData.product = row.product || ''
editFormData.projectType = row.projectType || ''
editFormData.description = row.description || ''
editFormData.status = row.status
editFormData.cancelReason = row.cancelReason || ''
editDialogVisible.value = true
}
//
const handleEditDialogClose = () => {
editFormRef.value?.resetFields()
Object.assign(editFormData, {
product: '',
projectType: '',
description: '',
status: 1,
cancelReason: ''
})
editingReportId.value = undefined
}
//
const handleEditSubmit = async () => {
if (!editingReportId.value) return
//
if (editFormData.status === 4 && !editFormData.cancelReason) {
ElMessage.warning('请填写作废原因')
return
}
try {
await updateReport(editingReportId.value, editFormData)
ElMessage.success('编辑成功')
editDialogVisible.value = false
fetchData()
} catch (error) {
console.error('编辑失败', error)
}
}
//
const handleWithdraw = async (row: Report) => {
try {
@ -486,10 +679,13 @@ const handleAuditSubmit = async () => {
const handleDialogClose = () => {
formRef.value?.resetFields()
Object.assign(formData, {
customerId: null,
schoolId: undefined,
schoolName: '',
product: '',
projectType: '',
description: ''
})
customerOptions.value = []
selectedSchool.value = null
}
//
@ -504,6 +700,13 @@ const fetchProtectDays = async () => {
onMounted(() => {
fetchProtectDays()
//
const { status } = route.query
if (status !== undefined) {
queryForm.status = status === '' ? undefined : parseInt(status as string)
}
fetchData()
})
</script>
@ -522,4 +725,18 @@ onMounted(() => {
.query-form {
margin-bottom: 20px;
}
/* 详情对话框样式优化 */
:deep(.el-descriptions) {
margin-top: 10px;
}
:deep(.el-descriptions__label) {
font-weight: 600;
background-color: #fafafa;
}
:deep(.el-descriptions__content) {
color: #333;
}
</style>