测试暂未发现问题

This commit is contained in:
ChoChoX
2026-06-25 21:49:55 +08:00
parent 5cd569797c
commit 984d5b8118
26 changed files with 2414 additions and 636 deletions

View File

@@ -1,41 +1,26 @@
import axios from 'axios' import request from './request'
import { mockContractAPI } from './mock/contract'
// 开关true 用 mockfalse 用真实接口
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)
} }

View File

@@ -1,41 +1,26 @@
import axios from 'axios' import request from './request'
import { mockCustomerAPI } from './mock/customer'
// 开关true 用 mockfalse 用真实接口
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)
} }

View File

@@ -1,41 +1,31 @@
import axios from 'axios' import request from './request'
import { mockEmployeeAPI } from './mock/employee'
// 开关true 用 mockfalse 用真实接口
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)
} }

View File

@@ -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)
) )

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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: '用户不存在' }
} }

View File

@@ -1,41 +1,26 @@
import axios from 'axios' import request from './request'
import { mockProductsAPI } from './mock/products'
// 开关true 用 mockfalse 用真实接口
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
View 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

View File

@@ -1,41 +1,26 @@
import axios from 'axios' import request from './request'
import { mockServiceAPI } from './mock/service'
// 开关true 用 mockfalse 用真实接口
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
View 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}`)
}

View File

@@ -1,41 +1,31 @@
import axios from 'axios' import request from './request'
import { mockUserAPI } from './mock/user'
// 开关true 用 mockfalse 用真实接口 // 获取用户列表(管理员)
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)
} }

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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
View 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
View 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
}
}

View File

@@ -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
}
}
}
}) })

View File

@@ -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"