chore: 从 git 中移除 backend/target 目录
- 使用 git rm --cached 从版本控制中删除 target 目录 - .gitignore 已配置忽略 target 和 node_modules - 本地文件保留,仅从 git 索引中删除 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
934b8aad40
commit
ea8f8390da
|
|
@ -1,81 +0,0 @@
|
||||||
server:
|
|
||||||
port: 8080
|
|
||||||
servlet:
|
|
||||||
context-path: /api
|
|
||||||
|
|
||||||
spring:
|
|
||||||
application:
|
|
||||||
name: by-crm
|
|
||||||
datasource:
|
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
url: jdbc:mysql://192.168.3.80:3306/by_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
|
||||||
username: root
|
|
||||||
password: "Boyun@123"
|
|
||||||
druid:
|
|
||||||
initial-size: 5
|
|
||||||
min-idle: 5
|
|
||||||
max-active: 20
|
|
||||||
max-wait: 60000
|
|
||||||
time-between-eviction-runs-millis: 60000
|
|
||||||
min-evictable-idle-time-millis: 300000
|
|
||||||
validation-query: SELECT 1
|
|
||||||
test-while-idle: true
|
|
||||||
test-on-borrow: false
|
|
||||||
test-on-return: false
|
|
||||||
filters: stat,wall
|
|
||||||
web-stat-filter:
|
|
||||||
enabled: true
|
|
||||||
url-pattern: /*
|
|
||||||
stat-view-servlet:
|
|
||||||
enabled: true
|
|
||||||
url-pattern: /druid/*
|
|
||||||
reset-enable: false
|
|
||||||
login-username: admin
|
|
||||||
login-password: admin123
|
|
||||||
|
|
||||||
jackson:
|
|
||||||
time-zone: GMT+8
|
|
||||||
date-format: yyyy-MM-dd HH:mm:ss
|
|
||||||
default-property-inclusion: non_null
|
|
||||||
|
|
||||||
mvc:
|
|
||||||
pathmatch:
|
|
||||||
matching-strategy: ant_path_matcher
|
|
||||||
|
|
||||||
# MyBatis 配置
|
|
||||||
mybatis:
|
|
||||||
mapper-locations: classpath:mapper/*.xml
|
|
||||||
type-aliases-package: com.bycrm.entity
|
|
||||||
configuration:
|
|
||||||
map-underscore-to-camel-case: true
|
|
||||||
cache-enabled: false
|
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
|
||||||
|
|
||||||
# JWT 配置
|
|
||||||
jwt:
|
|
||||||
# HS512 算法要求密钥至少 64 字节(512 位)
|
|
||||||
# 生产环境请使用环境变量或密钥管理系统存储此密钥
|
|
||||||
secret: by-crm-jwt-secret-key-2024-hs512-requires-at-least-64-bytes-for-secure-signing-please-change-in-production-environment
|
|
||||||
expiration: 86400000 # 24小时,单位:毫秒
|
|
||||||
|
|
||||||
# CRM 业务配置
|
|
||||||
crm:
|
|
||||||
report:
|
|
||||||
ttl-days: 90 # 保护期天数
|
|
||||||
allow-overlap: false # 是否允许重叠报备(生产环境必须为 false)
|
|
||||||
|
|
||||||
# Swagger 配置
|
|
||||||
springfox:
|
|
||||||
documentation:
|
|
||||||
swagger-ui:
|
|
||||||
enabled: true
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
com.bycrm.mapper: debug
|
|
||||||
org.springframework.web: info
|
|
||||||
pattern:
|
|
||||||
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,119 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.bycrm.mapper.CustomerMapper">
|
|
||||||
|
|
||||||
<resultMap id="BaseResultMap" type="com.bycrm.entity.Customer">
|
|
||||||
<id column="id" property="id"/>
|
|
||||||
<result column="name" property="name"/>
|
|
||||||
<result column="phone" property="phone"/>
|
|
||||||
<result column="address" property="address"/>
|
|
||||||
<result column="industry" property="industry"/>
|
|
||||||
<result column="status" property="status"/>
|
|
||||||
<result column="created_at" property="createdAt"/>
|
|
||||||
<result column="updated_at" property="updatedAt"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<resultMap id="CustomerVOResultMap" type="com.bycrm.vo.CustomerVO" extends="BaseResultMap">
|
|
||||||
<result column="status_desc" property="statusDesc"/>
|
|
||||||
<result column="current_dealer_id" property="currentDealerId"/>
|
|
||||||
<result column="current_dealer_name" property="currentDealerName"/>
|
|
||||||
<result column="protect_end_date" property="protectEndDate"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<select id="selectById" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_customer WHERE id = #{id}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectByNameLike" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_customer
|
|
||||||
WHERE name LIKE CONCAT('%', #{name}, '%')
|
|
||||||
LIMIT 10
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectPage" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_customer
|
|
||||||
<where>
|
|
||||||
<if test="name != null and name != ''">
|
|
||||||
AND name LIKE CONCAT('%', #{name}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="industry != null and industry != ''">
|
|
||||||
AND industry = #{industry}
|
|
||||||
</if>
|
|
||||||
<if test="status != null">
|
|
||||||
AND status = #{status}
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT #{query.size} OFFSET #{query.offset}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="countPage" resultType="java.lang.Long">
|
|
||||||
SELECT COUNT(*) FROM crm_customer
|
|
||||||
<where>
|
|
||||||
<if test="name != null and name != ''">
|
|
||||||
AND name LIKE CONCAT('%', #{name}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="industry != null and industry != ''">
|
|
||||||
AND industry = #{industry}
|
|
||||||
</if>
|
|
||||||
<if test="status != null">
|
|
||||||
AND status = #{status}
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<insert id="insert" parameterType="com.bycrm.entity.Customer" useGeneratedKeys="true" keyProperty="id">
|
|
||||||
INSERT INTO crm_customer (name, phone, address, industry, status)
|
|
||||||
VALUES (#{name}, #{phone}, #{address}, #{industry}, #{status})
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<update id="update" parameterType="com.bycrm.entity.Customer">
|
|
||||||
UPDATE crm_customer
|
|
||||||
<set>
|
|
||||||
<if test="name != null and name != ''">name = #{name},</if>
|
|
||||||
<if test="phone != null">phone = #{phone},</if>
|
|
||||||
<if test="address != null">address = #{address},</if>
|
|
||||||
<if test="industry != null">industry = #{industry},</if>
|
|
||||||
<if test="status != null">status = #{status},</if>
|
|
||||||
</set>
|
|
||||||
WHERE id = #{id}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<delete id="deleteById">
|
|
||||||
DELETE FROM crm_customer WHERE id = #{id}
|
|
||||||
</delete>
|
|
||||||
|
|
||||||
<select id="selectPageWithDealer" resultMap="CustomerVOResultMap">
|
|
||||||
SELECT
|
|
||||||
c.*,
|
|
||||||
CASE c.status
|
|
||||||
WHEN 0 THEN '可报备'
|
|
||||||
WHEN 1 THEN '保护中'
|
|
||||||
WHEN 2 THEN '已失效'
|
|
||||||
ELSE '未知'
|
|
||||||
END AS status_desc,
|
|
||||||
r.dealer_id AS current_dealer_id,
|
|
||||||
d.name AS current_dealer_name,
|
|
||||||
r.protect_end_date
|
|
||||||
FROM crm_customer c
|
|
||||||
LEFT JOIN crm_report r ON c.id = r.customer_id
|
|
||||||
AND r.status = 1
|
|
||||||
AND r.protect_end_date > NOW()
|
|
||||||
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
|
|
||||||
<where>
|
|
||||||
<if test="name != null and name != ''">
|
|
||||||
AND c.name LIKE CONCAT('%', #{name}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="industry != null and industry != ''">
|
|
||||||
AND c.industry = #{industry}
|
|
||||||
</if>
|
|
||||||
<if test="status != null">
|
|
||||||
AND c.status = #{status}
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
ORDER BY c.created_at DESC
|
|
||||||
LIMIT #{query.size} OFFSET #{query.offset}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</mapper>
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.bycrm.mapper.DealerMapper">
|
|
||||||
|
|
||||||
<resultMap id="BaseResultMap" type="com.bycrm.entity.Dealer">
|
|
||||||
<id column="id" property="id"/>
|
|
||||||
<result column="name" property="name"/>
|
|
||||||
<result column="code" property="code"/>
|
|
||||||
<result column="contact_person" property="contactPerson"/>
|
|
||||||
<result column="contact_phone" property="contactPhone"/>
|
|
||||||
<result column="email" property="email"/>
|
|
||||||
<result column="status" property="status"/>
|
|
||||||
<result column="created_at" property="createdAt"/>
|
|
||||||
<result column="updated_at" property="updatedAt"/>
|
|
||||||
<result column="user_id" property="userId"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<select id="selectById" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_dealer WHERE id = #{id}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectByCode" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_dealer WHERE code = #{code}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectList" resultMap="BaseResultMap">
|
|
||||||
SELECT d.*, u.id as user_id
|
|
||||||
FROM crm_dealer d
|
|
||||||
LEFT JOIN crm_user u ON u.dealer_id = d.id
|
|
||||||
<where>
|
|
||||||
<if test="name != null and name != ''">
|
|
||||||
AND d.name LIKE CONCAT('%', #{name}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="code != null and code != ''">
|
|
||||||
AND d.code LIKE CONCAT('%', #{code}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="status != null">
|
|
||||||
AND d.status = #{status}
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
ORDER BY d.created_at DESC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<insert id="insert" parameterType="com.bycrm.entity.Dealer" useGeneratedKeys="true" keyProperty="id">
|
|
||||||
INSERT INTO crm_dealer (name, code, contact_person, contact_phone, email, status)
|
|
||||||
VALUES (#{name}, #{code}, #{contactPerson}, #{contactPhone}, #{email}, #{status})
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<update id="update" parameterType="com.bycrm.entity.Dealer">
|
|
||||||
UPDATE crm_dealer
|
|
||||||
<set>
|
|
||||||
<if test="name != null and name != ''">name = #{name},</if>
|
|
||||||
<if test="code != null and code != ''">code = #{code},</if>
|
|
||||||
<if test="contactPerson != null and contactPerson != ''">contact_person = #{contactPerson},</if>
|
|
||||||
<if test="contactPhone != null and contactPhone != ''">contact_phone = #{contactPhone},</if>
|
|
||||||
<if test="email != null">email = #{email},</if>
|
|
||||||
<if test="status != null">status = #{status},</if>
|
|
||||||
</set>
|
|
||||||
WHERE id = #{id}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<delete id="deleteById">
|
|
||||||
DELETE FROM crm_dealer WHERE id = #{id}
|
|
||||||
</delete>
|
|
||||||
|
|
||||||
<select id="count" resultType="java.lang.Long">
|
|
||||||
SELECT COUNT(*) FROM crm_dealer
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</mapper>
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.bycrm.mapper.ReportMapper">
|
|
||||||
|
|
||||||
<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="description" property="description"/>
|
|
||||||
<result column="status" property="status"/>
|
|
||||||
<result column="reject_reason" property="rejectReason"/>
|
|
||||||
<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
|
|
||||||
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 * FROM crm_report
|
|
||||||
WHERE customer_id = #{customerId}
|
|
||||||
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
|
|
||||||
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}
|
|
||||||
</if>
|
|
||||||
<if test="dealerName != null and dealerName != ''">
|
|
||||||
AND d.name LIKE CONCAT('%', #{dealerName}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="customerName != null and customerName != ''">
|
|
||||||
AND c.name LIKE CONCAT('%', #{customerName}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="status != null">
|
|
||||||
AND r.status = #{status}
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
ORDER BY r.created_at DESC
|
|
||||||
LIMIT #{query.size} OFFSET #{query.offset}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="countPage" resultType="java.lang.Long">
|
|
||||||
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}
|
|
||||||
</if>
|
|
||||||
<if test="dealerName != null and dealerName != ''">
|
|
||||||
AND d.name LIKE CONCAT('%', #{dealerName}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="customerName != null and customerName != ''">
|
|
||||||
AND c.name LIKE CONCAT('%', #{customerName}, '%')
|
|
||||||
</if>
|
|
||||||
<if test="status != null">
|
|
||||||
AND r.status = #{status}
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<update id="update" parameterType="com.bycrm.entity.Report">
|
|
||||||
UPDATE crm_report
|
|
||||||
<set>
|
|
||||||
<if test="status != null">status = #{status},</if>
|
|
||||||
<if test="rejectReason != null">reject_reason = #{rejectReason},</if>
|
|
||||||
<if test="protectStartDate != null">protect_start_date = #{protectStartDate},</if>
|
|
||||||
<if test="protectEndDate != null">protect_end_date = #{protectEndDate},</if>
|
|
||||||
</set>
|
|
||||||
WHERE id = #{id}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<delete id="deleteById">
|
|
||||||
DELETE FROM crm_report WHERE id = #{id}
|
|
||||||
</delete>
|
|
||||||
|
|
||||||
<select id="selectExpiringReports" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_report
|
|
||||||
WHERE status = 1
|
|
||||||
AND protect_end_date <= #{endDate}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<update id="batchUpdateExpired">
|
|
||||||
UPDATE crm_report
|
|
||||||
SET status = 3
|
|
||||||
WHERE id IN
|
|
||||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
|
||||||
#{id}
|
|
||||||
</foreach>
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<select id="countByDealerId" resultType="java.lang.Long">
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM crm_report
|
|
||||||
<where>
|
|
||||||
<if test="dealerId != null">
|
|
||||||
AND dealer_id = #{dealerId}
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="countPendingByDealerId" resultType="java.lang.Long">
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM crm_report
|
|
||||||
WHERE status = 0
|
|
||||||
<if test="dealerId != null">
|
|
||||||
AND dealer_id = #{dealerId}
|
|
||||||
</if>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</mapper>
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.bycrm.mapper.SchoolMapper">
|
|
||||||
|
|
||||||
<resultMap id="BaseResultMap" type="com.bycrm.entity.School">
|
|
||||||
<id column="id" property="id"/>
|
|
||||||
<result column="school_code" property="schoolCode"/>
|
|
||||||
<result column="school_name" property="schoolName"/>
|
|
||||||
<result column="location" property="location"/>
|
|
||||||
<result column="created_at" property="createdAt"/>
|
|
||||||
<result column="updated_at" property="updatedAt"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<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})
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<insert id="batchInsert" parameterType="java.util.List">
|
|
||||||
INSERT INTO crm_school (school_code, school_name, location, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
<foreach collection="list" item="item" separator=",">
|
|
||||||
(#{item.schoolCode}, #{item.schoolName}, #{item.location}, #{item.createdAt}, #{item.updatedAt})
|
|
||||||
</foreach>
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<select id="searchByName" resultMap="BaseResultMap">
|
|
||||||
SELECT id, school_code, school_name, location, created_at, updated_at
|
|
||||||
FROM crm_school
|
|
||||||
WHERE school_name LIKE CONCAT('%', #{keyword}, '%')
|
|
||||||
ORDER BY school_name
|
|
||||||
LIMIT 50
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="findByCode" resultMap="BaseResultMap">
|
|
||||||
SELECT id, school_code, school_name, location, created_at, updated_at
|
|
||||||
FROM crm_school
|
|
||||||
WHERE school_code = #{schoolCode}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="findAll" resultMap="BaseResultMap">
|
|
||||||
SELECT id, school_code, school_name, location, created_at, updated_at
|
|
||||||
FROM crm_school
|
|
||||||
ORDER BY school_name
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="findByPage" resultMap="BaseResultMap">
|
|
||||||
SELECT id, school_code, school_name, location, created_at, updated_at
|
|
||||||
FROM crm_school
|
|
||||||
ORDER BY school_name
|
|
||||||
LIMIT #{offset}, #{limit}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="count" resultType="int">
|
|
||||||
SELECT COUNT(*) FROM crm_school
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</mapper>
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.bycrm.mapper.SystemConfigMapper">
|
|
||||||
|
|
||||||
<resultMap id="BaseResultMap" type="com.bycrm.entity.SystemConfig">
|
|
||||||
<id column="id" property="id"/>
|
|
||||||
<result column="config_key" property="configKey"/>
|
|
||||||
<result column="config_value" property="configValue"/>
|
|
||||||
<result column="config_label" property="configLabel"/>
|
|
||||||
<result column="config_type" property="configType"/>
|
|
||||||
<result column="description" property="description"/>
|
|
||||||
<result column="is_editable" property="isEditable"/>
|
|
||||||
<result column="created_at" property="createdAt"/>
|
|
||||||
<result column="updated_at" property="updatedAt"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<select id="selectAll" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_system_config ORDER BY id
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectByKey" resultMap="BaseResultMap">
|
|
||||||
SELECT * FROM crm_system_config WHERE config_key = #{configKey}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectValueByKey" resultType="java.lang.String">
|
|
||||||
SELECT config_value FROM crm_system_config WHERE config_key = #{configKey}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<update id="update" parameterType="com.bycrm.entity.SystemConfig">
|
|
||||||
UPDATE crm_system_config
|
|
||||||
SET config_value = #{configValue},
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE config_key = #{configKey}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<update id="batchUpdate">
|
|
||||||
<foreach collection="configs" item="config" separator=";">
|
|
||||||
UPDATE crm_system_config
|
|
||||||
SET config_value = #{config.configValue},
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE config_key = #{config.configKey}
|
|
||||||
</foreach>
|
|
||||||
</update>
|
|
||||||
|
|
||||||
</mapper>
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.bycrm.mapper.UserMapper">
|
|
||||||
|
|
||||||
<resultMap id="BaseResultMap" type="com.bycrm.entity.User">
|
|
||||||
<id column="id" property="id"/>
|
|
||||||
<result column="username" property="username"/>
|
|
||||||
<result column="password" property="password"/>
|
|
||||||
<result column="real_name" property="realName"/>
|
|
||||||
<result column="dealer_id" property="dealerId"/>
|
|
||||||
<result column="role" property="role"/>
|
|
||||||
<result column="status" property="status"/>
|
|
||||||
<result column="created_at" property="createdAt"/>
|
|
||||||
<result column="updated_at" property="updatedAt"/>
|
|
||||||
<result column="dealer_name" property="dealerName"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<select id="selectByUsername" resultMap="BaseResultMap">
|
|
||||||
SELECT u.*,
|
|
||||||
d.name AS dealer_name
|
|
||||||
FROM crm_user u
|
|
||||||
LEFT JOIN crm_dealer d ON u.dealer_id = d.id
|
|
||||||
WHERE u.username = #{username}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectById" resultMap="BaseResultMap">
|
|
||||||
SELECT u.*,
|
|
||||||
d.name AS dealer_name
|
|
||||||
FROM crm_user u
|
|
||||||
LEFT JOIN crm_dealer d ON u.dealer_id = d.id
|
|
||||||
WHERE u.id = #{id}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectList" resultMap="BaseResultMap">
|
|
||||||
SELECT u.*,
|
|
||||||
d.name AS dealer_name
|
|
||||||
FROM crm_user u
|
|
||||||
LEFT JOIN crm_dealer d ON u.dealer_id = d.id
|
|
||||||
ORDER BY u.created_at DESC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<insert id="insert" parameterType="com.bycrm.entity.User" useGeneratedKeys="true" keyProperty="id">
|
|
||||||
INSERT INTO crm_user (username, password, real_name, dealer_id, role, status)
|
|
||||||
VALUES (#{username}, #{password}, #{realName}, #{dealerId}, #{role}, #{status})
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<update id="update" parameterType="com.bycrm.entity.User">
|
|
||||||
UPDATE crm_user
|
|
||||||
<set>
|
|
||||||
<if test="password != null and password != ''">password = #{password},</if>
|
|
||||||
<if test="realName != null and realName != ''">real_name = #{realName},</if>
|
|
||||||
<if test="dealerId != null">dealer_id = #{dealerId},</if>
|
|
||||||
<if test="role != null">role = #{role},</if>
|
|
||||||
<if test="status != null">status = #{status},</if>
|
|
||||||
</set>
|
|
||||||
WHERE id = #{id}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<delete id="deleteById">
|
|
||||||
DELETE FROM crm_user WHERE id = #{id}
|
|
||||||
</delete>
|
|
||||||
|
|
||||||
</mapper>
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\DealerDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\DealerController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportAuditDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\ReportServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\LoginDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\AuthController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\util\JwtUtil.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\PageQuery.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Result.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dict.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\BusinessException.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\WebMvcConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\UserInfoVO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\DealerMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\UserMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Report.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Constants.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\PageResult.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\CorsConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\ReportController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Customer.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\UserService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\SwaggerConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\ByCrmApplication.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\ReportService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\User.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dealer.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\DealerService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\ReportMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\DealerServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\OperationLog.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\CustomerVO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\DictItem.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\GlobalExceptionHandler.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\ReportVO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\CustomerService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\MyBatisConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\CustomerDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\CustomerMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\CustomerController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\CustomerServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\annotations\AuthRequired.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\AuthInterceptor.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\UserServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\task\ReportExpireTask.java
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
com\bycrm\util\PasswordTest.class
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\DealerDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\DealerController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportAuditDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\ReportServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\LoginDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\AuthController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\util\JwtUtil.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\PageQuery.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Result.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dict.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\BusinessException.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\WebMvcConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\UserInfoVO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\DealerMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\UserMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Report.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Constants.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\PageResult.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\CorsConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\ReportController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\util\PasswordTest.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Customer.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\UserService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\SwaggerConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\ByCrmApplication.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\ReportService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\User.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dealer.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\DealerService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\ReportMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\DealerServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\OperationLog.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\CustomerVO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\DictItem.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\GlobalExceptionHandler.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\ReportVO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\CustomerService.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\MyBatisConfig.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\CustomerDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportDTO.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\CustomerMapper.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\CustomerController.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\CustomerServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\annotations\AuthRequired.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\AuthInterceptor.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\UserServiceImpl.java
|
|
||||||
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\task\ReportExpireTask.java
|
|
||||||
|
|
@ -10,27 +10,29 @@
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.0",
|
"@element-plus/icons-vue": "^2.3.0",
|
||||||
"vue-router": "^4.2.0",
|
"axios": "^1.6.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"dayjs": "^1.11.0",
|
||||||
|
"element-plus": "^2.5.0",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0",
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
"axios": "^1.6.0",
|
"vue": "^3.4.0",
|
||||||
"element-plus": "^2.5.0",
|
"vue-router": "^4.2.0"
|
||||||
"@element-plus/icons-vue": "^2.3.0",
|
|
||||||
"dayjs": "^1.11.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"vite": "^5.0.0",
|
|
||||||
"vue-tsc": "^1.8.0",
|
|
||||||
"typescript": "^5.3.0",
|
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||||
|
"@typescript-eslint/parser": "^6.15.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-plugin-vue": "^9.19.0",
|
"eslint-plugin-vue": "^9.19.0",
|
||||||
"@typescript-eslint/parser": "^6.15.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"sass": "^1.69.0"
|
"sass": "^1.69.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vue-tsc": "^1.8.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0",
|
"node": ">=16.0.0",
|
||||||
|
|
|
||||||
2664
frontend/pnpm-lock.yaml
Normal file
2664
frontend/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -15,3 +15,13 @@ export const getUserInfo = () => {
|
||||||
export const logout = () => {
|
export const logout = () => {
|
||||||
return http.post('/auth/logout')
|
return http.post('/auth/logout')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
export const changePassword = (data: { oldPassword: string; newPassword: string }) => {
|
||||||
|
return http.post('/auth/change-password', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码(管理员功能)
|
||||||
|
export const resetPassword = (data: { userId: number; newPassword: string }) => {
|
||||||
|
return http.post('/auth/reset-password', data)
|
||||||
|
}
|
||||||
|
|
|
||||||
18
frontend/src/api/dashboard.ts
Normal file
18
frontend/src/api/dashboard.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export interface DashboardStatistics {
|
||||||
|
customerCount: number
|
||||||
|
reportCount: number
|
||||||
|
pendingCount: number
|
||||||
|
dealerCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取首页统计数据
|
||||||
|
*/
|
||||||
|
export function getStatistics() {
|
||||||
|
return request<DashboardStatistics>({
|
||||||
|
url: '/dashboard/statistics',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
45
frontend/src/api/school.ts
Normal file
45
frontend/src/api/school.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { http } from '@/utils/request'
|
||||||
|
|
||||||
|
// 学校接口类型定义
|
||||||
|
export interface School {
|
||||||
|
id?: number
|
||||||
|
schoolCode: string
|
||||||
|
schoolName: string
|
||||||
|
location?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据名称搜索学校(用于自动完成)
|
||||||
|
export const searchSchoolByName = (keyword: string) => {
|
||||||
|
return http.get<School[]>('/school/search', { params: { keyword } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询所有学校
|
||||||
|
export const getAllSchools = () => {
|
||||||
|
return http.get<School[]>('/school/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建学校
|
||||||
|
export const createSchool = (data: Omit<School, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
|
return http.post<number>('/school', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量导入学校
|
||||||
|
export const batchImportSchools = (data: School[]) => {
|
||||||
|
return http.post<number>('/school/batch', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从Excel文件导入学校数据
|
||||||
|
export const importSchoolsFromExcel = (file: File) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return http.post<number>('/school/import', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从项目docs目录导入学校数据
|
||||||
|
export const importSchoolsFromDocs = () => {
|
||||||
|
return http.post<number>('/school/import-from-docs')
|
||||||
|
}
|
||||||
44
frontend/src/api/system.ts
Normal file
44
frontend/src/api/system.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import type { SystemConfig } from '@/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有系统配置(管理员)
|
||||||
|
*/
|
||||||
|
export function getAllConfigs() {
|
||||||
|
return request<SystemConfig[]>({
|
||||||
|
url: '/system/config',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定配置值
|
||||||
|
*/
|
||||||
|
export function getConfigValue(configKey: string) {
|
||||||
|
return request<string>({
|
||||||
|
url: `/system/config/value/${configKey}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新配置
|
||||||
|
*/
|
||||||
|
export function updateConfig(configKey: string, configValue: string) {
|
||||||
|
return request({
|
||||||
|
url: '/system/config',
|
||||||
|
method: 'put',
|
||||||
|
data: { configKey, configValue }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新配置
|
||||||
|
*/
|
||||||
|
export function batchUpdateConfigs(configs: Record<string, string>) {
|
||||||
|
return request({
|
||||||
|
url: '/system/config/batch',
|
||||||
|
method: 'put',
|
||||||
|
data: configs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,12 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'Dealer',
|
name: 'Dealer',
|
||||||
component: () => import('@/views/Dealer.vue'),
|
component: () => import('@/views/Dealer.vue'),
|
||||||
meta: { title: '经销商管理', requiresAdmin: true }
|
meta: { title: '经销商管理', requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'system-config',
|
||||||
|
name: 'SystemConfig',
|
||||||
|
component: () => import('@/views/SystemConfig.vue'),
|
||||||
|
meta: { title: '系统配置', requiresAdmin: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ export interface Customer {
|
||||||
industry?: string
|
industry?: string
|
||||||
status: number
|
status: number
|
||||||
statusDesc: string
|
statusDesc: string
|
||||||
|
currentDealerId?: number
|
||||||
|
currentDealerName?: string
|
||||||
|
protectEndDate?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +50,7 @@ export interface Dealer {
|
||||||
contactPhone: string
|
contactPhone: string
|
||||||
email?: string
|
email?: string
|
||||||
status: number
|
status: number
|
||||||
|
userId: number // 关联的用户ID,用于重置密码
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
@ -99,3 +103,16 @@ export interface DictItem {
|
||||||
label: string
|
label: string
|
||||||
value: string | number
|
value: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统配置相关类型
|
||||||
|
export interface SystemConfig {
|
||||||
|
id: number
|
||||||
|
configKey: string
|
||||||
|
configValue: string
|
||||||
|
configLabel: string
|
||||||
|
configType: 'string' | 'integer' | 'boolean'
|
||||||
|
description?: string
|
||||||
|
isEditable: number
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
|
||||||
61
frontend/src/utils/crypto.ts
Normal file
61
frontend/src/utils/crypto.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* 密码加密工具
|
||||||
|
* 使用 Web Crypto API 进行 SHA-256 哈希
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串转换为 ArrayBuffer
|
||||||
|
*/
|
||||||
|
function stringToArrayBuffer(str: string): ArrayBuffer {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
return encoder.encode(str).buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 ArrayBuffer 转换为十六进制字符串
|
||||||
|
*/
|
||||||
|
function arrayBufferToHex(buffer: ArrayBuffer): string {
|
||||||
|
const bytes = new Uint8Array(buffer)
|
||||||
|
return Array.from(bytes)
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 SHA-256 算法哈希密码
|
||||||
|
* @param password 明文密码
|
||||||
|
* @returns 哈希后的十六进制字符串
|
||||||
|
*/
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
if (!password) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 Web Crypto API 进行 SHA-256 哈希
|
||||||
|
const data = stringToArrayBuffer(password)
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
||||||
|
return arrayBufferToHex(hashBuffer)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('密码哈希失败:', error)
|
||||||
|
throw new Error('密码加密失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的加盐哈希(可选,用于增强安全性)
|
||||||
|
* @param password 明文密码
|
||||||
|
* @param salt 盐值
|
||||||
|
* @returns 哈希后的十六进制字符串
|
||||||
|
*/
|
||||||
|
export async function hashPasswordWithSalt(password: string, salt: string): Promise<string> {
|
||||||
|
if (!password) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saltedPassword = password + salt
|
||||||
|
const data = stringToArrayBuffer(saltedPassword)
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
||||||
|
return arrayBufferToHex(hashBuffer)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('密码哈希失败:', error)
|
||||||
|
throw new Error('密码加密失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
70
frontend/src/utils/password.ts
Normal file
70
frontend/src/utils/password.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import * as bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码加密工具
|
||||||
|
* 注意:BCrypt 每次加密相同密码会产生不同的哈希值(因为有随机盐)
|
||||||
|
* 这是正常的行为,后端使用 BCrypt.matches() 方法来验证密码
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成密码的 BCrypt 哈希值(用于注册或修改密码)
|
||||||
|
* @param password 原始密码
|
||||||
|
* @returns BCrypt 哈希值
|
||||||
|
*/
|
||||||
|
export function hashPassword(password: string): string {
|
||||||
|
const salt = bcrypt.genSaltSync(10)
|
||||||
|
return bcrypt.hashSync(password, salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证明文密码是否匹配 BCrypt 哈希值(用于前端预验证)
|
||||||
|
* 注意:这只是辅助验证,真正的验证应该在后端进行
|
||||||
|
* @param password 原始密码
|
||||||
|
* @param hash BCrypt 哈希值
|
||||||
|
* @returns 是否匹配
|
||||||
|
*/
|
||||||
|
export function verifyPassword(password: string, hash: string): boolean {
|
||||||
|
return bcrypt.compareSync(password, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端登录时的密码处理
|
||||||
|
* 说明:
|
||||||
|
* 1. 方案一(推荐):前端发送明文密码,后端使用 BCrypt 验证
|
||||||
|
* 2. 方案二:前端对密码进行简单哈希(如 MD5/SHA256)传输,后端再验证
|
||||||
|
*
|
||||||
|
* 注意:不建议在前端使用 BCrypt 加密后传输,因为:
|
||||||
|
* - BCrypt 每次加密相同密码会生成不同哈希值
|
||||||
|
* - 后端收到哈希值后无法使用 BCrypt.matches() 验证
|
||||||
|
* - 这需要修改后端验证逻辑,降低安全性
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方案一:直接返回明文密码(推荐,通过 HTTPS 传输)
|
||||||
|
*/
|
||||||
|
export function preparePasswordForLogin(password: string): string {
|
||||||
|
return password // 明文传输,后端 BCrypt 验证
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方案二:使用固定盐值的哈希(可选,用于额外的一层保护)
|
||||||
|
* 注意:这不是 BCrypt,而是简单的 SHA-256
|
||||||
|
*/
|
||||||
|
export async function hashPasswordForLogin(password: string): Promise<string> {
|
||||||
|
// 使用 Web Crypto API 进行 SHA-256 哈希
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const data = encoder.encode(password)
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
return hashHex
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐的登录密码处理方式
|
||||||
|
* 直接返回明文密码,让后端使用 BCrypt 验证
|
||||||
|
* 这是最安全和标准的方式
|
||||||
|
*/
|
||||||
|
export function getLoginPassword(password: string): string {
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,15 @@
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>客户管理</span>
|
<span>客户管理</span>
|
||||||
<el-button type="primary" @click="handleAdd">
|
<div style="display: flex; gap: 10px; align-items: center">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-tag type="info" size="large">
|
||||||
新增客户
|
当前保护期配置:{{ protectDays }} 天
|
||||||
</el-button>
|
</el-tag>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新增客户
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -60,6 +65,16 @@
|
||||||
<el-tag v-else type="info">已失效</el-tag>
|
<el-tag v-else type="info">已失效</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column prop="currentDealerName" label="当前经销商" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.status === 1 ? (row.currentDealerName || '-') : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="protectEndDate" label="保护期截止" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.status === 1 && row.protectEndDate ? formatDate(row.protectEndDate) : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
|
@ -76,6 +91,8 @@
|
||||||
:total="total"
|
:total="total"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
@size-change="fetchData"
|
@size-change="fetchData"
|
||||||
@current-change="fetchData"
|
@current-change="fetchData"
|
||||||
style="margin-top: 20px; justify-content: flex-end"
|
style="margin-top: 20px; justify-content: flex-end"
|
||||||
|
|
@ -91,7 +108,21 @@
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="客户名称" prop="name">
|
<el-form-item label="客户名称" prop="name">
|
||||||
<el-input v-model="formData.name" placeholder="请输入客户名称" />
|
<el-autocomplete
|
||||||
|
v-model="formData.name"
|
||||||
|
:fetch-suggestions="searchSchool"
|
||||||
|
placeholder="请输入客户名称"
|
||||||
|
:trigger-on-focus="false"
|
||||||
|
style="width: 100%"
|
||||||
|
@select="handleSchoolSelect"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="school-option">
|
||||||
|
<div class="school-name">{{ item.schoolName }}</div>
|
||||||
|
<div class="school-location">{{ item.location }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-autocomplete>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="联系电话" prop="phone">
|
<el-form-item label="联系电话" prop="phone">
|
||||||
<el-input v-model="formData.phone" placeholder="请输入联系电话" />
|
<el-input v-model="formData.phone" placeholder="请输入联系电话" />
|
||||||
|
|
@ -123,7 +154,12 @@
|
||||||
import { ref, reactive, onMounted, computed } from 'vue'
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { getCustomerPage, createCustomer, updateCustomer, deleteCustomer } from '@/api/customer'
|
import { getCustomerPage, createCustomer, updateCustomer, deleteCustomer } from '@/api/customer'
|
||||||
|
import { getConfigValue } from '@/api/system'
|
||||||
|
import { searchSchoolByName } from '@/api/school'
|
||||||
import type { Customer, CustomerForm } from '@/types'
|
import type { Customer, CustomerForm } from '@/types'
|
||||||
|
import type { School } from '@/api/school'
|
||||||
|
|
||||||
|
const protectDays = ref<number>(90)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<Customer[]>([])
|
const tableData = ref<Customer[]>([])
|
||||||
|
|
@ -131,6 +167,8 @@ const total = ref(0)
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const dialogTitle = computed(() => (formData.id ? '编辑客户' : '新增客户'))
|
const dialogTitle = computed(() => (formData.id ? '编辑客户' : '新增客户'))
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
const schoolSearchLoading = ref(false)
|
||||||
|
const schoolOptions = ref<School[]>([])
|
||||||
|
|
||||||
const queryForm = reactive({
|
const queryForm = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
|
|
@ -151,6 +189,44 @@ const rules: FormRules = {
|
||||||
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
|
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化日期,去掉时分秒
|
||||||
|
const formatDate = (dateStr: string | null | undefined) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
return dateStr.split(' ')[0] // 只保留日期部分,去掉时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索学校(用于自动完成)
|
||||||
|
const searchSchool = async (queryString: string, cb: any) => {
|
||||||
|
if (!queryString || queryString.trim().length < 1) {
|
||||||
|
cb([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
schoolSearchLoading.value = true
|
||||||
|
try {
|
||||||
|
const schools = await searchSchoolByName(queryString)
|
||||||
|
// 转换为 el-autocomplete 需要的格式
|
||||||
|
const suggestions = schools.map(school => ({
|
||||||
|
...school,
|
||||||
|
value: school.schoolName
|
||||||
|
}))
|
||||||
|
cb(suggestions)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索学校失败', error)
|
||||||
|
cb([])
|
||||||
|
} finally {
|
||||||
|
schoolSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择学校后的回调
|
||||||
|
const handleSchoolSelect = (item: School) => {
|
||||||
|
// 用户可以选择性地填充地址字段
|
||||||
|
if (item.location && !formData.address) {
|
||||||
|
formData.address = item.location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取行业标签
|
// 获取行业标签
|
||||||
const getIndustryLabel = (industry: string) => {
|
const getIndustryLabel = (industry: string) => {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
|
|
@ -208,7 +284,15 @@ const handleEdit = (row: Customer) => {
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
const handleDelete = async (row: Customer) => {
|
const handleDelete = async (row: Customer) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要删除该客户吗?', '提示', { type: 'warning' })
|
await ElMessageBox.confirm(
|
||||||
|
'确定要删除该客户吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
await deleteCustomer(row.id)
|
await deleteCustomer(row.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
@ -251,7 +335,18 @@ const handleDialogClose = () => {
|
||||||
delete formData.id
|
delete formData.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取保护期配置
|
||||||
|
const fetchProtectDays = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getConfigValue('report.protect.days')
|
||||||
|
protectDays.value = parseInt(res) || 90
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
fetchProtectDays()
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -270,4 +365,20 @@ onMounted(() => {
|
||||||
.query-form {
|
.query-form {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.school-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-location {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import * as statisticsApi from '@/api/customer'
|
import { getStatistics } from '@/api/dashboard'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
|
@ -77,14 +78,19 @@ const stats = ref({
|
||||||
dealerCount: 0
|
dealerCount: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载统计数据(模拟数据)
|
// 加载统计数据
|
||||||
onMounted(() => {
|
const fetchStatistics = async () => {
|
||||||
stats.value = {
|
try {
|
||||||
customerCount: 5,
|
const res = await getStatistics()
|
||||||
reportCount: 0,
|
stats.value = res
|
||||||
pendingCount: 0,
|
} catch (error) {
|
||||||
dealerCount: 3
|
console.error('加载统计数据失败', error)
|
||||||
|
ElMessage.error('加载统计数据失败')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStatistics()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
<el-form-item label="经销商名称">
|
<el-form-item label="经销商名称">
|
||||||
<el-input v-model="queryForm.name" placeholder="请输入经销商名称" clearable />
|
<el-input v-model="queryForm.name" placeholder="请输入经销商名称" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="经销商编码">
|
<el-form-item label="经销商账号">
|
||||||
<el-input v-model="queryForm.code" placeholder="请输入经销商编码" clearable />
|
<el-input v-model="queryForm.code" placeholder="请输入经销商账号" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-select v-model="queryForm.status" placeholder="请选择状态" clearable>
|
<el-select v-model="queryForm.status" placeholder="请选择状态" clearable>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<el-table :data="tableData" border stripe v-loading="loading">
|
<el-table :data="tableData" border stripe v-loading="loading">
|
||||||
<el-table-column prop="name" label="经销商名称" />
|
<el-table-column prop="name" label="经销商名称" />
|
||||||
<el-table-column prop="code" label="经销商编码" />
|
<el-table-column prop="code" label="经销商账号" />
|
||||||
<el-table-column prop="contactPerson" label="联系人" />
|
<el-table-column prop="contactPerson" label="联系人" />
|
||||||
<el-table-column prop="contactPhone" label="联系电话" />
|
<el-table-column prop="contactPhone" label="联系电话" />
|
||||||
<el-table-column prop="email" label="邮箱" />
|
<el-table-column prop="email" label="邮箱" />
|
||||||
|
|
@ -49,9 +49,10 @@
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="warning" @click="handleResetPassword(row)">重置密码</el-button>
|
||||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
@ -69,8 +70,8 @@
|
||||||
<el-form-item label="经销商名称" prop="name">
|
<el-form-item label="经销商名称" prop="name">
|
||||||
<el-input v-model="formData.name" placeholder="请输入经销商名称" />
|
<el-input v-model="formData.name" placeholder="请输入经销商名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="经销商编码" prop="code">
|
<el-form-item label="经销商账号" prop="code">
|
||||||
<el-input v-model="formData.code" placeholder="请输入经销商编码" />
|
<el-input v-model="formData.code" placeholder="请输入经销商账号" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="联系人" prop="contactPerson">
|
<el-form-item label="联系人" prop="contactPerson">
|
||||||
<el-input v-model="formData.contactPerson" placeholder="请输入联系人" />
|
<el-input v-model="formData.contactPerson" placeholder="请输入联系人" />
|
||||||
|
|
@ -87,12 +88,57 @@
|
||||||
<el-radio :label="0">禁用</el-radio>
|
<el-radio :label="0">禁用</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="初始密码" prop="password" v-if="!formData.id">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入初始密码(不填默认为123456)"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 4px">
|
||||||
|
长度6-20位,不填则默认密码为 123456
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 重置密码对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="resetPasswordDialogVisible"
|
||||||
|
title="重置密码"
|
||||||
|
width="500px"
|
||||||
|
@close="handleResetPasswordDialogClose"
|
||||||
|
>
|
||||||
|
<el-form ref="resetPasswordFormRef" :model="resetPasswordForm" :rules="resetPasswordRules" label-width="100px">
|
||||||
|
<el-form-item label="经销商名称">
|
||||||
|
<el-input v-model="resetPasswordForm.dealerName" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="resetPasswordForm.newPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码(6-20位)"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="resetPasswordForm.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="resetPasswordDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleResetPasswordSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -100,13 +146,17 @@
|
||||||
import { ref, reactive, onMounted, computed } from 'vue'
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { getDealerList, createDealer, updateDealer, deleteDealer } from '@/api/dealer'
|
import { getDealerList, createDealer, updateDealer, deleteDealer } from '@/api/dealer'
|
||||||
|
import { resetPassword } from '@/api/auth'
|
||||||
|
import { hashPassword } from '@/utils/crypto'
|
||||||
import type { Dealer } from '@/types'
|
import type { Dealer } from '@/types'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<Dealer[]>([])
|
const tableData = ref<Dealer[]>([])
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
|
const resetPasswordDialogVisible = ref(false)
|
||||||
const dialogTitle = computed(() => (formData.id ? '编辑经销商' : '新增经销商'))
|
const dialogTitle = computed(() => (formData.id ? '编辑经销商' : '新增经销商'))
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
const resetPasswordFormRef = ref<FormInstance>()
|
||||||
|
|
||||||
const queryForm = reactive({
|
const queryForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -114,20 +164,62 @@ const queryForm = reactive({
|
||||||
status: undefined as number | undefined
|
status: undefined as number | undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = reactive<Partial<Dealer> & { id?: number }>({
|
const formData = reactive<Partial<Dealer> & { id?: number; password?: string }>({
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
contactPerson: '',
|
contactPerson: '',
|
||||||
contactPhone: '',
|
contactPhone: '',
|
||||||
email: '',
|
email: '',
|
||||||
status: 1
|
status: 1,
|
||||||
|
password: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
name: [{ required: true, message: '请输入经销商名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入经销商名称', trigger: 'blur' }],
|
||||||
code: [{ required: true, message: '请输入经销商编码', trigger: 'blur' }],
|
code: [{ required: true, message: '请输入经销商账号', trigger: 'blur' }],
|
||||||
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||||
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }]
|
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
||||||
|
password: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
// 密码可选,如果填写则校验长度
|
||||||
|
if (value && (value.length < 6 || value.length > 20)) {
|
||||||
|
callback(new Error('密码长度必须在6-20位之间'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码表单
|
||||||
|
const resetPasswordForm = reactive({
|
||||||
|
userId: 0,
|
||||||
|
dealerName: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetPasswordRules: FormRules = {
|
||||||
|
newPassword: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '新密码长度必须在6-20位之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value !== resetPasswordForm.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询数据
|
// 查询数据
|
||||||
|
|
@ -170,7 +262,15 @@ const handleEdit = (row: Dealer) => {
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
const handleDelete = async (row: Dealer) => {
|
const handleDelete = async (row: Dealer) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要删除该经销商吗?', '提示', { type: 'warning' })
|
await ElMessageBox.confirm(
|
||||||
|
'确定要删除该经销商吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
await deleteDealer(row.id)
|
await deleteDealer(row.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
@ -201,6 +301,46 @@ const handleSubmit = async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置密码按钮
|
||||||
|
const handleResetPassword = (row: Dealer) => {
|
||||||
|
resetPasswordForm.userId = row.userId
|
||||||
|
resetPasswordForm.dealerName = row.name
|
||||||
|
resetPasswordDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交重置密码
|
||||||
|
const handleResetPasswordSubmit = async () => {
|
||||||
|
if (!resetPasswordFormRef.value) return
|
||||||
|
await resetPasswordFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
// 对密码进行 SHA-256 哈希后再传输
|
||||||
|
const hashedPassword = await hashPassword(resetPasswordForm.newPassword)
|
||||||
|
|
||||||
|
await resetPassword({
|
||||||
|
userId: resetPasswordForm.userId,
|
||||||
|
newPassword: hashedPassword
|
||||||
|
})
|
||||||
|
ElMessage.success('密码重置成功')
|
||||||
|
resetPasswordDialogVisible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置密码失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码对话框关闭
|
||||||
|
const handleResetPasswordDialogClose = () => {
|
||||||
|
resetPasswordFormRef.value?.resetFields()
|
||||||
|
Object.assign(resetPasswordForm, {
|
||||||
|
userId: 0,
|
||||||
|
dealerName: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 对话框关闭
|
// 对话框关闭
|
||||||
const handleDialogClose = () => {
|
const handleDialogClose = () => {
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
|
|
@ -210,7 +350,8 @@ const handleDialogClose = () => {
|
||||||
contactPerson: '',
|
contactPerson: '',
|
||||||
contactPhone: '',
|
contactPhone: '',
|
||||||
email: '',
|
email: '',
|
||||||
status: 1
|
status: 1,
|
||||||
|
password: ''
|
||||||
})
|
})
|
||||||
delete formData.id
|
delete formData.id
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@
|
||||||
<el-icon><Shop /></el-icon>
|
<el-icon><Shop /></el-icon>
|
||||||
<span>经销商管理</span>
|
<span>经销商管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item v-if="userStore.isAdmin" index="/system-config">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>系统配置</span>
|
||||||
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container>
|
<el-container>
|
||||||
|
|
@ -47,6 +51,10 @@
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="showChangePasswordDialog = true">
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
修改密码
|
||||||
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click="handleLogout">
|
<el-dropdown-item @click="handleLogout">
|
||||||
<el-icon><SwitchButton /></el-icon>
|
<el-icon><SwitchButton /></el-icon>
|
||||||
退出登录
|
退出登录
|
||||||
|
|
@ -61,14 +69,55 @@
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
|
||||||
|
<!-- 修改密码对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showChangePasswordDialog"
|
||||||
|
title="修改密码"
|
||||||
|
width="500px"
|
||||||
|
@close="handleDialogClose"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="passwordForm" :rules="passwordRules" label-width="100px">
|
||||||
|
<el-form-item label="原密码" prop="oldPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.oldPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入原密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.newPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码(6-20位)"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showChangePasswordDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleChangePassword">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { changePassword } from '@/api/auth'
|
||||||
|
import { hashPassword } from '@/utils/crypto'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -77,11 +126,81 @@ const userStore = useUserStore()
|
||||||
const activeMenu = computed(() => route.path)
|
const activeMenu = computed(() => route.path)
|
||||||
const currentPageTitle = computed(() => route.meta.title as string || '首页')
|
const currentPageTitle = computed(() => route.meta.title as string || '首页')
|
||||||
|
|
||||||
|
// 修改密码对话框
|
||||||
|
const showChangePasswordDialog = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const passwordForm = reactive({
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwordRules: FormRules = {
|
||||||
|
oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
|
||||||
|
newPassword: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '新密码长度必须在6-20位之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value !== passwordForm.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangePassword = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
// 对密码进行 SHA-256 哈希后再传输
|
||||||
|
const hashedOldPassword = await hashPassword(passwordForm.oldPassword)
|
||||||
|
const hashedNewPassword = await hashPassword(passwordForm.newPassword)
|
||||||
|
|
||||||
|
await changePassword({
|
||||||
|
oldPassword: hashedOldPassword,
|
||||||
|
newPassword: hashedNewPassword
|
||||||
|
})
|
||||||
|
ElMessage.success('密码修改成功,请重新登录')
|
||||||
|
showChangePasswordDialog.value = false
|
||||||
|
// 退出登录
|
||||||
|
userStore.logout()
|
||||||
|
router.push('/login')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDialogClose = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
Object.assign(passwordForm, {
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
await ElMessageBox.confirm(
|
||||||
type: 'warning'
|
'确定要退出登录吗?',
|
||||||
})
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
userStore.logout()
|
userStore.logout()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import { ref, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { hashPassword } from '@/utils/crypto'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
@ -59,7 +60,12 @@ const handleLogin = async () => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await userStore.login(loginForm)
|
// 对密码进行 SHA-256 哈希后再传输
|
||||||
|
const hashedPassword = await hashPassword(loginForm.password)
|
||||||
|
await userStore.login({
|
||||||
|
username: loginForm.username,
|
||||||
|
password: hashedPassword
|
||||||
|
})
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,12 @@
|
||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>报备管理</span>
|
<div>
|
||||||
|
<span>报备管理</span>
|
||||||
|
<el-tag type="info" size="small" style="margin-left: 10px">
|
||||||
|
保护期:{{ protectDays }} 天
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
<el-button type="primary" @click="handleAdd" v-if="!userStore.isAdmin">
|
<el-button type="primary" @click="handleAdd" v-if="!userStore.isAdmin">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
提交报备
|
提交报备
|
||||||
|
|
@ -49,7 +54,7 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="protectEndDate" label="保护期截止" width="180">
|
<el-table-column prop="protectEndDate" label="保护期截止" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.protectEndDate || '-' }}
|
{{ formatDate(row.protectEndDate) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
||||||
|
|
@ -83,6 +88,8 @@
|
||||||
:total="total"
|
:total="total"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
@size-change="fetchData"
|
@size-change="fetchData"
|
||||||
@current-change="fetchData"
|
@current-change="fetchData"
|
||||||
style="margin-top: 20px; justify-content: flex-end"
|
style="margin-top: 20px; justify-content: flex-end"
|
||||||
|
|
@ -157,10 +164,13 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { getReportPage, createReport, auditReport, withdrawReport } from '@/api/report'
|
import { getReportPage, createReport, auditReport, withdrawReport } from '@/api/report'
|
||||||
import { searchCustomerByName } from '@/api/customer'
|
import { searchCustomerByName } from '@/api/customer'
|
||||||
|
import { getConfigValue } from '@/api/system'
|
||||||
import type { Report, ReportForm } from '@/types'
|
import type { Report, ReportForm } from '@/types'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const protectDays = ref<number>(90)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<Report[]>([])
|
const tableData = ref<Report[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|
@ -193,6 +203,12 @@ const rules: FormRules = {
|
||||||
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }]
|
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化日期,去掉时分秒
|
||||||
|
const formatDate = (dateStr: string | null | undefined) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
return dateStr.split(' ')[0] // 只保留日期部分,去掉时间
|
||||||
|
}
|
||||||
|
|
||||||
// 查询数据
|
// 查询数据
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
@ -251,7 +267,7 @@ const handleView = (row: Report) => {
|
||||||
<p><strong>报备说明:</strong>${row.description || '-'}</p>
|
<p><strong>报备说明:</strong>${row.description || '-'}</p>
|
||||||
<p><strong>状态:</strong>${row.statusDesc}</p>
|
<p><strong>状态:</strong>${row.statusDesc}</p>
|
||||||
<p><strong>驳回原因:</strong>${row.rejectReason || '-'}</p>
|
<p><strong>驳回原因:</strong>${row.rejectReason || '-'}</p>
|
||||||
<p><strong>保护期:</strong>${row.protectStartDate || '-'} ~ ${row.protectEndDate || '-'}</p>
|
<p><strong>保护期:</strong>${formatDate(row.protectStartDate)} ~ ${formatDate(row.protectEndDate)}</p>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
'报备详情',
|
'报备详情',
|
||||||
|
|
@ -273,7 +289,15 @@ const handleAudit = (row: Report) => {
|
||||||
// 撤回按钮
|
// 撤回按钮
|
||||||
const handleWithdraw = async (row: Report) => {
|
const handleWithdraw = async (row: Report) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要撤回该报备吗?', '提示', { type: 'warning' })
|
await ElMessageBox.confirm(
|
||||||
|
'确定要撤回该报备吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
await withdrawReport(row.id)
|
await withdrawReport(row.id)
|
||||||
ElMessage.success('撤回成功')
|
ElMessage.success('撤回成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
@ -326,7 +350,18 @@ const handleDialogClose = () => {
|
||||||
customerOptions.value = []
|
customerOptions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取保护期配置
|
||||||
|
const fetchProtectDays = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getConfigValue('report.protect.days')
|
||||||
|
protectDays.value = parseInt(res) || 90
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
fetchProtectDays()
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
146
frontend/src/views/SystemConfig.vue
Normal file
146
frontend/src/views/SystemConfig.vue
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<div class="system-config-page">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>系统配置管理</span>
|
||||||
|
<el-button type="primary" @click="handleSave" :loading="saving">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
title="提示"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
>
|
||||||
|
修改配置后立即生效,请谨慎操作。保护期天数的修改仅影响新生成的报备。
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<el-form :model="formData" label-width="150px">
|
||||||
|
<el-divider content-position="left">报备配置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="保护期天数">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData['report.protect.days']"
|
||||||
|
:min="1"
|
||||||
|
:max="365"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
<span style="margin-left: 10px; color: #909399">天</span>
|
||||||
|
<div style="margin-top: 5px; color: #909399; font-size: 12px">
|
||||||
|
报备审核通过后,客户的保护时长。修改仅影响新生成的报备。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="允许重叠报备">
|
||||||
|
<el-switch
|
||||||
|
v-model="formData['report.allow.overlap']"
|
||||||
|
active-text="是"
|
||||||
|
inactive-text="否"
|
||||||
|
/>
|
||||||
|
<div style="margin-top: 5px; color: #F56C6C; font-size: 12px">
|
||||||
|
⚠️ 警告:生产环境必须设置为"否",否则会导致同一个客户被多个经销商报备!
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">当前配置状态</el-divider>
|
||||||
|
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="当前保护期天数">
|
||||||
|
{{ configs.find(c => c.configKey === 'report.protect.days')?.configValue || 90 }} 天
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="允许重叠报备">
|
||||||
|
<el-tag :type="configs.find(c => c.configKey === 'report.allow.overlap')?.configValue === 'true' ? 'danger' : 'success'">
|
||||||
|
{{ configs.find(c => c.configKey === 'report.allow.overlap')?.configValue === 'true' ? '是(危险)' : '否(安全)' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Check } from '@element-plus/icons-vue'
|
||||||
|
import { getAllConfigs, batchUpdateConfigs } from '@/api/system'
|
||||||
|
import type { SystemConfig } from '@/types'
|
||||||
|
|
||||||
|
const configs = ref<SystemConfig[]>([])
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const formData = reactive<Record<string, any>>({
|
||||||
|
'report.protect.days': 90,
|
||||||
|
'report.allow.overlap': false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
const fetchConfigs = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getAllConfigs()
|
||||||
|
configs.value = res
|
||||||
|
|
||||||
|
// 填充表单
|
||||||
|
res.forEach(config => {
|
||||||
|
if (config.configType === 'integer') {
|
||||||
|
formData[config.configKey] = parseInt(config.configValue)
|
||||||
|
} else if (config.configType === 'boolean') {
|
||||||
|
formData[config.configKey] = config.configValue === 'true'
|
||||||
|
} else {
|
||||||
|
formData[config.configKey] = config.configValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置失败', error)
|
||||||
|
ElMessage.error('加载配置失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
const handleSave = async () => {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const configMap: Record<string, string> = {}
|
||||||
|
Object.keys(formData).forEach(key => {
|
||||||
|
const value = formData[key]
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
configMap[key] = String(value)
|
||||||
|
} else {
|
||||||
|
configMap[key] = String(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await batchUpdateConfigs(configMap)
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
fetchConfigs()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败', error)
|
||||||
|
ElMessage.error('保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchConfigs()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.system-config-page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
tmpclaude-62fc-cwd
Normal file
1
tmpclaude-62fc-cwd
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/e/boyun-workspace/by-crm
|
||||||
Loading…
Reference in New Issue
Block a user