测试暂未发现问题
This commit is contained in:
@@ -1,41 +1,26 @@
|
|||||||
import axios from 'axios'
|
import request from './request'
|
||||||
import { mockContractAPI } from './mock/contract'
|
|
||||||
|
|
||||||
// 开关:true 用 mock,false 用真实接口
|
|
||||||
const USE_MOCK = true
|
|
||||||
|
|
||||||
// axios 实例
|
|
||||||
const request = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取合同列表
|
// 获取合同列表
|
||||||
export const getContractList = (params) => {
|
export const getContractList = (params) => {
|
||||||
if (USE_MOCK) return mockContractAPI.getList(params)
|
return request.get('/contracts', { params })
|
||||||
return request.get('/contracts', { params }).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取合同详情
|
// 获取合同详情
|
||||||
export const getContractDetail = (id) => {
|
export const getContractDetail = (id) => {
|
||||||
if (USE_MOCK) return mockContractAPI.getDetail(id)
|
return request.get(`/contracts/${id}`)
|
||||||
return request.get(`/contracts/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增合同
|
// 新增合同
|
||||||
export const createContract = (data) => {
|
export const createContract = (data) => {
|
||||||
if (USE_MOCK) return mockContractAPI.create(data)
|
return request.post('/contracts', data)
|
||||||
return request.post('/contracts', data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新合同
|
// 更新合同
|
||||||
export const updateContract = (id, data) => {
|
export const updateContract = (id, data) => {
|
||||||
if (USE_MOCK) return mockContractAPI.update(id, data)
|
return request.put(`/contracts/${id}`, data)
|
||||||
return request.put(`/contracts/${id}`, data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除合同
|
// 删除合同
|
||||||
export const deleteContractAPI = (id) => {
|
export const deleteContractAPI = (id) => {
|
||||||
if (USE_MOCK) return mockContractAPI.delete(id)
|
return request.delete(`/contracts/${id}`)
|
||||||
return request.delete(`/contracts/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,26 @@
|
|||||||
import axios from 'axios'
|
import request from './request'
|
||||||
import { mockCustomerAPI } from './mock/customer'
|
|
||||||
|
|
||||||
// 开关:true 用 mock,false 用真实接口
|
|
||||||
const USE_MOCK = true
|
|
||||||
|
|
||||||
// axios 实例
|
|
||||||
const request = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取客户列表
|
// 获取客户列表
|
||||||
export const getCustomerList = (params) => {
|
export const getCustomerList = (params, config) => {
|
||||||
if (USE_MOCK) return mockCustomerAPI.getList(params)
|
return request.get('/customers', { params, ...config })
|
||||||
return request.get('/customers', { params }).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取客户详情
|
// 获取客户详情
|
||||||
export const getCustomerDetail = (id) => {
|
export const getCustomerDetail = (id) => {
|
||||||
if (USE_MOCK) return mockCustomerAPI.getDetail(id)
|
return request.get(`/customers/${id}`)
|
||||||
return request.get(`/customers/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增客户
|
// 新增客户
|
||||||
export const createCustomer = (data) => {
|
export const createCustomer = (data) => {
|
||||||
if (USE_MOCK) return mockCustomerAPI.create(data)
|
return request.post('/customers', data)
|
||||||
return request.post('/customers', data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新客户
|
// 更新客户
|
||||||
export const updateCustomer = (id, data) => {
|
export const updateCustomer = (id, data) => {
|
||||||
if (USE_MOCK) return mockCustomerAPI.update(id, data)
|
return request.put(`/customers/${id}`, data)
|
||||||
return request.put(`/customers/${id}`, data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除客户
|
// 删除客户
|
||||||
export const deleteCustomerAPI = (id) => {
|
export const deleteCustomerAPI = (id) => {
|
||||||
if (USE_MOCK) return mockCustomerAPI.delete(id)
|
return request.delete(`/customers/${id}`)
|
||||||
return request.delete(`/customers/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,31 @@
|
|||||||
import axios from 'axios'
|
import request from './request'
|
||||||
import { mockEmployeeAPI } from './mock/employee'
|
|
||||||
|
|
||||||
// 开关:true 用 mock,false 用真实接口
|
|
||||||
const USE_MOCK = true
|
|
||||||
|
|
||||||
// axios 实例
|
|
||||||
const request = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取员工列表
|
// 获取员工列表
|
||||||
export const getEmployeeList = (params) => {
|
export const getEmployeeList = (params, config) => {
|
||||||
if (USE_MOCK) return mockEmployeeAPI.getList(params)
|
return request.get('/employees', { params, ...config })
|
||||||
return request.get('/employees', { params }).then(r => r.data)
|
}
|
||||||
|
|
||||||
|
// 获取简易员工列表(无数据范围限制,支持按部门过滤)
|
||||||
|
export const getSimpleEmployeeList = (params, config) => {
|
||||||
|
return request.get('/employees/simple', { params, ...config })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取员工详情
|
// 获取员工详情
|
||||||
export const getEmployeeDetail = (id) => {
|
export const getEmployeeDetail = (id) => {
|
||||||
if (USE_MOCK) return mockEmployeeAPI.getDetail(id)
|
return request.get(`/employees/${id}`)
|
||||||
return request.get(`/employees/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增员工
|
// 新增员工
|
||||||
export const createEmployee = (data) => {
|
export const createEmployee = (data) => {
|
||||||
if (USE_MOCK) return mockEmployeeAPI.create(data)
|
return request.post('/employees', data)
|
||||||
return request.post('/employees', data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新员工
|
// 更新员工
|
||||||
export const updateEmployee = (id, data) => {
|
export const updateEmployee = (id, data) => {
|
||||||
if (USE_MOCK) return mockEmployeeAPI.update(id, data)
|
return request.put(`/employees/${id}`, data)
|
||||||
return request.put(`/employees/${id}`, data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除员工
|
// 删除员工
|
||||||
export const deleteEmployeeAPI = (id) => {
|
export const deleteEmployeeAPI = (id) => {
|
||||||
if (USE_MOCK) return mockEmployeeAPI.delete(id)
|
return request.delete(`/employees/${id}`)
|
||||||
return request.delete(`/employees/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
const mockContracts = [
|
const mockContracts = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
contract_no: 'HT-2025-001',
|
|
||||||
contract_name: '蜜雪冰城加盟合同',
|
contract_name: '蜜雪冰城加盟合同',
|
||||||
type: '加盟合同',
|
type: '加盟合同',
|
||||||
party_a: '蜜雪冰城股份有限公司',
|
party_a: '蜜雪冰城股份有限公司',
|
||||||
@@ -17,7 +16,6 @@ const mockContracts = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
contract_no: 'HT-2025-002',
|
|
||||||
contract_name: '原材料供货合同',
|
contract_name: '原材料供货合同',
|
||||||
type: '供货合同',
|
type: '供货合同',
|
||||||
party_a: '蜜雪冰城股份有限公司',
|
party_a: '蜜雪冰城股份有限公司',
|
||||||
@@ -32,7 +30,6 @@ const mockContracts = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
contract_no: 'HT-2024-010',
|
|
||||||
contract_name: '门店租赁合同',
|
contract_name: '门店租赁合同',
|
||||||
type: '租赁合同',
|
type: '租赁合同',
|
||||||
party_a: '蜜雪冰城股份有限公司',
|
party_a: '蜜雪冰城股份有限公司',
|
||||||
@@ -47,7 +44,6 @@ const mockContracts = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
contract_no: 'HT-2025-003',
|
|
||||||
contract_name: '设备维护服务合同',
|
contract_name: '设备维护服务合同',
|
||||||
type: '服务合同',
|
type: '服务合同',
|
||||||
party_a: '蜜雪冰城股份有限公司',
|
party_a: '蜜雪冰城股份有限公司',
|
||||||
@@ -76,7 +72,7 @@ export const mockContractAPI = {
|
|||||||
if (params.keyword) {
|
if (params.keyword) {
|
||||||
const keyword = params.keyword.toLowerCase()
|
const keyword = params.keyword.toLowerCase()
|
||||||
result = result.filter(item =>
|
result = result.filter(item =>
|
||||||
item.contract_no.toLowerCase().includes(keyword) ||
|
String(item.id).includes(keyword) ||
|
||||||
item.contract_name.toLowerCase().includes(keyword) ||
|
item.contract_name.toLowerCase().includes(keyword) ||
|
||||||
item.party_b.toLowerCase().includes(keyword)
|
item.party_b.toLowerCase().includes(keyword)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const mockCustomers = [
|
|||||||
district: '南山区',
|
district: '南山区',
|
||||||
address: '科技园路1号',
|
address: '科技园路1号',
|
||||||
email: 'zhangsan@example.com',
|
email: 'zhangsan@example.com',
|
||||||
customer_type: 'VIP',
|
|
||||||
create_time: '2025-01-15 10:30:00'
|
create_time: '2025-01-15 10:30:00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -21,7 +20,6 @@ const mockCustomers = [
|
|||||||
district: '西湖区',
|
district: '西湖区',
|
||||||
address: '文三路100号',
|
address: '文三路100号',
|
||||||
email: 'lisi@example.com',
|
email: 'lisi@example.com',
|
||||||
customer_type: 'Normal',
|
|
||||||
create_time: '2025-02-20 14:20:00'
|
create_time: '2025-02-20 14:20:00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -33,7 +31,6 @@ const mockCustomers = [
|
|||||||
district: '武侯区',
|
district: '武侯区',
|
||||||
address: '天府大道200号',
|
address: '天府大道200号',
|
||||||
email: 'wangwu@example.com',
|
email: 'wangwu@example.com',
|
||||||
customer_type: 'VIP',
|
|
||||||
create_time: '2025-03-10 09:15:00'
|
create_time: '2025-03-10 09:15:00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,7 +42,6 @@ const mockCustomers = [
|
|||||||
district: '岳麓区',
|
district: '岳麓区',
|
||||||
address: '麓山南路100号',
|
address: '麓山南路100号',
|
||||||
email: 'zhaoliu@example.com',
|
email: 'zhaoliu@example.com',
|
||||||
customer_type: 'Normal',
|
|
||||||
create_time: '2025-04-05 16:45:00'
|
create_time: '2025-04-05 16:45:00'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -88,11 +84,6 @@ export const mockCustomerAPI = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 客户类型筛选
|
|
||||||
if (params.customer_type) {
|
|
||||||
result = result.filter(item => item.customer_type === params.customer_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 地区筛选(省/市/区)
|
// 地区筛选(省/市/区)
|
||||||
if (params.province) {
|
if (params.province) {
|
||||||
result = result.filter(item => item.province === params.province)
|
result = result.filter(item => item.province === params.province)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// 模拟员工数据
|
// 模拟员工数据(单一数据源,同时包含用户字段)
|
||||||
const mockEmployees = [
|
export const mockEmployees = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: '张伟',
|
name: '张伟',
|
||||||
@@ -14,6 +14,11 @@ const mockEmployees = [
|
|||||||
email: 'zhangwei@mxbc.com',
|
email: 'zhangwei@mxbc.com',
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: '负责门店日常运营',
|
remark: '负责门店日常运营',
|
||||||
|
// 用户字段
|
||||||
|
username: 'zhangwei',
|
||||||
|
password: '123456',
|
||||||
|
user_type: 'admin',
|
||||||
|
user_status: '1',
|
||||||
created_at: '2023-03-15 09:00:00',
|
created_at: '2023-03-15 09:00:00',
|
||||||
updated_at: '2023-03-15 09:00:00'
|
updated_at: '2023-03-15 09:00:00'
|
||||||
},
|
},
|
||||||
@@ -31,6 +36,11 @@ const mockEmployees = [
|
|||||||
email: 'lina@mxbc.com',
|
email: 'lina@mxbc.com',
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: '',
|
remark: '',
|
||||||
|
// 用户字段
|
||||||
|
username: 'lina',
|
||||||
|
password: '123456',
|
||||||
|
user_type: 'user',
|
||||||
|
user_status: '1',
|
||||||
created_at: '2023-06-01 09:00:00',
|
created_at: '2023-06-01 09:00:00',
|
||||||
updated_at: '2023-06-01 09:00:00'
|
updated_at: '2023-06-01 09:00:00'
|
||||||
},
|
},
|
||||||
@@ -48,6 +58,11 @@ const mockEmployees = [
|
|||||||
email: 'wangqiang@mxbc.com',
|
email: 'wangqiang@mxbc.com',
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: '负责系统架构设计',
|
remark: '负责系统架构设计',
|
||||||
|
// 用户字段(无用户账号)
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
user_type: null,
|
||||||
|
user_status: null,
|
||||||
created_at: '2022-11-10 09:00:00',
|
created_at: '2022-11-10 09:00:00',
|
||||||
updated_at: '2022-11-10 09:00:00'
|
updated_at: '2022-11-10 09:00:00'
|
||||||
},
|
},
|
||||||
@@ -65,6 +80,11 @@ const mockEmployees = [
|
|||||||
email: 'liuyang@mxbc.com',
|
email: 'liuyang@mxbc.com',
|
||||||
status: 0,
|
status: 0,
|
||||||
remark: '已离职',
|
remark: '已离职',
|
||||||
|
// 用户字段(无用户账号)
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
user_type: null,
|
||||||
|
user_status: null,
|
||||||
created_at: '2024-01-20 09:00:00',
|
created_at: '2024-01-20 09:00:00',
|
||||||
updated_at: '2025-03-01 17:00:00'
|
updated_at: '2025-03-01 17:00:00'
|
||||||
},
|
},
|
||||||
@@ -82,6 +102,11 @@ const mockEmployees = [
|
|||||||
email: 'zhaomin@mxbc.com',
|
email: 'zhaomin@mxbc.com',
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: '',
|
remark: '',
|
||||||
|
// 用户字段
|
||||||
|
username: 'zhaomin',
|
||||||
|
password: '123456',
|
||||||
|
user_type: 'user',
|
||||||
|
user_status: '0',
|
||||||
created_at: '2023-09-05 09:00:00',
|
created_at: '2023-09-05 09:00:00',
|
||||||
updated_at: '2023-09-05 09:00:00'
|
updated_at: '2023-09-05 09:00:00'
|
||||||
},
|
},
|
||||||
@@ -99,6 +124,11 @@ const mockEmployees = [
|
|||||||
email: 'chengang@mxbc.com',
|
email: 'chengang@mxbc.com',
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: '负责原料入库出库',
|
remark: '负责原料入库出库',
|
||||||
|
// 用户字段(无用户账号)
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
user_type: null,
|
||||||
|
user_status: null,
|
||||||
created_at: '2022-04-12 09:00:00',
|
created_at: '2022-04-12 09:00:00',
|
||||||
updated_at: '2022-04-12 09:00:00'
|
updated_at: '2022-04-12 09:00:00'
|
||||||
}
|
}
|
||||||
@@ -169,6 +199,11 @@ export const mockEmployeeAPI = {
|
|||||||
id: maxId + 1,
|
id: maxId + 1,
|
||||||
status: 1,
|
status: 1,
|
||||||
...data,
|
...data,
|
||||||
|
// 用户字段初始为空
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
user_type: null,
|
||||||
|
user_status: null,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now
|
updated_at: now
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,26 @@
|
|||||||
// 模拟用户数据
|
import { mockEmployees } from './employee'
|
||||||
const mockUsers = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
username: 'admin',
|
|
||||||
password: '123456',
|
|
||||||
name: '系统管理员',
|
|
||||||
user_type: 'admin',
|
|
||||||
status: '1',
|
|
||||||
createdate: '2025-01-10 08:00:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
username: 'zhangsan',
|
|
||||||
password: '123456',
|
|
||||||
name: '张三',
|
|
||||||
user_type: 'user',
|
|
||||||
status: '1',
|
|
||||||
createdate: '2025-02-15 10:30:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
username: 'lisi',
|
|
||||||
password: '123456',
|
|
||||||
name: '李四',
|
|
||||||
user_type: 'user',
|
|
||||||
status: '0',
|
|
||||||
createdate: '2025-03-20 14:00:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
username: 'wangwu',
|
|
||||||
password: '123456',
|
|
||||||
name: '王五',
|
|
||||||
user_type: 'admin',
|
|
||||||
status: '1',
|
|
||||||
createdate: '2025-04-05 09:15:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
username: 'zhaoliu',
|
|
||||||
password: '123456',
|
|
||||||
name: '赵六',
|
|
||||||
user_type: 'user',
|
|
||||||
status: '0',
|
|
||||||
createdate: '2025-05-10 16:45:00'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 模拟网络延迟
|
// 模拟网络延迟
|
||||||
const delay = (ms = 300) => new Promise(r => setTimeout(r, ms))
|
const delay = (ms = 300) => new Promise(r => setTimeout(r, ms))
|
||||||
|
|
||||||
// Mock API
|
// 将员工记录转换为用户格式(字段名映射)
|
||||||
|
const toUserFormat = (employee) => ({
|
||||||
|
id: employee.id,
|
||||||
|
username: employee.username,
|
||||||
|
name: employee.name,
|
||||||
|
user_type: employee.user_type,
|
||||||
|
status: employee.user_status,
|
||||||
|
employee_id: employee.id,
|
||||||
|
createdate: employee.created_at
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock API(操作同一数据源 mockEmployees)
|
||||||
export const mockUserAPI = {
|
export const mockUserAPI = {
|
||||||
// 获取列表
|
// 获取用户列表(返回有用户账号的员工记录)
|
||||||
async getList(params = {}) {
|
async getList(params = {}) {
|
||||||
await delay()
|
await delay()
|
||||||
let result = [...mockUsers]
|
// 筛选有用户名的记录(即已创建用户的员工)
|
||||||
|
let result = mockEmployees.filter(item => item.username).map(toUserFormat)
|
||||||
|
|
||||||
// 关键词搜索
|
// 关键词搜索
|
||||||
if (params.keyword) {
|
if (params.keyword) {
|
||||||
@@ -67,16 +32,15 @@ export const mockUserAPI = {
|
|||||||
result = result.filter(item => String(item.id) === keyword)
|
result = result.filter(item => String(item.id) === keyword)
|
||||||
} else if (searchField === '1') {
|
} else if (searchField === '1') {
|
||||||
// 用户名:模糊匹配
|
// 用户名:模糊匹配
|
||||||
result = result.filter(item => item.username.includes(keyword))
|
result = result.filter(item => item.username && item.username.includes(keyword))
|
||||||
} else if (searchField === '3') {
|
} else if (searchField === '3') {
|
||||||
// 真实姓名:模糊匹配
|
// 真实姓名:模糊匹配
|
||||||
result = result.filter(item => item.name.includes(keyword))
|
result = result.filter(item => item.name.toLowerCase().includes(keyword))
|
||||||
} else {
|
} else {
|
||||||
// 默认:模糊匹配所有字段
|
|
||||||
result = result.filter(item =>
|
result = result.filter(item =>
|
||||||
String(item.id).includes(keyword) ||
|
String(item.id).includes(keyword) ||
|
||||||
item.username.includes(keyword) ||
|
(item.username && item.username.includes(keyword)) ||
|
||||||
item.name.includes(keyword)
|
item.name.toLowerCase().includes(keyword)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,47 +58,80 @@ export const mockUserAPI = {
|
|||||||
return { code: 200, data: result, total: result.length }
|
return { code: 200, data: result, total: result.length }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取详情
|
// 获取用户详情
|
||||||
async getDetail(id) {
|
async getDetail(id) {
|
||||||
await delay()
|
await delay()
|
||||||
const user = mockUsers.find(item => item.id === id)
|
const employee = mockEmployees.find(item => item.id === id && item.username)
|
||||||
if (user) {
|
if (employee) {
|
||||||
return { code: 200, data: user }
|
return { code: 200, data: toUserFormat(employee) }
|
||||||
}
|
}
|
||||||
return { code: 404, message: '用户不存在' }
|
return { code: 404, message: '用户不存在' }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 新增
|
// 创建用户(实际上是更新已有员工记录,添加用户字段)
|
||||||
async create(data) {
|
async create(data) {
|
||||||
await delay()
|
await delay()
|
||||||
const maxId = mockUsers.length > 0 ? Math.max(...mockUsers.map(item => item.id)) : 0
|
// 查找对应的员工记录
|
||||||
const newUser = {
|
const employeeIndex = mockEmployees.findIndex(item => item.id === data.id || item.id === data.employee_id)
|
||||||
id: maxId + 1,
|
if (employeeIndex === -1) {
|
||||||
...data,
|
return { code: 404, message: '关联员工不存在' }
|
||||||
createdate: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
|
||||||
}
|
}
|
||||||
mockUsers.push(newUser)
|
|
||||||
return { code: 200, data: newUser, message: '新增成功' }
|
// 检查是否已有用户账号
|
||||||
|
if (mockEmployees[employeeIndex].username) {
|
||||||
|
return { code: 400, message: '该员工已有用户账号' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新员工记录,添加用户字段
|
||||||
|
mockEmployees[employeeIndex] = {
|
||||||
|
...mockEmployees[employeeIndex],
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
user_type: data.user_type || 'user',
|
||||||
|
user_status: data.status || '1',
|
||||||
|
updated_at: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 200, data: toUserFormat(mockEmployees[employeeIndex]), message: '用户创建成功' }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新
|
// 更新用户(更新用户字段)
|
||||||
async update(id, data) {
|
async update(id, data) {
|
||||||
await delay()
|
await delay()
|
||||||
const index = mockUsers.findIndex(item => item.id === id)
|
const index = mockEmployees.findIndex(item => item.id === id && item.username)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
mockUsers[index] = { ...mockUsers[index], ...data }
|
// 只更新用户相关字段
|
||||||
return { code: 200, data: mockUsers[index], message: '更新成功' }
|
const updateFields = {}
|
||||||
|
if (data.username !== undefined) updateFields.username = data.username
|
||||||
|
if (data.password !== undefined) updateFields.password = data.password
|
||||||
|
if (data.user_type !== undefined) updateFields.user_type = data.user_type
|
||||||
|
if (data.status !== undefined) updateFields.user_status = data.status
|
||||||
|
|
||||||
|
mockEmployees[index] = {
|
||||||
|
...mockEmployees[index],
|
||||||
|
...updateFields,
|
||||||
|
updated_at: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||||||
|
}
|
||||||
|
return { code: 200, data: toUserFormat(mockEmployees[index]), message: '更新成功' }
|
||||||
}
|
}
|
||||||
return { code: 404, message: '用户不存在' }
|
return { code: 404, message: '用户不存在' }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除
|
// 删除用户(清除用户字段,保留员工记录)
|
||||||
async delete(id) {
|
async delete(id) {
|
||||||
await delay()
|
await delay()
|
||||||
const index = mockUsers.findIndex(item => item.id === id)
|
const index = mockEmployees.findIndex(item => item.id === id && item.username)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
mockUsers.splice(index, 1)
|
// 清除用户字段,而不是删除记录
|
||||||
return { code: 200, message: '删除成功' }
|
mockEmployees[index] = {
|
||||||
|
...mockEmployees[index],
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
user_type: null,
|
||||||
|
user_status: null,
|
||||||
|
updated_at: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||||||
|
}
|
||||||
|
return { code: 200, message: '用户已删除' }
|
||||||
}
|
}
|
||||||
return { code: 404, message: '用户不存在' }
|
return { code: 404, message: '用户不存在' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,26 @@
|
|||||||
import axios from 'axios'
|
import request from './request'
|
||||||
import { mockProductsAPI } from './mock/products'
|
|
||||||
|
|
||||||
// 开关:true 用 mock,false 用真实接口
|
|
||||||
const USE_MOCK = true
|
|
||||||
|
|
||||||
// axios 实例
|
|
||||||
const request = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取产品列表
|
// 获取产品列表
|
||||||
export const getProductsList = (params) => {
|
export const getProductsList = (params) => {
|
||||||
if (USE_MOCK) return mockProductsAPI.getList(params)
|
return request.get('/products', { params })
|
||||||
return request.get('/products', { params }).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取产品详情
|
// 获取产品详情
|
||||||
export const getProductsDetail = (id) => {
|
export const getProductsDetail = (id) => {
|
||||||
if (USE_MOCK) return mockProductsAPI.getDetail(id)
|
return request.get(`/products/${id}`)
|
||||||
return request.get(`/products/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增产品
|
// 新增产品
|
||||||
export const createProducts = (data) => {
|
export const createProducts = (data) => {
|
||||||
if (USE_MOCK) return mockProductsAPI.create(data)
|
return request.post('/products', data)
|
||||||
return request.post('/products', data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新产品
|
// 更新产品
|
||||||
export const updateProducts = (id, data) => {
|
export const updateProducts = (id, data) => {
|
||||||
if (USE_MOCK) return mockProductsAPI.update(id, data)
|
return request.put(`/products/${id}`, data)
|
||||||
return request.put(`/products/${id}`, data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除产品
|
// 删除产品
|
||||||
export const deleteProductsAPI = (id) => {
|
export const deleteProductsAPI = (id) => {
|
||||||
if (USE_MOCK) return mockProductsAPI.delete(id)
|
return request.delete(`/products/${id}`)
|
||||||
return request.delete(`/products/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/api/request.js
Normal file
53
src/api/request.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器:自动添加 token
|
||||||
|
request.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器:统一处理错误(config.quiet = true 时不弹窗)
|
||||||
|
request.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
const status = error.response?.status
|
||||||
|
const message = error.response?.data?.message || '请求失败'
|
||||||
|
const quiet = error.config?.quiet
|
||||||
|
const isLogin = error.config?.url?.includes('/user/login')
|
||||||
|
|
||||||
|
if (!quiet) {
|
||||||
|
if (status === 401) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
router.push('/login')
|
||||||
|
ElMessage.error('登录已过期,请重新登录')
|
||||||
|
} else if (status === 403 && !isLogin) {
|
||||||
|
ElMessage.error('权限不足')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default request
|
||||||
@@ -1,41 +1,26 @@
|
|||||||
import axios from 'axios'
|
import request from './request'
|
||||||
import { mockServiceAPI } from './mock/service'
|
|
||||||
|
|
||||||
// 开关:true 用 mock,false 用真实接口
|
|
||||||
const USE_MOCK = true
|
|
||||||
|
|
||||||
// axios 实例
|
|
||||||
const request = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取售后列表
|
// 获取售后列表
|
||||||
export const getServiceList = (params) => {
|
export const getServiceList = (params) => {
|
||||||
if (USE_MOCK) return mockServiceAPI.getList(params)
|
return request.get('/after-sales', { params })
|
||||||
return request.get('/after-sales', { params }).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取售后详情
|
// 获取售后详情
|
||||||
export const getServiceDetail = (id) => {
|
export const getServiceDetail = (id) => {
|
||||||
if (USE_MOCK) return mockServiceAPI.getDetail(id)
|
return request.get(`/after-sales/${id}`)
|
||||||
return request.get(`/after-sales/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增售后
|
// 新增售后
|
||||||
export const createService = (data) => {
|
export const createService = (data) => {
|
||||||
if (USE_MOCK) return mockServiceAPI.create(data)
|
return request.post('/after-sales', data)
|
||||||
return request.post('/after-sales', data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新售后
|
// 更新售后
|
||||||
export const updateService = (id, data) => {
|
export const updateService = (id, data) => {
|
||||||
if (USE_MOCK) return mockServiceAPI.update(id, data)
|
return request.put(`/after-sales/${id}`, data)
|
||||||
return request.put(`/after-sales/${id}`, data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除售后
|
// 删除售后
|
||||||
export const deleteServiceAPI = (id) => {
|
export const deleteServiceAPI = (id) => {
|
||||||
if (USE_MOCK) return mockServiceAPI.delete(id)
|
return request.delete(`/after-sales/${id}`)
|
||||||
return request.delete(`/after-sales/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/api/suppliers.js
Normal file
26
src/api/suppliers.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
// 获取供应商列表
|
||||||
|
export const getSupplierList = (params, config) => {
|
||||||
|
return request.get('/suppliers', { params, ...config })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取供应商详情
|
||||||
|
export const getSupplierDetail = (id) => {
|
||||||
|
return request.get(`/suppliers/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增供应商
|
||||||
|
export const createSupplier = (data) => {
|
||||||
|
return request.post('/suppliers', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新供应商
|
||||||
|
export const updateSupplier = (id, data) => {
|
||||||
|
return request.put(`/suppliers/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除供应商
|
||||||
|
export const deleteSupplierAPI = (id) => {
|
||||||
|
return request.delete(`/suppliers/${id}`)
|
||||||
|
}
|
||||||
@@ -1,41 +1,31 @@
|
|||||||
import axios from 'axios'
|
import request from './request'
|
||||||
import { mockUserAPI } from './mock/user'
|
|
||||||
|
|
||||||
// 开关:true 用 mock,false 用真实接口
|
// 获取用户列表(管理员)
|
||||||
const USE_MOCK = true
|
export const getUserList = (params, config) => {
|
||||||
|
return request.get('/users', { params, ...config })
|
||||||
|
}
|
||||||
|
|
||||||
// axios 实例
|
// 获取简易用户列表(用于下拉选择,仅需登录)
|
||||||
const request = axios.create({
|
export const getSimpleUserList = (config) => {
|
||||||
baseURL: '/api',
|
return request.get('/user/list', config)
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取用户列表
|
|
||||||
export const getUserList = (params) => {
|
|
||||||
if (USE_MOCK) return mockUserAPI.getList(params)
|
|
||||||
return request.get('/users', { params }).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户详情
|
// 获取用户详情
|
||||||
export const getUserDetail = (id) => {
|
export const getUserDetail = (id) => {
|
||||||
if (USE_MOCK) return mockUserAPI.getDetail(id)
|
return request.get(`/users/${id}`)
|
||||||
return request.get(`/users/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增用户
|
// 创建用户
|
||||||
export const createUser = (data) => {
|
export const createUser = (data) => {
|
||||||
if (USE_MOCK) return mockUserAPI.create(data)
|
return request.post('/users', data)
|
||||||
return request.post('/users', data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户
|
// 更新用户
|
||||||
export const updateUser = (id, data) => {
|
export const updateUser = (id, data) => {
|
||||||
if (USE_MOCK) return mockUserAPI.update(id, data)
|
return request.put(`/users/${id}`, data)
|
||||||
return request.put(`/users/${id}`, data).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除用户
|
// 删除用户
|
||||||
export const deleteUserAPI = (id) => {
|
export const deleteUserAPI = (id) => {
|
||||||
if (USE_MOCK) return mockUserAPI.delete(id)
|
return request.delete(`/users/${id}`)
|
||||||
return request.delete(`/users/${id}`).then(r => r.data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import {reactive, ref} from 'vue'
|
|||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
import {Lock, User} from '@element-plus/icons-vue'
|
import {Lock, User} from '@element-plus/icons-vue'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
|
import { login } from '../store/user'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -93,9 +94,13 @@ const handleLogin = async () => {
|
|||||||
try {
|
try {
|
||||||
await loginFormRef.value.validate()
|
await loginFormRef.value.validate()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await new Promise((r) => setTimeout(r, 800))
|
const res = await login(loginForm.username, loginForm.password)
|
||||||
ElMessage.success('登录成功(模拟)')
|
if (res.code === 0 || res.code === 200) {
|
||||||
router.push('/panel')
|
ElMessage.success('登录成功')
|
||||||
|
router.push('/panel/home')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '登录失败')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 校验失败,element-plus 会自动显示红色提示
|
// 校验失败,element-plus 会自动显示红色提示
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
} from '../api/contract'
|
} from '../api/contract'
|
||||||
import { getCustomerList } from '../api/customer'
|
import { getCustomerList } from '../api/customer'
|
||||||
import { getEmployeeList } from '../api/employee'
|
import { getEmployeeList } from '../api/employee'
|
||||||
|
import { getSupplierList } from '../api/suppliers'
|
||||||
|
import { getSimpleUserList } from '../api/user'
|
||||||
|
import { formatDateTime, formatDate } from "../utils/date"
|
||||||
|
import { hasPermission, getUserInfo } from '../store/user'
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const select = ref('1')
|
const select = ref('1')
|
||||||
@@ -24,18 +28,36 @@ const showDetailDialog = ref(false)
|
|||||||
const currentDetail = ref(null)
|
const currentDetail = ref(null)
|
||||||
const customerList = ref([])
|
const customerList = ref([])
|
||||||
const employeeList = ref([])
|
const employeeList = ref([])
|
||||||
|
const supplierList = ref([])
|
||||||
|
const userList = ref([])
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
// 采购经理只能操作采购合同,招商经理只能操作加盟合同(提前定义,供 form 初始化使用)
|
||||||
|
const isAdmin = computed(() => getUserInfo()?.roleName === 'admin')
|
||||||
|
const isProcurementManager = computed(() => getUserInfo()?.roleName === 'procurement_manager')
|
||||||
|
const isFranchiseManager = computed(() => getUserInfo()?.roleName === 'franchise_manager')
|
||||||
|
const getDefaultType = () => {
|
||||||
|
if (isProcurementManager.value) return 'supply'
|
||||||
|
if (isFranchiseManager.value) return 'franchise'
|
||||||
|
return 'franchise'
|
||||||
|
}
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
contract_no: '',
|
|
||||||
contract_name: '',
|
contract_name: '',
|
||||||
|
type: getDefaultType(),
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
|
supplier_id: '',
|
||||||
contract_content: '',
|
contract_content: '',
|
||||||
employee_id: '',
|
employee_id: '',
|
||||||
effective_date: '',
|
effective_date: '',
|
||||||
expiry_date: '',
|
expiry_date: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
status: '待审批',
|
status: '待审批',
|
||||||
remark: ''
|
remark: '',
|
||||||
|
responsible_user_id: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -43,18 +65,56 @@ onMounted(() => {
|
|||||||
fetchOptions()
|
fetchOptions()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取客户和员工列表供下拉选择
|
// 监听搜索结果变化,重置页码
|
||||||
|
watch(searchResults, () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页后的数据
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return searchResults.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取客户、员工、供应商和用户列表供下拉选择
|
||||||
const fetchOptions = async () => {
|
const fetchOptions = async () => {
|
||||||
const [cRes, eRes] = await Promise.all([getCustomerList(), getEmployeeList()])
|
const results = await Promise.allSettled([
|
||||||
customerList.value = cRes.data
|
getCustomerList(null, { quiet: true }),
|
||||||
employeeList.value = eRes.data
|
getEmployeeList(null, { quiet: true }),
|
||||||
|
getSupplierList(null, { quiet: true }),
|
||||||
|
getSimpleUserList({ quiet: true })
|
||||||
|
])
|
||||||
|
if (results[0].status === 'fulfilled') {
|
||||||
|
const cRes = results[0].value
|
||||||
|
customerList.value = cRes.data?.list || cRes.data || []
|
||||||
|
}
|
||||||
|
if (results[1].status === 'fulfilled') {
|
||||||
|
const eRes = results[1].value
|
||||||
|
employeeList.value = eRes.data?.list || eRes.data || []
|
||||||
|
}
|
||||||
|
if (results[2].status === 'fulfilled') {
|
||||||
|
const sRes = results[2].value
|
||||||
|
supplierList.value = sRes.data?.list || sRes.data || []
|
||||||
|
}
|
||||||
|
if (results[3].status === 'fulfilled') {
|
||||||
|
const uRes = results[3].value
|
||||||
|
userList.value = uRes.data || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取合同列表
|
// 获取合同列表
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getContractList()
|
const res = await getContractList()
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -68,20 +128,21 @@ const handleSearch = async () => {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
let keyword = search.value
|
const params = {}
|
||||||
if (select.value === '1' && keyword && !keyword.startsWith('HT')) {
|
if (select.value === '1') {
|
||||||
keyword = 'HT' + keyword
|
params.id = Number(search.value)
|
||||||
|
} else if (select.value === '2') {
|
||||||
|
params.contract_name = search.value
|
||||||
}
|
}
|
||||||
|
if (dateRange.value && dateRange.value.length === 2) {
|
||||||
const params = {
|
params.effective_date_start = formatDate(dateRange.value[0])
|
||||||
keyword,
|
params.effective_date_end = formatDate(dateRange.value[1])
|
||||||
searchField: select.value,
|
|
||||||
startDate: dateRange.value?.[0],
|
|
||||||
endDate: dateRange.value?.[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getContractList(params)
|
const res = await getContractList(params)
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -111,16 +172,18 @@ const editContract = (row) => {
|
|||||||
isEditMode.value = true
|
isEditMode.value = true
|
||||||
currentContractId.value = row.id
|
currentContractId.value = row.id
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
contract_no: row.contract_no,
|
|
||||||
contract_name: row.contract_name,
|
contract_name: row.contract_name,
|
||||||
|
type: row.type || 'franchise',
|
||||||
customer_id: row.customer_id,
|
customer_id: row.customer_id,
|
||||||
|
supplier_id: row.supplier_id,
|
||||||
contract_content: row.contract_content,
|
contract_content: row.contract_content,
|
||||||
employee_id: row.employee_id,
|
employee_id: row.employee_id,
|
||||||
effective_date: row.effective_date,
|
effective_date: row.effective_date,
|
||||||
expiry_date: row.expiry_date,
|
expiry_date: row.expiry_date,
|
||||||
amount: row.amount,
|
amount: row.amount,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
remark: row.remark
|
remark: row.remark,
|
||||||
|
responsible_user_id: row.responsible_user_id || ''
|
||||||
})
|
})
|
||||||
showAddForm.value = true
|
showAddForm.value = true
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
@@ -128,25 +191,53 @@ const editContract = (row) => {
|
|||||||
|
|
||||||
// 保存合同
|
// 保存合同
|
||||||
const saveContract = async () => {
|
const saveContract = async () => {
|
||||||
|
// 表单校验
|
||||||
|
if (!form.contract_name) {
|
||||||
|
return ElMessage.warning('请输入合同名称')
|
||||||
|
}
|
||||||
|
if (form.type !== 'supply' && !form.customer_id) {
|
||||||
|
return ElMessage.warning('加盟合同必须选择客户')
|
||||||
|
}
|
||||||
|
if (form.type === 'supply' && !form.supplier_id) {
|
||||||
|
return ElMessage.warning('采购合同必须选择供应商')
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
customer_id: form.customer_id || null,
|
||||||
|
supplier_id: form.supplier_id || null,
|
||||||
|
employee_id: form.employee_id || null,
|
||||||
|
responsible_user_id: form.responsible_user_id || null
|
||||||
|
}
|
||||||
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateContract(currentContractId.value, { ...form })
|
await updateContract(currentContractId.value, payload)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
} else {
|
} else {
|
||||||
await createContract({ ...form })
|
await createContract(payload)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
}
|
}
|
||||||
cancelAdd()
|
cancelAdd()
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (e) {
|
||||||
|
// 错误已由拦截器处理
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除合同
|
// 删除合同
|
||||||
const deleteContract = (id) => {
|
const deleteContract = (id) => {
|
||||||
ElMessageBox.confirm('确定删除该合同吗?', '提示', {
|
ElMessageBox.confirm('确定删除该合同吗?', '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
|
try {
|
||||||
await deleteContractAPI(id)
|
await deleteContractAPI(id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (e) {
|
||||||
|
// 错误已由拦截器处理
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,16 +251,18 @@ const cancelAdd = () => {
|
|||||||
// 重置表单
|
// 重置表单
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
contract_no: '',
|
|
||||||
contract_name: '',
|
contract_name: '',
|
||||||
|
type: getDefaultType(),
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
|
supplier_id: '',
|
||||||
contract_content: '',
|
contract_content: '',
|
||||||
employee_id: '',
|
employee_id: '',
|
||||||
effective_date: '',
|
effective_date: '',
|
||||||
expiry_date: '',
|
expiry_date: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
status: '待审批',
|
status: '待审批',
|
||||||
remark: ''
|
remark: '',
|
||||||
|
responsible_user_id: ''
|
||||||
})
|
})
|
||||||
isEditMode.value = false
|
isEditMode.value = false
|
||||||
currentContractId.value = null
|
currentContractId.value = null
|
||||||
@@ -191,6 +284,7 @@ const closeDetail = () => {
|
|||||||
const getStatusType = (status) => {
|
const getStatusType = (status) => {
|
||||||
const map = {
|
const map = {
|
||||||
'生效中': 'success',
|
'生效中': 'success',
|
||||||
|
'生效': 'success',
|
||||||
'已到期': 'danger',
|
'已到期': 'danger',
|
||||||
'待审批': 'warning',
|
'待审批': 'warning',
|
||||||
'已终止': 'info'
|
'已终止': 'info'
|
||||||
@@ -229,10 +323,23 @@ const dateShortcuts = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 格式化日期
|
// 检查权限
|
||||||
const formatDate = (date) => {
|
const canCreate = computed(() => hasPermission('contract', 'create'))
|
||||||
if (!date) return ''
|
const canUpdate = computed(() => hasPermission('contract', 'update'))
|
||||||
return date
|
const canDelete = computed(() => hasPermission('contract', 'delete'))
|
||||||
|
|
||||||
|
// 招商经理只能编辑加盟合同,采购经理只能编辑采购合同
|
||||||
|
const canEditRow = (row) => {
|
||||||
|
if (!canUpdate.value) return false
|
||||||
|
if (isProcurementManager.value && row.type !== 'supply') return false
|
||||||
|
if (isFranchiseManager.value && row.type !== 'franchise') return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const canDeleteRow = (row) => {
|
||||||
|
if (!canDelete.value) return false
|
||||||
|
if (isProcurementManager.value && row.type !== 'supply') return false
|
||||||
|
if (isFranchiseManager.value && row.type !== 'franchise') return false
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -242,8 +349,8 @@ const formatDate = (date) => {
|
|||||||
<div class="contract-search">
|
<div class="contract-search">
|
||||||
<!-- 第一行:搜索框 + 按钮 -->
|
<!-- 第一行:搜索框 + 按钮 -->
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词" class="contract-search-with-select"
|
||||||
class="contract-search-with-select" @keyup.enter="handleSearch">
|
@keyup.enter="handleSearch">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<el-select v-model="select" placeholder="请选择" style="width: 115px">
|
<el-select v-model="select" placeholder="请选择" style="width: 115px">
|
||||||
<el-option label="编号" value="1" />
|
<el-option label="编号" value="1" />
|
||||||
@@ -256,7 +363,7 @@ const formatDate = (date) => {
|
|||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<div class="search-buttons">
|
<div class="search-buttons">
|
||||||
<el-button type="success" @click="addContract">新增合同</el-button>
|
<el-button type="success" @click="addContract" v-if="canCreate">新增合同</el-button>
|
||||||
<el-button type="default" @click="resetSearch">重置</el-button>
|
<el-button type="default" @click="resetSearch">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,15 +372,8 @@ const formatDate = (date) => {
|
|||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">筛选条件:</span>
|
<span class="filter-label">筛选条件:</span>
|
||||||
|
|
||||||
<el-date-picker
|
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="生效日期"
|
||||||
v-model="dateRange"
|
end-placeholder="到期日期" style="margin-left: 10px;" @change="handleDateChange" />
|
||||||
type="daterange"
|
|
||||||
range-separator="至"
|
|
||||||
start-placeholder="生效日期"
|
|
||||||
end-placeholder="到期日期"
|
|
||||||
style="margin-left: 10px;"
|
|
||||||
@change="handleDateChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -292,12 +392,30 @@ const formatDate = (date) => {
|
|||||||
<span class="total-count">共找到 {{ searchResults.length }} 条合同</span>
|
<span class="total-count">共找到 {{ searchResults.length }} 条合同</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="searchResults" border style="width: 100%" @row-dblclick="showDetail" row-class-name="clickable-row">
|
<el-table :data="paginatedData" border style="width: 100%" @row-dblclick="showDetail"
|
||||||
<el-table-column prop="id" label="ID" width="60" />
|
row-class-name="clickable-row">
|
||||||
<el-table-column prop="contract_no" label="合同编号" width="130" />
|
<el-table-column prop="id" label="编号" width="70" />
|
||||||
<el-table-column prop="contract_name" label="合同名称" min-width="180" />
|
<el-table-column prop="contract_name" label="合同名称" min-width="180" />
|
||||||
<el-table-column prop="customer_id" label="客户ID" width="80" />
|
<el-table-column prop="type" label="类型" width="90">
|
||||||
<el-table-column prop="employee_id" label="业务员ID" width="90" />
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.type === 'franchise' ? 'success' : 'warning'">{{ row.type === 'franchise' ? '加盟' : '采购' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="customer_name" label="客户" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.customer_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="supplier_name" label="供应商" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.supplier_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="employee_name" label="业务员" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.employee_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="amount" label="金额" width="110">
|
<el-table-column prop="amount" label="金额" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
¥{{ row.amount?.toLocaleString() }}
|
¥{{ row.amount?.toLocaleString() }}
|
||||||
@@ -308,15 +426,36 @@ const formatDate = (date) => {
|
|||||||
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
|
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="effective_date" label="生效日期" width="110" />
|
<el-table-column prop="effective_date" label="生效日期" width="110">
|
||||||
<el-table-column prop="expiry_date" label="到期日期" width="110" />
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.effective_date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="expiry_date" label="到期日期" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.expiry_date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="140" fixed="right">
|
<el-table-column label="操作" width="140" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="editContract(row)">编辑</el-button>
|
<el-button link type="primary" @click="editContract(row)" v-if="canEditRow(row)">编辑</el-button>
|
||||||
<el-button link type="danger" @click="deleteContract(row.id)">删除</el-button>
|
<el-button link type="danger" @click="deleteContract(row.id)" v-if="canDeleteRow(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="searchResults.length"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -326,24 +465,34 @@ const formatDate = (date) => {
|
|||||||
<el-form :model="form" label-width="auto" style="max-width: 700px;" label-position="top">
|
<el-form :model="form" label-width="auto" style="max-width: 700px;" label-position="top">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="合同编号" required>
|
<el-form-item label="合同名称" required>
|
||||||
<el-input v-model="form.contract_no" placeholder="如: HT-001" />
|
<el-input v-model="form.contract_name" placeholder="请输入合同名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="合同名称" required>
|
<el-form-item label="合同类型" required>
|
||||||
<el-input v-model="form.contract_name" placeholder="请输入合同名称" />
|
<el-select v-model="form.type" placeholder="请选择合同类型" style="width: 100%;" :disabled="isProcurementManager || isFranchiseManager">
|
||||||
|
<el-option v-if="!isProcurementManager" label="加盟合同" value="franchise" />
|
||||||
|
<el-option v-if="!isFranchiseManager" label="采购合同" value="supply" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="客户" required>
|
<!-- 加盟合同选客户 -->
|
||||||
|
<el-form-item v-if="form.type !== 'supply'" label="客户" required>
|
||||||
<el-select v-model="form.customer_id" placeholder="请选择客户" style="width: 100%;" filterable>
|
<el-select v-model="form.customer_id" placeholder="请选择客户" style="width: 100%;" filterable>
|
||||||
<el-option v-for="c in customerList" :key="c.id" :label="`${c.id} - ${c.name}`" :value="c.id" />
|
<el-option v-for="c in customerList" :key="c.id" :label="`${c.id} - ${c.name}`" :value="c.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<!-- 采购合同选供应商 -->
|
||||||
|
<el-form-item v-else label="供应商" required>
|
||||||
|
<el-select v-model="form.supplier_id" placeholder="请选择供应商" style="width: 100%;" filterable>
|
||||||
|
<el-option v-for="s in supplierList" :key="s.id" :label="`${s.id} - ${s.name}`" :value="s.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="业务员">
|
<el-form-item label="业务员">
|
||||||
@@ -357,22 +506,35 @@ const formatDate = (date) => {
|
|||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="生效日期" required>
|
<el-form-item label="生效日期" required>
|
||||||
<el-date-picker v-model="form.effective_date" type="date" style="width: 100%;" />
|
<el-date-picker v-model="form.effective_date" type="date" value-format="YYYY-MM-DD" style="width: 100%;" />
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="到期日期" required>
|
|
||||||
<el-date-picker v-model="form.expiry_date" type="date" style="width: 100%;" />
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="到期日期" required>
|
||||||
|
<el-date-picker v-model="form.expiry_date" type="date" value-format="YYYY-MM-DD" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="合同金额" required>
|
<el-form-item label="合同金额" required>
|
||||||
<el-input-number v-model="form.amount" :min="0" :step="1000" style="width: 100%;" />
|
<el-input-number v-model="form.amount" :min="0" :step="1000" style="width: 100%;" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" v-if="isAdmin">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="负责人">
|
||||||
|
<el-select v-model="form.responsible_user_id" placeholder="请选择负责人" style="width: 100%;" filterable clearable>
|
||||||
|
<el-option v-for="u in userList" :key="u.id" :label="`${u.id} - ${u.real_name || u.username}`" :value="u.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="状态" required>
|
<el-form-item label="状态" required>
|
||||||
<el-select v-model="form.status" style="width: 100%;">
|
<el-select v-model="form.status" style="width: 100%;">
|
||||||
@@ -404,29 +566,34 @@ const formatDate = (date) => {
|
|||||||
<el-dialog v-model="showDetailDialog" title="合同详情" width="650px" @close="closeDetail">
|
<el-dialog v-model="showDetailDialog" title="合同详情" width="650px" @close="closeDetail">
|
||||||
<div v-if="currentDetail" class="detail-content">
|
<div v-if="currentDetail" class="detail-content">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="合同编号">{{ currentDetail.contract_no }}</el-descriptions-item>
|
<el-descriptions-item label="合同编号">{{ currentDetail.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合同名称" :span="2">{{ currentDetail.contract_name }}</el-descriptions-item>
|
<el-descriptions-item label="合同名称">{{ currentDetail.contract_name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户ID">{{ currentDetail.customer_id }}</el-descriptions-item>
|
<el-descriptions-item label="合同类型">{{ currentDetail.type === 'franchise' ? '加盟合同' : '采购合同' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="业务员ID">{{ currentDetail.employee_id }}</el-descriptions-item>
|
<el-descriptions-item v-if="currentDetail.type !== 'supply'" label="客户">{{ currentDetail.customer_name || currentDetail.customer_id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-else label="供应商">{{ currentDetail.supplier_name || currentDetail.supplier_id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="业务员">{{ currentDetail.employee_name || currentDetail.employee_id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合同金额" :span="2">
|
<el-descriptions-item label="合同金额" :span="2">
|
||||||
<span class="amount-text">¥{{ currentDetail.amount?.toLocaleString() }}</span>
|
<span class="amount-text">¥{{ currentDetail.amount?.toLocaleString() }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="getStatusType(currentDetail.status)">{{ currentDetail.status }}</el-tag>
|
<el-tag :type="getStatusType(currentDetail.status)">{{ currentDetail.status }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="生效日期">{{ currentDetail.effective_date }}</el-descriptions-item>
|
<el-descriptions-item label="生效日期">{{ formatDate(currentDetail.effective_date) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="到期日期" :span="2">{{ currentDetail.expiry_date }}</el-descriptions-item>
|
<el-descriptions-item label="到期日期" :span="2">{{ formatDate(currentDetail.expiry_date) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合同内容" :span="2">
|
<el-descriptions-item label="合同内容" :span="2">
|
||||||
<div class="content-text">{{ currentDetail.contract_content || '无' }}</div>
|
<div class="content-text">{{ currentDetail.contract_content || '无' }}</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">
|
<el-descriptions-item label="备注" :span="2">
|
||||||
{{ currentDetail.remark || '无' }}
|
{{ currentDetail.remark || '无' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="负责人">
|
||||||
|
{{ currentDetail.responsible_user_name || currentDetail.responsible_user_id || '无' }}
|
||||||
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="closeDetail">关闭</el-button>
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
<el-button type="primary" @click="closeDetail(); editContract(currentDetail)">编辑</el-button>
|
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editContract(d) })()">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -481,7 +648,8 @@ const formatDate = (date) => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-container, .empty-container {
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,4 +673,12 @@ const formatDate = (date) => {
|
|||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
updateCustomer,
|
updateCustomer,
|
||||||
deleteCustomerAPI
|
deleteCustomerAPI
|
||||||
} from '../api/customer'
|
} from '../api/customer'
|
||||||
|
import { getSimpleUserList } from '../api/user'
|
||||||
|
import { hasPermission, getUserInfo } from '../store/user'
|
||||||
|
|
||||||
|
const isAdmin = computed(() => getUserInfo()?.roleName === 'admin')
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
@@ -18,8 +22,8 @@ const form = reactive({
|
|||||||
region: [],
|
region: [],
|
||||||
address: '',
|
address: '',
|
||||||
email: '',
|
email: '',
|
||||||
customer_type: 'Normal',
|
remark: '',
|
||||||
remark: ''
|
responsible_user_id: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
@@ -34,6 +38,11 @@ let searchSeq = 0
|
|||||||
const showDetailDialog = ref(false)
|
const showDetailDialog = ref(false)
|
||||||
const currentDetail = ref(null)
|
const currentDetail = ref(null)
|
||||||
const filterRegion = ref([])
|
const filterRegion = ref([])
|
||||||
|
const userList = ref([])
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
// 只保留省市两级的地区数据
|
// 只保留省市两级的地区数据
|
||||||
const regionDataTwoLevel = computed(() => {
|
const regionDataTwoLevel = computed(() => {
|
||||||
@@ -46,38 +55,60 @@ const regionDataTwoLevel = computed(() => {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
// 分页后的数据
|
||||||
fetchData()
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return searchResults.value.slice(start, end)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 切换代理商类型时清空已选地区
|
onMounted(() => {
|
||||||
watch(() => form.customer_type, () => {
|
fetchData()
|
||||||
form.region = []
|
// 仅管理员可加载用户列表用于选择负责人
|
||||||
|
if (isAdmin.value) {
|
||||||
|
getSimpleUserList({ quiet: true }).then(res => {
|
||||||
|
userList.value = res.data?.list || res.data || []
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听搜索结果变化,重置页码
|
||||||
|
watch(searchResults, () => {
|
||||||
|
currentPage.value = 1
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取客户列表
|
// 获取客户列表
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getCustomerList()
|
const res = await getCustomerList()
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
|
// 如果在表单页面,先关闭表单
|
||||||
|
if (showAddForm.value) {
|
||||||
|
cancelAdd()
|
||||||
|
}
|
||||||
|
|
||||||
const seq = ++searchSeq
|
const seq = ++searchSeq
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
keyword: search.value,
|
name: select.value === '1' ? search.value : undefined,
|
||||||
searchField: select.value,
|
id: select.value === '2' ? Number(search.value) : undefined,
|
||||||
province: filterRegion.value?.[0] ? CodeToText[filterRegion.value[0]] : '',
|
province: filterRegion.value?.[0] ? CodeToText[filterRegion.value[0]] : undefined,
|
||||||
city: filterRegion.value?.[1] ? CodeToText[filterRegion.value[1]] : ''
|
city: filterRegion.value?.[1] ? CodeToText[filterRegion.value[1]] : undefined
|
||||||
}
|
}
|
||||||
const res = await getCustomerList(params)
|
const res = await getCustomerList(params)
|
||||||
if (seq !== searchSeq) return
|
if (seq !== searchSeq) return
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
} finally {
|
} finally {
|
||||||
if (seq === searchSeq) loading.value = false
|
if (seq === searchSeq) loading.value = false
|
||||||
@@ -92,8 +123,8 @@ const resetForm = () => {
|
|||||||
form.region = []
|
form.region = []
|
||||||
form.address = ''
|
form.address = ''
|
||||||
form.email = ''
|
form.email = ''
|
||||||
form.customer_type = 'Normal'
|
|
||||||
form.remark = ''
|
form.remark = ''
|
||||||
|
form.responsible_user_id = ''
|
||||||
isEditMode.value = false
|
isEditMode.value = false
|
||||||
currentCustomerId.value = null
|
currentCustomerId.value = null
|
||||||
}
|
}
|
||||||
@@ -118,8 +149,8 @@ const editCustomer = (row) => {
|
|||||||
]
|
]
|
||||||
form.address = row.address
|
form.address = row.address
|
||||||
form.email = row.email
|
form.email = row.email
|
||||||
form.customer_type = row.customer_type
|
|
||||||
form.remark = row.remark
|
form.remark = row.remark
|
||||||
|
form.responsible_user_id = row.responsible_user_id || ''
|
||||||
showAddForm.value = true
|
showAddForm.value = true
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
}
|
}
|
||||||
@@ -133,9 +164,10 @@ const saveCustomer = async () => {
|
|||||||
district: CodeToText[form.region[2]] || '',
|
district: CodeToText[form.region[2]] || '',
|
||||||
address: form.address,
|
address: form.address,
|
||||||
email: form.email,
|
email: form.email,
|
||||||
customer_type: form.customer_type,
|
remark: form.remark,
|
||||||
remark: form.remark
|
responsible_user_id: form.responsible_user_id !== '' ? form.responsible_user_id : null
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateCustomer(currentCustomerId.value, formData)
|
await updateCustomer(currentCustomerId.value, formData)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
@@ -146,16 +178,25 @@ const saveCustomer = async () => {
|
|||||||
|
|
||||||
cancelAdd()
|
cancelAdd()
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '操作失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
const deleteCustomer = async (id) => {
|
const deleteCustomer = async (id) => {
|
||||||
ElMessageBox.confirm('确定删除该客户吗?', '提示', {
|
ElMessageBox.confirm('确定删除该客户吗?', '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
|
try {
|
||||||
await deleteCustomerAPI(id)
|
await deleteCustomerAPI(id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '删除失败')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +213,7 @@ const clearSearch = () => {
|
|||||||
filterRegion.value = []
|
filterRegion.value = []
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
showAddForm.value = false
|
showAddForm.value = false
|
||||||
|
currentPage.value = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,20 +229,29 @@ const closeDetail = () => {
|
|||||||
currentDetail.value = null
|
currentDetail.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const canCreate = computed(() => hasPermission('customer', 'create'))
|
||||||
|
const canUpdate = computed(() => hasPermission('customer', 'update'))
|
||||||
|
const canDelete = computed(() => hasPermission('customer', 'delete'))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="customer-container">
|
<div class="customer-container">
|
||||||
<!-- 搜索栏区域 -->
|
<!-- 搜索栏区域 -->
|
||||||
<div class="customer-search">
|
<div class="customer-search">
|
||||||
<el-input v-model="search" style="max-width: 600px" placeholder="Please input"
|
<div class="search-row">
|
||||||
|
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
||||||
class="customer-search-with-select" @keyup.enter="handleSearch">
|
class="customer-search-with-select" @keyup.enter="handleSearch">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<el-select v-model="select" placeholder="Select" style="width: 115px">
|
<el-select v-model="select" placeholder="请选择" style="width: 115px">
|
||||||
<el-option label="姓名" value="1" />
|
<el-option label="姓名" value="1" />
|
||||||
<el-option label="编号" value="2" />
|
<el-option label="编号" value="2" />
|
||||||
<el-option label="电话" value="3" />
|
|
||||||
<el-option label="邮箱" value="4" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
@@ -208,12 +259,11 @@ const closeDetail = () => {
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<el-button type="default" @click="clearSearch" style="margin-left:20px">
|
<div class="search-buttons">
|
||||||
重置
|
<el-button color="#E60012" @click="addCustomer" v-if="canCreate">新增客户</el-button>
|
||||||
</el-button>
|
<el-button type="default" @click="clearSearch">重置</el-button>
|
||||||
<el-button color="#E60012" @click="addCustomer" style="margin-left: 20px">
|
</div>
|
||||||
新增客户
|
</div>
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">筛选条件:</span>
|
<span class="filter-label">筛选条件:</span>
|
||||||
@@ -239,31 +289,40 @@ const closeDetail = () => {
|
|||||||
<el-empty description="暂无数据" />
|
<el-empty description="暂无数据" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="list-header">
|
||||||
|
<span class="total-count">共找到 {{ searchResults.length }} 条记录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else class="customer-table">
|
<el-table :data="paginatedData" v-loading="loading" border style="width: 100%" @row-dblclick="handleRowDblClick" row-class-name="clickable-row">
|
||||||
<el-table :data="searchResults" v-loading="loading" border style="width: 100%" @row-dblclick="handleRowDblClick">
|
|
||||||
<el-table-column prop="id" label="编号" width="80" />
|
<el-table-column prop="id" label="编号" width="80" />
|
||||||
<el-table-column prop="name" label="姓名" width="120" />
|
<el-table-column prop="name" label="姓名" width="120" />
|
||||||
<el-table-column prop="phone" label="电话" width="150" />
|
<el-table-column prop="phone" label="电话" width="150" />
|
||||||
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column prop="customer_type" label="代理商类型" width="120">
|
<el-table-column prop="responsible_user_name" label="负责人" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.customer_type === 'VIP' ? 'danger' : 'info'">
|
{{ row.responsible_user_name || '-' }}
|
||||||
{{ row.customer_type === 'VIP' ? '地区总代理' : '加盟商' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="editCustomer(row)">编辑</el-button>
|
<el-button link type="primary" @click="editCustomer(row)" v-if="canUpdate">编辑</el-button>
|
||||||
<el-button link type="danger" @click="deleteCustomer(row.id)">删除</el-button>
|
<el-button link type="danger" @click="deleteCustomer(row.id)" v-if="canDelete">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div style="margin-top: 10px; color: #909399; font-size: 14px;">
|
<!-- 分页 -->
|
||||||
找到 {{ searchResults.length }} 条结果
|
<div class="pagination-container">
|
||||||
<el-button link type="primary" @click="clearSearch">清空搜索</el-button>
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="searchResults.length"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,19 +337,19 @@ const closeDetail = () => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="电话" required>
|
<el-form-item label="电话" required>
|
||||||
<el-input v-model="form.phone" :controls="false" :min="0" :max="99999999999" :precision="0"
|
<el-input v-model="form.phone" maxlength="11"
|
||||||
placeholder="请输入11位手机号" style="width: 100%" />
|
placeholder="请输入11位手机号" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="地区">
|
<el-form-item label="地区">
|
||||||
<el-cascader v-model="form.region"
|
<el-cascader v-model="form.region"
|
||||||
:options="form.customer_type === 'VIP' ? regionDataTwoLevel : regionData"
|
:options="regionData"
|
||||||
:props="{ expandTrigger: 'hover' }"
|
:props="{ expandTrigger: 'hover' }"
|
||||||
:placeholder="form.customer_type === 'VIP' ? '请选择省/市(地区总代理)' : '请选择省/市/区'"
|
placeholder="请选择省/市/区"
|
||||||
clearable style="width: 100%" />
|
clearable style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="详细地址" v-if="form.customer_type !== 'VIP'">
|
<el-form-item label="详细地址">
|
||||||
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -298,11 +357,10 @@ const closeDetail = () => {
|
|||||||
<el-input v-model="form.email" placeholder="请输入邮箱地址" />
|
<el-input v-model="form.email" placeholder="请输入邮箱地址" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="代理商类型" required>
|
<el-form-item v-if="isAdmin" label="负责人">
|
||||||
<el-radio-group v-model="form.customer_type">
|
<el-select v-model="form.responsible_user_id" placeholder="请选择负责人" style="width: 100%;" filterable clearable>
|
||||||
<el-radio value="VIP">地区总代理</el-radio>
|
<el-option v-for="u in userList" :key="u.id" :label="`${u.id} - ${u.real_name || u.username}`" :value="u.id" />
|
||||||
<el-radio value="Normal">加盟商</el-radio>
|
</el-select>
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注">
|
<el-form-item label="备注">
|
||||||
@@ -324,15 +382,11 @@ const closeDetail = () => {
|
|||||||
<el-descriptions-item label="姓名">{{ currentDetail.name }}</el-descriptions-item>
|
<el-descriptions-item label="姓名">{{ currentDetail.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="电话">{{ currentDetail.phone }}</el-descriptions-item>
|
<el-descriptions-item label="电话">{{ currentDetail.phone }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="电子邮箱">{{ currentDetail.email }}</el-descriptions-item>
|
<el-descriptions-item label="电子邮箱">{{ currentDetail.email }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="代理商类型">
|
|
||||||
<el-tag :type="currentDetail.customer_type === 'VIP' ? 'danger' : 'info'">
|
|
||||||
{{ currentDetail.customer_type === 'VIP' ? '地区总代理' : '加盟商' }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="地区">
|
<el-descriptions-item label="地区">
|
||||||
{{ [currentDetail.province, currentDetail.city, currentDetail.district].filter(Boolean).join(' ') }}
|
{{ [currentDetail.province, currentDetail.city, currentDetail.district].filter(Boolean).join(' ') }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="详细地址" :span="2">{{ currentDetail.address || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="详细地址" :span="2">{{ currentDetail.address || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="负责人">{{ currentDetail.responsible_user_name || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">
|
<el-descriptions-item label="备注" :span="2">
|
||||||
{{ currentDetail.remark || '无' }}
|
{{ currentDetail.remark || '无' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -340,10 +394,79 @@ const closeDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="closeDetail">关闭</el-button>
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
<el-button type="primary" @click="closeDetail(); editCustomer(currentDetail)">编辑</el-button>
|
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editCustomer(d) })()">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.customer-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { ref, onMounted, reactive, watch } from 'vue'
|
import { ref, onMounted, reactive, watch, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
getEmployeeList,
|
getEmployeeList,
|
||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
updateEmployee,
|
updateEmployee,
|
||||||
deleteEmployeeAPI
|
deleteEmployeeAPI
|
||||||
} from '../api/employee'
|
} from '../api/employee'
|
||||||
|
import { hasPermission } from '../store/user'
|
||||||
|
import { formatDateTime, formatDate } from '../utils/date'
|
||||||
|
import { createUser } from '../api/user'
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -28,7 +31,6 @@ const form = reactive({
|
|||||||
const search = ref('')
|
const search = ref('')
|
||||||
const select = ref('1')
|
const select = ref('1')
|
||||||
const filterStatus = ref('')
|
const filterStatus = ref('')
|
||||||
const filterDepartment = ref('')
|
|
||||||
const searchResults = ref([])
|
const searchResults = ref([])
|
||||||
const showSearchResults = ref(false)
|
const showSearchResults = ref(false)
|
||||||
const showAddForm = ref(false)
|
const showAddForm = ref(false)
|
||||||
@@ -38,42 +40,89 @@ const loading = ref(false)
|
|||||||
const showDetailDialog = ref(false)
|
const showDetailDialog = ref(false)
|
||||||
const currentDetail = ref(null)
|
const currentDetail = ref(null)
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
// 新增用户相关状态
|
||||||
|
const showCreateUserForm = ref(false)
|
||||||
|
const justCreatedEmployee = ref(null)
|
||||||
|
const userForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
name: '',
|
||||||
|
role_id: 2,
|
||||||
|
is_active: 1,
|
||||||
|
employee_id: null
|
||||||
|
})
|
||||||
|
|
||||||
// 部门列表
|
// 部门列表
|
||||||
const departmentOptions = [
|
const departmentOptions = [
|
||||||
'运营部', '销售部', '技术部', '市场部', '财务部', '仓储部', '人事部', '行政部'
|
'运营部', '销售部', '技术部', '市场部', '财务部', '仓储部', '人事部', '行政部',
|
||||||
|
'招商部', '采购部', '总经理办公室', '信息技术部'
|
||||||
]
|
]
|
||||||
|
|
||||||
// 学历列表
|
// 学历列表
|
||||||
const educationOptions = ['高中', '专科', '本科', '硕士', '博士']
|
const educationOptions = ['高中', '专科', '本科', '硕士', '博士']
|
||||||
|
|
||||||
|
// 分页后的数据
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return searchResults.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听筛选变化
|
// 监听筛选变化
|
||||||
watch([filterStatus, filterDepartment], () => {
|
watch([filterStatus], () => {
|
||||||
|
currentPage.value = 1
|
||||||
handleSearch()
|
handleSearch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听搜索结果变化,重置页码
|
||||||
|
watch(searchResults, () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
// 获取员工列表
|
// 获取员工列表
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getEmployeeList()
|
const res = await getEmployeeList()
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
|
// 如果在表单页面,先关闭表单
|
||||||
|
if (showAddForm.value || showCreateUserForm.value) {
|
||||||
|
cancelAdd()
|
||||||
|
cancelCreateUser()
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getEmployeeList({
|
const params = {
|
||||||
keyword: search.value,
|
status: filterStatus.value !== '' ? filterStatus.value : undefined
|
||||||
searchField: select.value,
|
}
|
||||||
status: filterStatus.value,
|
if (search.value) {
|
||||||
department: filterDepartment.value
|
if (select.value === '1') {
|
||||||
})
|
params.id = Number(search.value)
|
||||||
searchResults.value = res.data
|
} else if (select.value === '2') {
|
||||||
|
params.name = search.value
|
||||||
|
} else if (select.value === '3') {
|
||||||
|
params.department = search.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await getEmployeeList(params)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -82,9 +131,9 @@ const handleSearch = async () => {
|
|||||||
const resetSearch = () => {
|
const resetSearch = () => {
|
||||||
search.value = ''
|
search.value = ''
|
||||||
filterStatus.value = ''
|
filterStatus.value = ''
|
||||||
filterDepartment.value = ''
|
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
showAddForm.value = false
|
showAddForm.value = false
|
||||||
|
currentPage.value = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,26 +204,54 @@ const saveEmployee = async () => {
|
|||||||
remark: form.remark
|
remark: form.remark
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateEmployee(currentEmployeeId.value, formData)
|
await updateEmployee(currentEmployeeId.value, formData)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
} else {
|
|
||||||
await createEmployee(formData)
|
|
||||||
ElMessage.success('新增成功')
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAdd()
|
cancelAdd()
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} else {
|
||||||
|
const res = await createEmployee(formData)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
// 保存刚创建的员工信息
|
||||||
|
justCreatedEmployee.value = res.data
|
||||||
|
// 弹出确认框:是否将员工新增为系统用户
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'员工已成功创建!是否将该员工新增为管理系统用户?',
|
||||||
|
'创建系统用户',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定新增用户',
|
||||||
|
cancelButtonText: '暂不创建',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
// 确认:进入用户创建表单
|
||||||
|
openUserForm(res.data)
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消:返回列表
|
||||||
|
cancelAdd()
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '操作失败,请稍后重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除员工
|
// 删除员工
|
||||||
const deleteEmployee = async (id) => {
|
const deleteEmployee = async (id) => {
|
||||||
ElMessageBox.confirm('确定删除该员工吗?', '提示', {
|
ElMessageBox.confirm('确定删除该员工吗?', '提示', {
|
||||||
|
confirmButtonText:'确认',
|
||||||
|
cancelButtonText:'取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
|
try {
|
||||||
await deleteEmployeeAPI(id)
|
await deleteEmployeeAPI(id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || '删除失败,请稍后重试')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,13 +274,71 @@ const closeDetail = () => {
|
|||||||
currentDetail.value = null
|
currentDetail.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开用户创建表单(从员工新增后调用)
|
||||||
|
const openUserForm = (employee) => {
|
||||||
|
showAddForm.value = false
|
||||||
|
showSearchResults.value = false
|
||||||
|
showCreateUserForm.value = true
|
||||||
|
// 自动填充员工信息
|
||||||
|
userForm.username = ''
|
||||||
|
userForm.password = ''
|
||||||
|
userForm.name = employee.name
|
||||||
|
userForm.role_id = 2
|
||||||
|
userForm.is_active = 1
|
||||||
|
userForm.employee_id = employee.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户(从员工页面创建)
|
||||||
|
const saveUserFromEmployee = async () => {
|
||||||
|
if (!userForm.username) {
|
||||||
|
ElMessage.warning('请输入用户名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!userForm.password) {
|
||||||
|
ElMessage.warning('请输入密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await createUser({
|
||||||
|
username: userForm.username,
|
||||||
|
password: userForm.password,
|
||||||
|
role_id: userForm.role_id,
|
||||||
|
employee_id: userForm.employee_id,
|
||||||
|
is_active: userForm.is_active
|
||||||
|
})
|
||||||
|
ElMessage.success('用户创建成功')
|
||||||
|
cancelCreateUser()
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消创建用户
|
||||||
|
const cancelCreateUser = () => {
|
||||||
|
showCreateUserForm.value = false
|
||||||
|
justCreatedEmployee.value = null
|
||||||
|
userForm.username = ''
|
||||||
|
userForm.password = ''
|
||||||
|
userForm.name = ''
|
||||||
|
userForm.role_id = 2
|
||||||
|
userForm.is_active = 1
|
||||||
|
userForm.employee_id = null
|
||||||
|
showSearchResults.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const canCreate = computed(() => hasPermission('employee', 'create'))
|
||||||
|
const canUpdate = computed(() => hasPermission('employee', 'update'))
|
||||||
|
const canDelete = computed(() => hasPermission('employee', 'delete'))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="employee-container">
|
<div class="employee-container">
|
||||||
<!-- 搜索栏区域 -->
|
<!-- 搜索栏区域 -->
|
||||||
<div class="employee-search">
|
<div class="employee-search">
|
||||||
<!-- 第一行:搜索框 + 按钮 -->
|
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
||||||
class="employee-search-with-select" @keyup.enter="handleSearch">
|
class="employee-search-with-select" @keyup.enter="handleSearch">
|
||||||
@@ -220,17 +355,13 @@ const closeDetail = () => {
|
|||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<div class="search-buttons">
|
<div class="search-buttons">
|
||||||
<el-button type="success" @click="addEmployee">新增员工</el-button>
|
<el-button type="success" @click="addEmployee" v-if="canCreate">新增员工</el-button>
|
||||||
<el-button type="default" @click="resetSearch">重置</el-button>
|
<el-button type="default" @click="resetSearch">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第二行:筛选条件 -->
|
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">筛选条件:</span>
|
<span class="filter-label">筛选条件:</span>
|
||||||
<el-select v-model="filterDepartment" placeholder="所属部门" clearable style="width: 140px; margin-right: 12px;">
|
|
||||||
<el-option v-for="dept in departmentOptions" :key="dept" :label="dept" :value="dept" />
|
|
||||||
</el-select>
|
|
||||||
<el-select v-model="filterStatus" placeholder="在职状态" clearable style="width: 140px;">
|
<el-select v-model="filterStatus" placeholder="在职状态" clearable style="width: 140px;">
|
||||||
<el-option label="在职" :value="1" />
|
<el-option label="在职" :value="1" />
|
||||||
<el-option label="离职" :value="0" />
|
<el-option label="离职" :value="0" />
|
||||||
@@ -247,8 +378,12 @@ const closeDetail = () => {
|
|||||||
<el-empty description="暂无数据" />
|
<el-empty description="暂无数据" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="employee-table">
|
<div v-else>
|
||||||
<el-table :data="searchResults" v-loading="loading" border style="width: 100%" @row-dblclick="handleRowDblClick">
|
<div class="list-header">
|
||||||
|
<span class="total-count">共找到 {{ searchResults.length }} 条记录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="paginatedData" v-loading="loading" border style="width: 100%" @row-dblclick="handleRowDblClick" row-class-name="clickable-row">
|
||||||
<el-table-column prop="id" label="ID" width="60" />
|
<el-table-column prop="id" label="ID" width="60" />
|
||||||
<el-table-column prop="name" label="姓名" width="80" />
|
<el-table-column prop="name" label="姓名" width="80" />
|
||||||
<el-table-column prop="gender" label="性别" width="60">
|
<el-table-column prop="gender" label="性别" width="60">
|
||||||
@@ -266,7 +401,11 @@ const closeDetail = () => {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="phone" label="联系电话" width="130" />
|
<el-table-column prop="phone" label="联系电话" width="130" />
|
||||||
<el-table-column prop="entry_date" label="入职时间" width="110" />
|
<el-table-column prop="entry_date" label="入职时间" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.entry_date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="70">
|
<el-table-column prop="status" label="状态" width="70">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||||
@@ -276,15 +415,23 @@ const closeDetail = () => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="editEmployee(row)">编辑</el-button>
|
<el-button link type="primary" @click="editEmployee(row)" v-if="canUpdate">编辑</el-button>
|
||||||
<el-button link type="danger" @click="deleteEmployee(row.id)">删除</el-button>
|
<el-button link type="danger" @click="deleteEmployee(row.id)" v-if="canDelete">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div style="margin-top: 10px; color: #909399; font-size: 14px;">
|
<!-- 分页 -->
|
||||||
找到 {{ searchResults.length }} 条结果
|
<div class="pagination-container">
|
||||||
<el-button link type="primary" @click="clearSearch">清空搜索</el-button>
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="searchResults.length"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,6 +530,62 @@ const closeDetail = () => {
|
|||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增用户表单(从员工创建) -->
|
||||||
|
<div v-if="showCreateUserForm" class="employee-info-label">
|
||||||
|
<el-divider content-position="left">为员工创建系统用户</el-divider>
|
||||||
|
<el-form :model="userForm" label-width="auto" style="max-width: 700px" label-position="top">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="关联员工ID">
|
||||||
|
<el-input :model-value="userForm.employee_id" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="员工姓名">
|
||||||
|
<el-input :model-value="userForm.name" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="用户名" required>
|
||||||
|
<el-input v-model="userForm.username" placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="密码" required>
|
||||||
|
<el-input v-model="userForm.password" type="password" show-password placeholder="请输入密码" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="用户角色">
|
||||||
|
<el-select v-model="userForm.role_id" placeholder="请选择角色" style="width: 100%;">
|
||||||
|
<el-option label="普通用户" :value="2" />
|
||||||
|
<el-option label="系统管理员" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="用户状态">
|
||||||
|
<el-radio-group v-model="userForm.is_active">
|
||||||
|
<el-radio :value="1">启用</el-radio>
|
||||||
|
<el-radio :value="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="saveUserFromEmployee">保存用户</el-button>
|
||||||
|
<el-button type="danger" @click="cancelCreateUser">暂不创建</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 员工详情弹窗 -->
|
<!-- 员工详情弹窗 -->
|
||||||
<el-dialog v-model="showDetailDialog" title="员工详情" width="650px" @close="closeDetail">
|
<el-dialog v-model="showDetailDialog" title="员工详情" width="650px" @close="closeDetail">
|
||||||
<div v-if="currentDetail" class="detail-content">
|
<div v-if="currentDetail" class="detail-content">
|
||||||
@@ -399,7 +602,7 @@ const closeDetail = () => {
|
|||||||
<el-descriptions-item label="工资">
|
<el-descriptions-item label="工资">
|
||||||
<span class="amount-text">¥{{ currentDetail.salary?.toLocaleString() }}</span>
|
<span class="amount-text">¥{{ currentDetail.salary?.toLocaleString() }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="入职时间">{{ currentDetail.entry_date }}</el-descriptions-item>
|
<el-descriptions-item label="入职时间">{{ formatDate(currentDetail.entry_date) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="联系电话">{{ currentDetail.phone }}</el-descriptions-item>
|
<el-descriptions-item label="联系电话">{{ currentDetail.phone }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="电子邮箱" :span="2">{{ currentDetail.email }}</el-descriptions-item>
|
<el-descriptions-item label="电子邮箱" :span="2">{{ currentDetail.email }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="在职状态">
|
<el-descriptions-item label="在职状态">
|
||||||
@@ -407,7 +610,7 @@ const closeDetail = () => {
|
|||||||
{{ currentDetail.status === 1 ? '在职' : '离职' }}
|
{{ currentDetail.status === 1 ? '在职' : '离职' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ currentDetail.created_at }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatDateTime(currentDetail.created_at) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">
|
<el-descriptions-item label="备注" :span="2">
|
||||||
{{ currentDetail.remark || '无' }}
|
{{ currentDetail.remark || '无' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -415,7 +618,7 @@ const closeDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="closeDetail">关闭</el-button>
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
<el-button type="primary" @click="closeDetail(); editEmployee(currentDetail)">编辑</el-button>
|
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editEmployee(d) })()" v-if="canUpdate">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -423,5 +626,78 @@ const closeDetail = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.employee-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-search {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e6a23c;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue'
|
import {ref, computed} from 'vue'
|
||||||
import {useRoute} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { getUserInfo, logout, hasPermission } from '../store/user'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const isCollapse = ref(false)
|
const isCollapse = ref(false)
|
||||||
const switchFold = () => {
|
const switchFold = () => {
|
||||||
isCollapse.value = !isCollapse.value
|
isCollapse.value = !isCollapse.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
const userInfo = computed(() => getUserInfo())
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
await logout()
|
||||||
|
router.push('/login')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const checkPermission = (resource, action) => {
|
||||||
|
return hasPermission(resource, action)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -26,11 +49,17 @@ const switchFold = () => {
|
|||||||
<h1 class="logo-title">蜜雪冰城管理系统</h1>
|
<h1 class="logo-title">蜜雪冰城管理系统</h1>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<!-- 右侧退出 -->
|
<!-- 右侧用户信息和退出 -->
|
||||||
<el-menu-item index="/login" class="logout-item">
|
<div class="header-right">
|
||||||
|
<span class="user-info" v-if="userInfo">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
{{ userInfo.name }}({{ userInfo.roleName }})
|
||||||
|
</span>
|
||||||
|
<el-menu-item index="" class="logout-item" @click="handleLogout">
|
||||||
<el-icon><SwitchButton/></el-icon>
|
<el-icon><SwitchButton/></el-icon>
|
||||||
<template #title>退出登录</template>
|
<template #title>退出登录</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
</div>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
@@ -48,32 +77,37 @@ const switchFold = () => {
|
|||||||
<template #title>首页</template>
|
<template #title>首页</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/panel/customer">
|
<el-menu-item index="/panel/customer" v-if="checkPermission('customer', 'read')">
|
||||||
<el-icon><Avatar /></el-icon>
|
<el-icon><Avatar /></el-icon>
|
||||||
<template #title>客户管理</template>
|
<template #title>客户管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/panel/contract">
|
<el-menu-item index="/panel/contract" v-if="checkPermission('contract', 'read')">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
<template #title>合同管理</template>
|
<template #title>合同管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/panel/service">
|
<el-menu-item index="/panel/service" v-if="checkPermission('after_sale', 'read')">
|
||||||
<el-icon><Service /></el-icon>
|
<el-icon><Service /></el-icon>
|
||||||
<template #title>售后管理</template>
|
<template #title>售后管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/panel/products">
|
<el-menu-item index="/panel/products" v-if="checkPermission('product', 'read')">
|
||||||
<el-icon><IceTea /></el-icon>
|
<el-icon><IceTea /></el-icon>
|
||||||
<template #title>产品管理</template>
|
<template #title>产品管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/panel/employee">
|
<el-menu-item index="/panel/supplier" v-if="checkPermission('supplier', 'read')">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
<template #title>供应商管理</template>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/panel/employee" v-if="checkPermission('employee', 'read')">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<template #title>员工管理</template>
|
<template #title>员工管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/panel/user">
|
<el-menu-item index="/panel/user" v-if="checkPermission('user', 'manage')">
|
||||||
<el-icon><UserFilled /></el-icon>
|
<el-icon><UserFilled /></el-icon>
|
||||||
<template #title>用户管理</template>
|
<template #title>用户管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
@@ -142,8 +176,22 @@ const switchFold = () => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-right: 20px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.logout-item {
|
.logout-item {
|
||||||
margin-left: auto !important;
|
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { ref, onMounted, reactive, watch } from 'vue'
|
import { ref, onMounted, reactive, watch, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
getProductsList,
|
getProductsList,
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
updateProducts,
|
updateProducts,
|
||||||
deleteProductsAPI
|
deleteProductsAPI
|
||||||
} from '../api/products'
|
} from '../api/products'
|
||||||
|
import { getSupplierList } from '../api/suppliers'
|
||||||
|
import { hasPermission } from '../store/user'
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -32,6 +34,11 @@ const currentProductId = ref(null)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showDetailDialog = ref(false)
|
const showDetailDialog = ref(false)
|
||||||
const currentDetail = ref(null)
|
const currentDetail = ref(null)
|
||||||
|
const supplierList = ref([])
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
// 产品类型映射
|
// 产品类型映射
|
||||||
const productTypeMap = {
|
const productTypeMap = {
|
||||||
@@ -44,31 +51,67 @@ const productTypeMap = {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
|
fetchSuppliers()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fetchSuppliers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getSupplierList(null, { quiet: true })
|
||||||
|
supplierList.value = res.data?.list || res.data || []
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
// 监听筛选变化
|
// 监听筛选变化
|
||||||
watch(filterType, () => {
|
watch(filterType, () => {
|
||||||
|
currentPage.value = 1
|
||||||
handleSearch()
|
handleSearch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听搜索结果变化,重置页码
|
||||||
|
watch(searchResults, () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页后的数据
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return searchResults.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
// 获取产品列表
|
// 获取产品列表
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getProductsList()
|
const res = await getProductsList()
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
|
// 如果在表单页面,先关闭表单
|
||||||
|
if (showAddForm.value) {
|
||||||
|
cancelAdd()
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getProductsList({
|
const params = {
|
||||||
keyword: search.value,
|
type: filterType.value || undefined
|
||||||
searchField: select.value,
|
}
|
||||||
type: filterType.value
|
if (select.value === '1') {
|
||||||
})
|
params.id = Number(search.value)
|
||||||
searchResults.value = res.data
|
} else if (select.value === '2') {
|
||||||
|
params.name = search.value
|
||||||
|
} else if (select.value === '3') {
|
||||||
|
params.supplier = search.value
|
||||||
|
}
|
||||||
|
const res = await getProductsList(params)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -79,6 +122,7 @@ const resetSearch = () => {
|
|||||||
filterType.value = ''
|
filterType.value = ''
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
showAddForm.value = false
|
showAddForm.value = false
|
||||||
|
currentPage.value = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +181,7 @@ const saveProduct = async () => {
|
|||||||
remark: form.remark
|
remark: form.remark
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateProducts(currentProductId.value, formData)
|
await updateProducts(currentProductId.value, formData)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
@@ -147,16 +192,25 @@ const saveProduct = async () => {
|
|||||||
|
|
||||||
cancelAdd()
|
cancelAdd()
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '操作失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除产品
|
// 删除产品
|
||||||
const deleteProduct = async (id) => {
|
const deleteProduct = async (id) => {
|
||||||
ElMessageBox.confirm('确定删除该产品吗?', '提示', {
|
ElMessageBox.confirm('确定删除该产品吗?', '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
|
try {
|
||||||
await deleteProductsAPI(id)
|
await deleteProductsAPI(id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '删除失败')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,13 +245,22 @@ const getTypeTag = (type) => {
|
|||||||
return map[type] || 'info'
|
return map[type] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const canCreate = computed(() => hasPermission('product', 'create'))
|
||||||
|
const canUpdate = computed(() => hasPermission('product', 'update'))
|
||||||
|
const canDelete = computed(() => hasPermission('product', 'delete'))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="products-container">
|
<div class="products-container">
|
||||||
<!-- 搜索栏区域 -->
|
<!-- 搜索栏区域 -->
|
||||||
<div class="products-search">
|
<div class="products-search">
|
||||||
<!-- 第一行:搜索框 + 按钮 -->
|
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
||||||
class="products-search-with-select" @keyup.enter="handleSearch">
|
class="products-search-with-select" @keyup.enter="handleSearch">
|
||||||
@@ -214,12 +277,11 @@ const getTypeTag = (type) => {
|
|||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<div class="search-buttons">
|
<div class="search-buttons">
|
||||||
<el-button type="success" @click="addProduct">新增产品</el-button>
|
<el-button type="success" @click="addProduct" v-if="canCreate">新增产品</el-button>
|
||||||
<el-button type="default" @click="resetSearch">重置</el-button>
|
<el-button type="default" @click="resetSearch">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第二行:筛选条件 -->
|
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">筛选条件:</span>
|
<span class="filter-label">筛选条件:</span>
|
||||||
<el-select v-model="filterType" placeholder="产品类型" clearable style="width: 140px; margin-right: 12px;">
|
<el-select v-model="filterType" placeholder="产品类型" clearable style="width: 140px; margin-right: 12px;">
|
||||||
@@ -241,8 +303,13 @@ const getTypeTag = (type) => {
|
|||||||
<el-empty description="暂无数据" />
|
<el-empty description="暂无数据" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="products-table">
|
<div v-else>
|
||||||
<el-table :data="searchResults" v-loading="loading" border style="width: 100%" @row-dblclick="handleRowDblClick">
|
<div class="list-header">
|
||||||
|
<span class="total-count">共找到 {{ searchResults.length }} 条记录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="paginatedData" v-loading="loading" border style="width: 100%"
|
||||||
|
@row-dblclick="handleRowDblClick" row-class-name="clickable-row">
|
||||||
<el-table-column prop="id" label="ID" width="70" />
|
<el-table-column prop="id" label="ID" width="70" />
|
||||||
<el-table-column prop="name" label="产品名称" min-width="120" />
|
<el-table-column prop="name" label="产品名称" min-width="120" />
|
||||||
<el-table-column prop="type" label="产品类型" width="110">
|
<el-table-column prop="type" label="产品类型" width="110">
|
||||||
@@ -263,15 +330,23 @@ const getTypeTag = (type) => {
|
|||||||
<el-table-column prop="supplier" label="供应商" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="supplier" label="供应商" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="150" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="editProduct(row)">编辑</el-button>
|
<el-button link type="primary" @click="editProduct(row)" v-if="canUpdate">编辑</el-button>
|
||||||
<el-button link type="danger" @click="deleteProduct(row.id)">删除</el-button>
|
<el-button link type="danger" @click="deleteProduct(row.id)" v-if="canDelete">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div style="margin-top: 10px; color: #909399; font-size: 14px;">
|
<!-- 分页 -->
|
||||||
找到 {{ searchResults.length }} 条结果
|
<div class="pagination-container">
|
||||||
<el-button link type="primary" @click="clearSearch">清空搜索</el-button>
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="searchResults.length"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +386,9 @@ const getTypeTag = (type) => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="供应商">
|
<el-form-item label="供应商">
|
||||||
<el-input v-model="form.supplier" placeholder="请输入供应商名称" />
|
<el-select v-model="form.supplier" placeholder="请选择供应商" style="width: 100%;" filterable clearable>
|
||||||
|
<el-option v-for="s in supplierList" :key="s.id" :label="s.name" :value="s.name" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注">
|
<el-form-item label="备注">
|
||||||
@@ -350,7 +427,7 @@ const getTypeTag = (type) => {
|
|||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="closeDetail">关闭</el-button>
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
<el-button type="primary" @click="closeDetail(); editProduct(currentDetail)">编辑</el-button>
|
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editProduct(d) })()">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -358,5 +435,78 @@ const getTypeTag = (type) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.products-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-search {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e6a23c;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { ref, onMounted, reactive, watch, nextTick } from 'vue'
|
import { ref, onMounted, reactive, watch, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
getServiceList,
|
getServiceList,
|
||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
deleteServiceAPI
|
deleteServiceAPI
|
||||||
} from '../api/service'
|
} from '../api/service'
|
||||||
import { getCustomerList } from '../api/customer'
|
import { getCustomerList } from '../api/customer'
|
||||||
import { getEmployeeList } from '../api/employee'
|
import { getSimpleEmployeeList } from '../api/employee'
|
||||||
|
import { hasPermission } from '../store/user'
|
||||||
|
import { formatDateTime, formatDate } from '../utils/date'
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
@@ -36,10 +38,14 @@ const currentDetail = ref(null)
|
|||||||
const customerList = ref([])
|
const customerList = ref([])
|
||||||
const employeeList = ref([])
|
const employeeList = ref([])
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
// 状态映射
|
// 状态映射
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
'待处理': 'warning',
|
'待处理': 'warning',
|
||||||
'处理中': '',
|
'处理中': 'primary',
|
||||||
'已完成': 'success'
|
'已完成': 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +54,18 @@ onMounted(() => {
|
|||||||
fetchOptions()
|
fetchOptions()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取客户和员工列表供下拉选择
|
// 获取客户和员工列表供下拉选择(仅运营部员工可跟进售后)
|
||||||
const fetchOptions = async () => {
|
const fetchOptions = async () => {
|
||||||
const [cRes, eRes] = await Promise.all([getCustomerList(), getEmployeeList()])
|
const results = await Promise.allSettled([
|
||||||
customerList.value = cRes.data
|
getCustomerList(null, { quiet: true }),
|
||||||
employeeList.value = eRes.data
|
getSimpleEmployeeList({ department: '运营部' }, { quiet: true })
|
||||||
|
])
|
||||||
|
if (results[0].status === 'fulfilled') {
|
||||||
|
customerList.value = results[0].value.data?.list || results[0].value.data || []
|
||||||
|
}
|
||||||
|
if (results[1].status === 'fulfilled') {
|
||||||
|
employeeList.value = results[1].value.data || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听状态变化:待处理时清空业务员
|
// 监听状态变化:待处理时清空业务员
|
||||||
@@ -64,27 +77,57 @@ watch(() => form.handle_status, (val) => {
|
|||||||
|
|
||||||
// 监听筛选变化
|
// 监听筛选变化
|
||||||
watch(filterStatus, () => {
|
watch(filterStatus, () => {
|
||||||
|
currentPage.value = 1
|
||||||
handleSearch()
|
handleSearch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听搜索结果变化,重置页码
|
||||||
|
watch(searchResults, () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页后的数据
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return searchResults.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
// 获取售后列表
|
// 获取售后列表
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getServiceList()
|
const res = await getServiceList()
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
|
// 如果在表单页面,先关闭表单
|
||||||
|
if (showAddForm.value) {
|
||||||
|
cancelAdd()
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getServiceList({
|
const params = {
|
||||||
keyword: search.value,
|
handle_status: filterStatus.value || undefined
|
||||||
searchField: select.value,
|
}
|
||||||
handle_status: filterStatus.value
|
if (select.value === '1') {
|
||||||
})
|
params.id = Number(search.value)
|
||||||
searchResults.value = res.data
|
} else if (select.value === '2') {
|
||||||
|
params.customer_id = search.value
|
||||||
|
} else if (select.value === '3') {
|
||||||
|
params.feedback = search.value
|
||||||
|
} else if (select.value === '4') {
|
||||||
|
params.customer_name = search.value
|
||||||
|
}
|
||||||
|
const res = await getServiceList(params)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -95,6 +138,7 @@ const resetSearch = () => {
|
|||||||
filterStatus.value = ''
|
filterStatus.value = ''
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
showAddForm.value = false
|
showAddForm.value = false
|
||||||
|
currentPage.value = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +200,7 @@ const saveService = async () => {
|
|||||||
remark: form.remark
|
remark: form.remark
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateService(currentServiceId.value, formData)
|
await updateService(currentServiceId.value, formData)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
@@ -166,16 +211,25 @@ const saveService = async () => {
|
|||||||
|
|
||||||
cancelAdd()
|
cancelAdd()
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message || '操作失败,请重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除售后
|
// 删除售后
|
||||||
const deleteService = async (id) => {
|
const deleteService = async (id) => {
|
||||||
ElMessageBox.confirm('确定删除该售后记录吗?', '提示', {
|
ElMessageBox.confirm('确定删除该售后记录吗?', '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
|
try {
|
||||||
await deleteServiceAPI(id)
|
await deleteServiceAPI(id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message || '删除失败,请重试')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,20 +252,30 @@ const closeDetail = () => {
|
|||||||
currentDetail.value = null
|
currentDetail.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const canCreate = computed(() => hasPermission('after_sale', 'create'))
|
||||||
|
const canUpdate = computed(() => hasPermission('after_sale', 'update'))
|
||||||
|
const canDelete = computed(() => hasPermission('after_sale', 'delete'))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="service-container">
|
<div class="service-container">
|
||||||
<!-- 搜索栏区域 -->
|
<!-- 搜索栏区域 -->
|
||||||
<div class="service-search">
|
<div class="service-search">
|
||||||
<!-- 第一行:搜索框 + 按钮 -->
|
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
||||||
class="service-search-with-select" @keyup.enter="handleSearch">
|
class="service-search-with-select" @keyup.enter="handleSearch">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<el-select v-model="select" placeholder="请选择" style="width: 115px">
|
<el-select v-model="select" placeholder="请选择" style="width: 115px">
|
||||||
<el-option label="ID" value="1" />
|
<el-option label="编号" value="1" />
|
||||||
<el-option label="客户ID" value="2" />
|
<el-option label="客户ID" value="2" />
|
||||||
|
<el-option label="客户姓名" value="4" />
|
||||||
<el-option label="反馈内容" value="3" />
|
<el-option label="反馈内容" value="3" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
@@ -221,12 +285,11 @@ const closeDetail = () => {
|
|||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<div class="search-buttons">
|
<div class="search-buttons">
|
||||||
<el-button type="success" @click="addService">新增售后</el-button>
|
<el-button type="success" @click="addService" v-if="canCreate">新增售后</el-button>
|
||||||
<el-button type="default" @click="resetSearch">重置</el-button>
|
<el-button type="default" @click="resetSearch">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第二行:筛选条件 -->
|
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">筛选条件:</span>
|
<span class="filter-label">筛选条件:</span>
|
||||||
<el-select v-model="filterStatus" placeholder="处理状态" clearable style="width: 140px;">
|
<el-select v-model="filterStatus" placeholder="处理状态" clearable style="width: 140px;">
|
||||||
@@ -246,9 +309,14 @@ const closeDetail = () => {
|
|||||||
<el-empty description="暂无数据" />
|
<el-empty description="暂无数据" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="service-table">
|
<div v-else>
|
||||||
<el-table :data="searchResults" v-loading="loading" border style="width: 100%" @row-dblclick="handleRowDblClick">
|
<div class="list-header">
|
||||||
<el-table-column prop="id" label="ID" width="70" />
|
<span class="total-count">共找到 {{ searchResults.length }} 条记录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="paginatedData" v-loading="loading" border style="width: 100%"
|
||||||
|
@row-dblclick="handleRowDblClick" row-class-name="clickable-row">
|
||||||
|
<el-table-column prop="id" label="编号" width="70" />
|
||||||
<el-table-column prop="customer_id" label="客户ID" width="90" />
|
<el-table-column prop="customer_id" label="客户ID" width="90" />
|
||||||
<el-table-column prop="feedback" label="反馈内容" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="feedback" label="反馈内容" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column prop="employee_id" label="业务员ID" width="100">
|
<el-table-column prop="employee_id" label="业务员ID" width="100">
|
||||||
@@ -263,19 +331,31 @@ const closeDetail = () => {
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="service_date" label="售后日期" width="120" />
|
<el-table-column prop="service_date" label="售后日期" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.service_date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="handle_method" label="处理方式" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="handle_method" label="处理方式" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="150" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="editService(row)">编辑</el-button>
|
<el-button link type="primary" @click="editService(row)" v-if="canUpdate">编辑</el-button>
|
||||||
<el-button link type="danger" @click="deleteService(row.id)">删除</el-button>
|
<el-button link type="danger" @click="deleteService(row.id)" v-if="canDelete">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div style="margin-top: 10px; color: #909399; font-size: 14px;">
|
<!-- 分页 -->
|
||||||
找到 {{ searchResults.length }} 条结果
|
<div class="pagination-container">
|
||||||
<el-button link type="primary" @click="clearSearch">清空搜索</el-button>
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="searchResults.length"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,7 +383,8 @@ const closeDetail = () => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="售后日期" required>
|
<el-form-item label="售后日期" required>
|
||||||
<el-date-picker v-model="form.service_date" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" style="width: 100%;" />
|
<el-date-picker v-model="form.service_date" type="date" value-format="YYYY-MM-DD"
|
||||||
|
placeholder="请选择日期" style="width: 100%;" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="处理业务员">
|
<el-form-item label="处理业务员">
|
||||||
@@ -333,7 +414,7 @@ const closeDetail = () => {
|
|||||||
<div v-if="currentDetail" class="detail-content">
|
<div v-if="currentDetail" class="detail-content">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="售后编号">{{ currentDetail.id }}</el-descriptions-item>
|
<el-descriptions-item label="售后编号">{{ currentDetail.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="售后日期">{{ currentDetail.service_date }}</el-descriptions-item>
|
<el-descriptions-item label="售后日期">{{ formatDate(currentDetail.service_date) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户ID">{{ currentDetail.customer_id }}</el-descriptions-item>
|
<el-descriptions-item label="客户ID">{{ currentDetail.customer_id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="业务员ID">{{ currentDetail.employee_id || '未分配' }}</el-descriptions-item>
|
<el-descriptions-item label="业务员ID">{{ currentDetail.employee_id || '未分配' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="处理状态">
|
<el-descriptions-item label="处理状态">
|
||||||
@@ -341,7 +422,7 @@ const closeDetail = () => {
|
|||||||
{{ currentDetail.handle_status }}
|
{{ currentDetail.handle_status }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ currentDetail.created_at }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatDateTime(currentDetail.created_at) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="反馈内容" :span="2">
|
<el-descriptions-item label="反馈内容" :span="2">
|
||||||
<div class="content-text">{{ currentDetail.feedback }}</div>
|
<div class="content-text">{{ currentDetail.feedback }}</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -355,7 +436,7 @@ const closeDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="closeDetail">关闭</el-button>
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
<el-button type="primary" @click="closeDetail(); editService(currentDetail)">编辑</el-button>
|
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editService(d) })()">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -363,5 +444,79 @@ const closeDetail = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.service-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-search {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
466
src/components/supplier.vue
Normal file
466
src/components/supplier.vue
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
import { ref, onMounted, reactive, watch, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getSupplierList,
|
||||||
|
createSupplier,
|
||||||
|
updateSupplier,
|
||||||
|
deleteSupplierAPI
|
||||||
|
} from '../api/suppliers'
|
||||||
|
import { hasPermission } from '../store/user'
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
contact: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
type: '',
|
||||||
|
content: '',
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const search = ref('')
|
||||||
|
const select = ref('1')
|
||||||
|
const filterType = ref('')
|
||||||
|
const filterStatus = ref('')
|
||||||
|
const searchResults = ref([])
|
||||||
|
const showSearchResults = ref(false)
|
||||||
|
const showAddForm = ref(false)
|
||||||
|
const isEditMode = ref(false)
|
||||||
|
const currentSupplierId = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const showDetailDialog = ref(false)
|
||||||
|
const currentDetail = ref(null)
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
// 供应商类型
|
||||||
|
const supplierTypes = ['原材料', '包装', '设备']
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听筛选变化
|
||||||
|
watch([filterType, filterStatus], () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
handleSearch()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听搜索结果变化
|
||||||
|
watch(searchResults, () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页后的数据
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return searchResults.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取供应商列表
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const res = await getSupplierList()
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
|
showSearchResults.value = true
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = async () => {
|
||||||
|
// 如果在表单页面,先关闭表单
|
||||||
|
if (showAddForm.value) {
|
||||||
|
cancelAdd()
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
type: filterType.value || undefined,
|
||||||
|
status: filterStatus.value !== '' ? filterStatus.value : undefined
|
||||||
|
}
|
||||||
|
// 根据搜索类型添加参数
|
||||||
|
if (search.value) {
|
||||||
|
if (select.value === '1') {
|
||||||
|
params.id = Number(search.value)
|
||||||
|
} else if (select.value === '2') {
|
||||||
|
params.name = search.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await getSupplierList(params)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
|
showSearchResults.value = true
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
const resetForm = () => {
|
||||||
|
form.name = ''
|
||||||
|
form.contact = ''
|
||||||
|
form.phone = ''
|
||||||
|
form.address = ''
|
||||||
|
form.type = ''
|
||||||
|
form.content = ''
|
||||||
|
form.status = 1
|
||||||
|
isEditMode.value = false
|
||||||
|
currentSupplierId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示新增表单
|
||||||
|
const addSupplier = () => {
|
||||||
|
resetForm()
|
||||||
|
showAddForm.value = true
|
||||||
|
showSearchResults.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const editSupplier = (row) => {
|
||||||
|
isEditMode.value = true
|
||||||
|
currentSupplierId.value = row.id
|
||||||
|
form.name = row.name
|
||||||
|
form.contact = row.contact
|
||||||
|
form.phone = row.phone
|
||||||
|
form.address = row.address
|
||||||
|
form.type = row.type
|
||||||
|
form.content = row.content
|
||||||
|
form.status = row.status
|
||||||
|
showAddForm.value = true
|
||||||
|
showSearchResults.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存供应商
|
||||||
|
const saveSupplier = async () => {
|
||||||
|
if (!form.name) {
|
||||||
|
ElMessage.warning('请输入供应商名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
name: form.name,
|
||||||
|
contact: form.contact,
|
||||||
|
phone: form.phone,
|
||||||
|
address: form.address,
|
||||||
|
type: form.type,
|
||||||
|
content: form.content,
|
||||||
|
status: form.status
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEditMode.value) {
|
||||||
|
await updateSupplier(currentSupplierId.value, formData)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createSupplier(formData)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAdd()
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除供应商
|
||||||
|
const deleteSupplier = async (id) => {
|
||||||
|
ElMessageBox.confirm('确定删除该供应商吗?', '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
await deleteSupplierAPI(id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '删除失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const cancelAdd = () => {
|
||||||
|
resetForm()
|
||||||
|
showAddForm.value = false
|
||||||
|
showSearchResults.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空搜索
|
||||||
|
const clearSearch = () => {
|
||||||
|
search.value = ''
|
||||||
|
filterType.value = ''
|
||||||
|
filterStatus.value = ''
|
||||||
|
showSearchResults.value = false
|
||||||
|
showAddForm.value = false
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双击行查看详情
|
||||||
|
const handleRowDblClick = (row) => {
|
||||||
|
currentDetail.value = { ...row }
|
||||||
|
showDetailDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭详情弹窗
|
||||||
|
const closeDetail = () => {
|
||||||
|
showDetailDialog.value = false
|
||||||
|
currentDetail.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const canCreate = computed(() => hasPermission('supplier', 'create'))
|
||||||
|
const canUpdate = computed(() => hasPermission('supplier', 'update'))
|
||||||
|
const canDelete = computed(() => hasPermission('supplier', 'delete'))
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="supplier-container">
|
||||||
|
<!-- 搜索栏区域 -->
|
||||||
|
<div class="supplier-search">
|
||||||
|
<div class="search-row">
|
||||||
|
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
||||||
|
class="supplier-search-with-select" @keyup.enter="handleSearch">
|
||||||
|
<template #prepend>
|
||||||
|
<el-select v-model="select" placeholder="请选择" style="width: 115px">
|
||||||
|
<el-option label="ID" value="1" />
|
||||||
|
<el-option label="名称" value="2" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<el-button :icon="Search" @click="handleSearch" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<div class="search-buttons">
|
||||||
|
<el-button type="success" @click="addSupplier" v-if="canCreate">新增供应商</el-button>
|
||||||
|
<el-button type="default" @click="clearSearch">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-label">筛选条件:</span>
|
||||||
|
<el-select v-model="filterType" placeholder="供应商类型" clearable style="width: 140px; margin-right: 12px;">
|
||||||
|
<el-option v-for="type in supplierTypes" :key="type" :label="type" :value="type" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="filterStatus" placeholder="状态" clearable style="width: 140px;">
|
||||||
|
<el-option label="正常" :value="1" />
|
||||||
|
<el-option label="停用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showSearchResults" class="supplier-list">
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<el-skeleton :rows="5" animated />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="searchResults.length === 0" class="empty-container">
|
||||||
|
<el-empty description="暂无数据" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="list-header">
|
||||||
|
<span class="total-count">共找到 {{ searchResults.length }} 条记录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="paginatedData" v-loading="loading" border style="width: 100%"
|
||||||
|
@row-dblclick="handleRowDblClick" row-class-name="clickable-row">
|
||||||
|
<el-table-column prop="id" label="ID" width="70" />
|
||||||
|
<el-table-column prop="name" label="供应商名称" min-width="150" />
|
||||||
|
<el-table-column prop="contact" label="联系人" width="100" />
|
||||||
|
<el-table-column prop="phone" label="联系电话" width="130" />
|
||||||
|
<el-table-column prop="type" label="类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag>{{ row.type || '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="address" label="地址" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ row.status === 1 ? '正常' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="editSupplier(row)" v-if="canUpdate">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="deleteSupplier(row.id)" v-if="canDelete">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="searchResults.length"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑供应商表单 -->
|
||||||
|
<div v-if="showAddForm" class="supplier-info-label">
|
||||||
|
<el-divider content-position="left">{{ isEditMode ? '编辑供应商信息' : '新增供应商' }}</el-divider>
|
||||||
|
<el-form :model="form" label-width="auto" style="max-width: 600px" label-position="top">
|
||||||
|
<el-form-item label="供应商名称" required>
|
||||||
|
<el-input v-model="form.name" placeholder="请输入供应商名称" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="联系人">
|
||||||
|
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="联系电话">
|
||||||
|
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="地址">
|
||||||
|
<el-input v-model="form.address" placeholder="请输入地址" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="类型">
|
||||||
|
<el-select v-model="form.type" placeholder="请选择类型" style="width: 100%;">
|
||||||
|
<el-option v-for="type in supplierTypes" :key="type" :label="type" :value="type" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注说明">
|
||||||
|
<el-input v-model="form.content" type="textarea" :rows="3" placeholder="请输入备注说明" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" required>
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :value="1">正常</el-radio>
|
||||||
|
<el-radio :value="0">停用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="saveSupplier">保存</el-button>
|
||||||
|
<el-button type="danger" @click="cancelAdd">取消</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 供应商详情弹窗 -->
|
||||||
|
<el-dialog v-model="showDetailDialog" title="供应商详情" width="650px" @close="closeDetail">
|
||||||
|
<div v-if="currentDetail" class="detail-content">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="供应商ID">{{ currentDetail.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="供应商名称">{{ currentDetail.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人">{{ currentDetail.contact || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话">{{ currentDetail.phone || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="类型">
|
||||||
|
<el-tag>{{ currentDetail.type || '无' }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="currentDetail.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ currentDetail.status === 1 ? '正常' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="地址" :span="2">{{ currentDetail.address || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注说明" :span="2">{{ currentDetail.content || '无' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editSupplier(d) })()" v-if="canUpdate">编辑</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.supplier-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplier-search {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { Search, Lock } from '@element-plus/icons-vue'
|
import { Search, Lock } from '@element-plus/icons-vue'
|
||||||
import { ref, onMounted, reactive, watch } from 'vue'
|
import { ref, onMounted, reactive, watch, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
getUserList,
|
getUserList,
|
||||||
@@ -9,13 +9,17 @@ import {
|
|||||||
updateUser,
|
updateUser,
|
||||||
deleteUserAPI
|
deleteUserAPI
|
||||||
} from '../api/user'
|
} from '../api/user'
|
||||||
|
import { getEmployeeList } from '../api/employee'
|
||||||
|
import { hasPermission } from '../store/user'
|
||||||
|
import { formatDateTime } from '../utils/date'
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
name: '',
|
role_id: 2,
|
||||||
user_type: 'user',
|
employee_id: null,
|
||||||
status: '1'
|
department: '',
|
||||||
|
is_active: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
@@ -31,34 +35,91 @@ const loading = ref(false)
|
|||||||
const showDetailDialog = ref(false)
|
const showDetailDialog = ref(false)
|
||||||
const currentDetail = ref(null)
|
const currentDetail = ref(null)
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
// 角色列表
|
||||||
|
const roleList = ref([
|
||||||
|
{ id: 1, name: 'admin', description: '系统管理员' },
|
||||||
|
{ id: 2, name: 'general_manager', description: '总经理' },
|
||||||
|
{ id: 3, name: 'franchise_manager', description: '招商经理' },
|
||||||
|
{ id: 4, name: 'operations_manager', description: '运营经理' },
|
||||||
|
{ id: 5, name: 'procurement_manager', description: '采购经理' },
|
||||||
|
{ id: 6, name: 'finance', description: '财务人员' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 员工列表
|
||||||
|
const employeeList = ref([])
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
|
fetchEmployees()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听筛选变化,自动触发查询
|
// 监听筛选变化
|
||||||
watch([filterType, filterStatus], () => {
|
watch([filterType, filterStatus], () => {
|
||||||
|
currentPage.value = 1
|
||||||
handleSearch()
|
handleSearch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听搜索结果变化
|
||||||
|
watch(searchResults, () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页后的数据
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return searchResults.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取员工列表
|
||||||
|
const fetchEmployees = async () => {
|
||||||
|
const res = await getEmployeeList()
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
employeeList.value = res.data.list || res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户列表
|
// 获取用户列表
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getUserList()
|
const res = await getUserList()
|
||||||
searchResults.value = res.data
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
|
// 如果在表单页面,先关闭表单
|
||||||
|
if (showAddForm.value) {
|
||||||
|
cancelAdd()
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await getUserList({
|
const params = {
|
||||||
keyword: search.value,
|
role_id: filterType.value || undefined,
|
||||||
searchField: select.value,
|
is_active: filterStatus.value !== '' ? filterStatus.value : undefined
|
||||||
user_type: filterType.value,
|
}
|
||||||
status: filterStatus.value
|
// 根据搜索类型添加参数
|
||||||
})
|
if (search.value) {
|
||||||
searchResults.value = res.data
|
if (select.value === '1') {
|
||||||
|
params.username = search.value
|
||||||
|
} else if (select.value === '2') {
|
||||||
|
params.real_name = search.value
|
||||||
|
} else if (select.value === '3') {
|
||||||
|
params.id = Number(search.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await getUserList(params)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
searchResults.value = res.data.list || res.data
|
||||||
|
}
|
||||||
showSearchResults.value = true
|
showSearchResults.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -67,9 +128,10 @@ const handleSearch = async () => {
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
form.username = ''
|
form.username = ''
|
||||||
form.password = ''
|
form.password = ''
|
||||||
form.name = ''
|
form.role_id = 2
|
||||||
form.user_type = 'user'
|
form.employee_id = null
|
||||||
form.status = '1'
|
form.department = ''
|
||||||
|
form.is_active = 1
|
||||||
isEditMode.value = false
|
isEditMode.value = false
|
||||||
currentUserId.value = null
|
currentUserId.value = null
|
||||||
}
|
}
|
||||||
@@ -87,20 +149,31 @@ const editUser = (row) => {
|
|||||||
currentUserId.value = row.id
|
currentUserId.value = row.id
|
||||||
form.username = row.username
|
form.username = row.username
|
||||||
form.password = ''
|
form.password = ''
|
||||||
form.name = row.name
|
form.role_id = row.role_id
|
||||||
form.user_type = row.user_type
|
form.employee_id = row.employee_id
|
||||||
form.status = row.status
|
form.department = row.department
|
||||||
|
form.is_active = row.is_active
|
||||||
showAddForm.value = true
|
showAddForm.value = true
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存用户
|
// 保存用户
|
||||||
const saveUser = async () => {
|
const saveUser = async () => {
|
||||||
|
if (!form.username) {
|
||||||
|
ElMessage.warning('请输入用户名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isEditMode.value && !form.password) {
|
||||||
|
ElMessage.warning('请输入密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
username: form.username,
|
username: form.username,
|
||||||
name: form.name,
|
role_id: form.role_id,
|
||||||
user_type: form.user_type,
|
employee_id: form.employee_id,
|
||||||
status: form.status
|
department: form.department,
|
||||||
|
is_active: form.is_active
|
||||||
}
|
}
|
||||||
if (!isEditMode.value) {
|
if (!isEditMode.value) {
|
||||||
formData.password = form.password
|
formData.password = form.password
|
||||||
@@ -108,6 +181,7 @@ const saveUser = async () => {
|
|||||||
formData.password = form.password
|
formData.password = form.password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateUser(currentUserId.value, formData)
|
await updateUser(currentUserId.value, formData)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
@@ -118,16 +192,25 @@ const saveUser = async () => {
|
|||||||
|
|
||||||
cancelAdd()
|
cancelAdd()
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '操作失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除用户
|
// 删除用户
|
||||||
const deleteUser = async (id) => {
|
const deleteUser = async (id) => {
|
||||||
ElMessageBox.confirm('确定删除该用户吗?', '提示', {
|
ElMessageBox.confirm('确定删除该用户吗?', '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
|
try {
|
||||||
await deleteUserAPI(id)
|
await deleteUserAPI(id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
fetchData()
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || error.message || '删除失败')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +228,7 @@ const clearSearch = () => {
|
|||||||
filterStatus.value = ''
|
filterStatus.value = ''
|
||||||
showSearchResults.value = false
|
showSearchResults.value = false
|
||||||
showAddForm.value = false
|
showAddForm.value = false
|
||||||
|
currentPage.value = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,19 +244,36 @@ const closeDetail = () => {
|
|||||||
currentDetail.value = null
|
currentDetail.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 员工选择变化
|
||||||
|
const handleEmployeeChange = (empId) => {
|
||||||
|
const emp = employeeList.value.find(e => e.id === empId)
|
||||||
|
if (emp) {
|
||||||
|
form.department = emp.department
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const canManage = computed(() => hasPermission('user', 'manage'))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="user-container">
|
<div class="user-container">
|
||||||
<!-- 搜索栏区域 -->
|
<!-- 搜索栏区域 -->
|
||||||
<div class="user-search">
|
<div class="user-search">
|
||||||
<el-input v-model="search" style="max-width: 600px" placeholder="Please input"
|
<div class="search-row">
|
||||||
|
<el-input v-model="search" style="max-width: 500px" placeholder="请输入搜索关键词"
|
||||||
class="user-search-with-select" @keyup.enter="handleSearch">
|
class="user-search-with-select" @keyup.enter="handleSearch">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<el-select v-model="select" placeholder="Select" style="width: 115px">
|
<el-select v-model="select" placeholder="请选择" style="width: 115px">
|
||||||
<el-option label="用户名" value="1" />
|
<el-option label="用户名" value="1" />
|
||||||
<el-option label="编号" value="2" />
|
<el-option label="真实姓名" value="2" />
|
||||||
<el-option label="真实姓名" value="3" />
|
<el-option label="ID" value="3" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
@@ -180,25 +281,22 @@ const closeDetail = () => {
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<el-button type="default" @click="clearSearch" style="margin-left:20px">
|
<div class="search-buttons">
|
||||||
重置
|
<el-button color="#E60012" @click="addUser" v-if="canManage">新增用户</el-button>
|
||||||
</el-button>
|
<el-button type="default" @click="clearSearch">重置</el-button>
|
||||||
<el-button color="#E60012" @click="addUser" style="margin-left: 20px">
|
</div>
|
||||||
新增用户
|
</div>
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<span class="filter-label">筛选条件:</span>
|
<span class="filter-label">筛选条件:</span>
|
||||||
<el-select v-model="filterType" placeholder="用户类型" clearable style="width: 140px; margin-right: 12px;">
|
<el-select v-model="filterType" placeholder="用户角色" clearable style="width: 140px; margin-right: 12px;">
|
||||||
<el-option label="管理员" value="admin" />
|
<el-option v-for="role in roleList" :key="role.id" :label="role.description" :value="role.id" />
|
||||||
<el-option label="普通用户" value="user" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-model="filterStatus" placeholder="用户状态" clearable style="width: 140px;">
|
<el-select v-model="filterStatus" placeholder="用户状态" clearable style="width: 140px;">
|
||||||
<el-option label="启用" value="1" />
|
<el-option label="启用" :value="1" />
|
||||||
<el-option label="封禁中" value="0" />
|
<el-option label="禁用" :value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showSearchResults" class="user-list">
|
<div v-if="showSearchResults" class="user-list">
|
||||||
@@ -210,28 +308,43 @@ const closeDetail = () => {
|
|||||||
<el-empty description="暂无数据" />
|
<el-empty description="暂无数据" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="list-header">
|
||||||
|
<span class="total-count">共找到 {{ searchResults.length }} 条记录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else class="user-table">
|
<el-table :data="paginatedData" v-loading="loading" border style="width: 100%"
|
||||||
<el-table :data="searchResults" v-loading="loading" border style="width: 100%" @row-dblclick="handleRowDblClick">
|
@row-dblclick="handleRowDblClick" row-class-name="clickable-row">
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="username" label="用户名" width="120" />
|
<el-table-column prop="username" label="用户名" width="120" />
|
||||||
<el-table-column prop="name" label="真实姓名" width="120" />
|
<el-table-column prop="real_name" label="真实姓名" width="120">
|
||||||
<el-table-column prop="createdate" label="创建时间" width="200" />
|
|
||||||
<el-table-column prop="user_type" label="用户类型" width="120">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.user_type === 'admin' ? 'danger' : 'info'">
|
{{ row.real_name || '-' }}
|
||||||
{{ row.user_type === 'admin' ? '管理员' : '普通用户' }}
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="role_name" label="角色" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.role_description || row.role_name }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="department" label="部门" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.department || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="is_active" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_active === 1 ? 'success' : 'danger'">
|
||||||
|
{{ row.is_active === 1 ? '启用' : '禁用' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="用户状态" width="120">
|
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.status === '0' ? 'danger' : 'info'">
|
{{ formatDateTime(row.created_at) }}
|
||||||
{{ row.status === '0' ? '封禁中' : '启用' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right" v-if="canManage">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="editUser(row)">编辑</el-button>
|
<el-button link type="primary" @click="editUser(row)">编辑</el-button>
|
||||||
<el-button link type="danger" @click="deleteUser(row.id)">删除</el-button>
|
<el-button link type="danger" @click="deleteUser(row.id)">删除</el-button>
|
||||||
@@ -239,42 +352,55 @@ const closeDetail = () => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div style="margin-top: 10px; color: #909399; font-size: 14px;">
|
<!-- 分页 -->
|
||||||
找到 {{ searchResults.length }} 条结果
|
<div class="pagination-container">
|
||||||
<el-button link type="primary" @click="clearSearch">清空搜索</el-button>
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="searchResults.length"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
prev-text="上一页"
|
||||||
|
next-text="下一页"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 新增用户表单 -->
|
<!-- 新增用户表单 -->
|
||||||
<div v-if="showAddForm" class="user-info-label">
|
<div v-if="showAddForm" class="user-info-label">
|
||||||
<el-divider content-position="left">{{ isEditMode ? '编辑用户信息' : '新增用户' }}</el-divider>
|
<el-divider content-position="left">{{ isEditMode ? '编辑用户信息' : '新增用户' }}</el-divider>
|
||||||
<el-form :model="form" label-width="auto" style="max-width: 600px" label-position="top">
|
<el-form :model="form" label-width="auto" style="max-width: 600px" label-position="top">
|
||||||
<el-form-item label="用户名" required>
|
<el-form-item label="用户名" required>
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
<el-input v-model="form.username" placeholder="请输入用户名" :disabled="isEditMode" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="密码" :required="!isEditMode">
|
<el-form-item label="密码" :required="!isEditMode">
|
||||||
<el-input v-model="form.password" :prefix-icon="Lock" clearable placeholder="请输入密码"
|
<el-input v-model="form.password" :prefix-icon="Lock" clearable :placeholder="isEditMode ? '留空则不修改' : '请输入密码'" show-password
|
||||||
show-password type="password" />
|
type="password" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="真实姓名" required>
|
<el-form-item label="关联员工">
|
||||||
<el-input v-model="form.name" placeholder=" 请输入用户真实姓名" />
|
<el-select v-model="form.employee_id" placeholder="请选择关联员工" style="width: 100%;" filterable clearable
|
||||||
|
@change="handleEmployeeChange">
|
||||||
|
<el-option v-for="emp in employeeList" :key="emp.id" :label="`${emp.id} - ${emp.name}`" :value="emp.id" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="用户类型" required>
|
<el-form-item label="所属部门">
|
||||||
<el-radio-group v-model="form.user_type">
|
<el-input v-model="form.department" placeholder="选择员工后自动填充" />
|
||||||
<el-radio value="admin">管理员</el-radio>
|
</el-form-item>
|
||||||
<el-radio value="user">普通用户</el-radio>
|
|
||||||
</el-radio-group>
|
<el-form-item label="用户角色" required>
|
||||||
|
<el-select v-model="form.role_id" placeholder="请选择角色" style="width: 100%;">
|
||||||
|
<el-option v-for="role in roleList" :key="role.id" :label="role.description" :value="role.id" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="用户状态" required>
|
<el-form-item label="用户状态" required>
|
||||||
<el-radio-group v-model="form.status">
|
<el-radio-group v-model="form.is_active">
|
||||||
<el-radio value="0">禁用</el-radio>
|
<el-radio :value="1">启用</el-radio>
|
||||||
<el-radio value="1">启用</el-radio>
|
<el-radio :value="0">禁用</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -292,23 +418,21 @@ const closeDetail = () => {
|
|||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="用户ID">{{ currentDetail.id }}</el-descriptions-item>
|
<el-descriptions-item label="用户ID">{{ currentDetail.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="用户名">{{ currentDetail.username }}</el-descriptions-item>
|
<el-descriptions-item label="用户名">{{ currentDetail.username }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="真实姓名">{{ currentDetail.name }}</el-descriptions-item>
|
<el-descriptions-item label="真实姓名">{{ currentDetail.real_name || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ currentDetail.createdate }}</el-descriptions-item>
|
<el-descriptions-item label="角色">{{ currentDetail.role_description || currentDetail.role_name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="用户类型">
|
<el-descriptions-item label="部门">{{ currentDetail.department || '-' }}</el-descriptions-item>
|
||||||
<el-tag :type="currentDetail.user_type === 'admin' ? 'danger' : 'info'">
|
<el-descriptions-item label="关联员工">{{ currentDetail.employee_id || '-' }}</el-descriptions-item>
|
||||||
{{ currentDetail.user_type === 'admin' ? '管理员' : '普通用户' }}
|
<el-descriptions-item label="状态">
|
||||||
</el-tag>
|
<el-tag :type="currentDetail.is_active === 1 ? 'success' : 'danger'">
|
||||||
</el-descriptions-item>
|
{{ currentDetail.is_active === 1 ? '启用' : '禁用' }}
|
||||||
<el-descriptions-item label="用户状态">
|
|
||||||
<el-tag :type="currentDetail.status === '0' ? 'danger' : 'info'">
|
|
||||||
{{ currentDetail.status === '0' ? '封禁中' : '启用' }}
|
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDateTime(currentDetail.created_at) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="closeDetail">关闭</el-button>
|
<el-button @click="closeDetail">关闭</el-button>
|
||||||
<el-button type="primary" @click="closeDetail(); editUser(currentDetail)">编辑</el-button>
|
<el-button type="primary" @click="(() => { const d = currentDetail; closeDetail(); editUser(d) })()" v-if="canManage">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -316,5 +440,72 @@ const closeDetail = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.user-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Service from "./components/service.vue";
|
|||||||
import Products from "./components/products.vue";
|
import Products from "./components/products.vue";
|
||||||
import Employee from "./components/employee.vue";
|
import Employee from "./components/employee.vue";
|
||||||
import User from "./components/user.vue";
|
import User from "./components/user.vue";
|
||||||
|
import Supplier from "./components/supplier.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: "/", redirect: "/login" },
|
{ path: "/", redirect: "/login" },
|
||||||
@@ -18,12 +19,13 @@ const routes = [
|
|||||||
redirect: "/panel/home",
|
redirect: "/panel/home",
|
||||||
children: [
|
children: [
|
||||||
{ path: "home", component: Home },
|
{ path: "home", component: Home },
|
||||||
{ path: "customer", component: Customer },
|
{ path: "customer", component: Customer, meta: { permission: 'customer:read' } },
|
||||||
{ path: "contract", component: Contract },
|
{ path: "contract", component: Contract, meta: { permission: 'contract:read' } },
|
||||||
{ path: "service", component: Service },
|
{ path: "service", component: Service, meta: { permission: 'after_sale:read' } },
|
||||||
{ path: "products", component: Products },
|
{ path: "products", component: Products, meta: { permission: 'product:read' } },
|
||||||
{ path: "employee", component: Employee },
|
{ path: "employee", component: Employee, meta: { permission: 'employee:read' } },
|
||||||
{ path: "user", component: User }
|
{ path: "user", component: User, meta: { permission: 'user:manage' } },
|
||||||
|
{ path: "supplier", component: Supplier, meta: { permission: 'supplier:read' } }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -32,4 +34,39 @@ const router = createRouter({
|
|||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 全局前置守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
// 登录页
|
||||||
|
if (to.path === '/login') {
|
||||||
|
if (token) {
|
||||||
|
// 已登录,跳转到首页
|
||||||
|
next('/panel/home')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他页面需要登录
|
||||||
|
if (!token) {
|
||||||
|
next('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查路由权限
|
||||||
|
if (to.meta.permission) {
|
||||||
|
const [resource, action] = to.meta.permission.split(':')
|
||||||
|
const userInfo = JSON.parse(localStorage.getItem('userInfo') || 'null')
|
||||||
|
if (!userInfo?.permissions?.includes(to.meta.permission)) {
|
||||||
|
next('/panel/home')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
72
src/store/user.js
Normal file
72
src/store/user.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import request from '../api/request'
|
||||||
|
|
||||||
|
// 用户状态
|
||||||
|
const state = reactive({
|
||||||
|
token: localStorage.getItem('token') || '',
|
||||||
|
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否已登录
|
||||||
|
const isLoggedIn = ref(!!state.token)
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
export const login = async (username, password) => {
|
||||||
|
const res = await request.post('/user/login', { username, password })
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
state.token = res.data.token
|
||||||
|
state.userInfo = res.data.userInfo
|
||||||
|
isLoggedIn.value = true
|
||||||
|
localStorage.setItem('token', res.data.token)
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
export const logout = async () => {
|
||||||
|
try {
|
||||||
|
await request.post('/user/logout')
|
||||||
|
} catch (e) {
|
||||||
|
// 即使请求失败也清除本地状态
|
||||||
|
}
|
||||||
|
state.token = ''
|
||||||
|
state.userInfo = null
|
||||||
|
isLoggedIn.value = false
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
export const fetchUserInfo = async () => {
|
||||||
|
const res = await request.get('/user/info')
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
state.userInfo = res.data
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(res.data))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有某个权限
|
||||||
|
export const hasPermission = (resource, action) => {
|
||||||
|
if (!state.userInfo?.permissions) return false
|
||||||
|
const permission = `${resource}:${action}`
|
||||||
|
return state.userInfo.permissions.includes(permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
export const getUserInfo = () => state.userInfo
|
||||||
|
|
||||||
|
// 获取 token
|
||||||
|
export const getToken = () => state.token
|
||||||
|
|
||||||
|
export default {
|
||||||
|
state,
|
||||||
|
isLoggedIn,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
fetchUserInfo,
|
||||||
|
hasPermission,
|
||||||
|
getUserInfo,
|
||||||
|
getToken
|
||||||
|
}
|
||||||
32
src/utils/date.js
Normal file
32
src/utils/date.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 格式化日期时间(带时间)
|
||||||
|
export const formatDateTime = (dateStr) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (isNaN(date.getTime())) return dateStr
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
} catch {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期(只显示日期部分)
|
||||||
|
export const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (isNaN(date.getTime())) return dateStr
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
} catch {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,13 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@@ -72,6 +72,16 @@
|
|||||||
resolved "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz"
|
resolved "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz"
|
||||||
integrity sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==
|
integrity sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu@1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz"
|
||||||
|
integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl@1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz"
|
||||||
|
integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==
|
||||||
|
|
||||||
"@rolldown/binding-win32-x64-msvc@1.0.3":
|
"@rolldown/binding-win32-x64-msvc@1.0.3":
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz"
|
||||||
@@ -279,6 +289,16 @@ fdir@^6.5.0:
|
|||||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
||||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz"
|
||||||
|
integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz"
|
||||||
|
integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
|
||||||
|
|
||||||
lightningcss-win32-x64-msvc@1.32.0:
|
lightningcss-win32-x64-msvc@1.32.0:
|
||||||
version "1.32.0"
|
version "1.32.0"
|
||||||
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz"
|
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user