减少冗余代码
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
|
|||||||
@@ -1,78 +1,80 @@
|
|||||||
import request from '@/utils/request'
|
import { adminRequest, indexRequest } from '@/utils/request'
|
||||||
|
|
||||||
|
// ==================== 前台接口(/api/index) ====================
|
||||||
|
|
||||||
export const userApi = {
|
export const userApi = {
|
||||||
// 登录接口
|
|
||||||
login(username, password) {
|
login(username, password) {
|
||||||
// 这里的 data 会被 axios 放在请求体 (Body) 中发送
|
return indexRequest.post('/login', { username, password })
|
||||||
return request.post('/login', {
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 退出登录
|
|
||||||
logout() {
|
logout() {
|
||||||
return request.post('/logout')
|
return indexRequest.post('/logout')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 后台接口(/api/admin) ====================
|
||||||
|
|
||||||
export const ticketApi = {
|
export const ticketApi = {
|
||||||
//获取工单各个完成状态的统计数量
|
// 获取工单各完成状态的统计数量
|
||||||
getticketsstatuscount(){
|
getticketdashboardstats() {
|
||||||
return request.get('/getticketstatuscount')
|
return adminRequest.get('/getticketdashboardstats')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取工单列表
|
// 获取工单列表
|
||||||
getTickets(params) {
|
getTickets(params) {
|
||||||
return request.get('/ticketsfind', { params })
|
return adminRequest.get('/ticketsfind', { params })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 新增工单
|
// 新增工单
|
||||||
addTicket(data) {
|
addTicket(data) {
|
||||||
return request.post('/addticket', data)
|
return adminRequest.post('/addticket', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新工单
|
// 更新工单
|
||||||
updateTicket(id, data) {
|
updateTicket(id, data) {
|
||||||
return request.put(`/ticket/${id}`, data)
|
return adminRequest.put(`/ticket/${id}`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除工单
|
// 删除工单
|
||||||
deleteTicket(id) {
|
deleteTicket(id) {
|
||||||
return request.delete(`/ticket/${id}`)
|
return adminRequest.delete(`/ticket/${id}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取运营人员列表
|
// 获取运营人员列表(按公司分组)
|
||||||
getOperators() {
|
getOperatorsNew() {
|
||||||
return request.get('/operators')
|
return adminRequest.get('/getOperatorList')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取员工列表
|
// 获取员工列表
|
||||||
getStaffs(params) {
|
getUserStaffs() {
|
||||||
return request.get('/getStaffList', { params })
|
return adminRequest.get('/getUserStaffList')
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// 批量更新工单
|
||||||
|
batchUpdateTicket(data) {
|
||||||
|
return adminRequest.put('/tickets/batch-update', data)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customerApi = {
|
export const customerApi = {
|
||||||
getCustomers() {
|
getCustomers() {
|
||||||
return request.get('/customers')
|
return adminRequest.get('/customers')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// 添加客户,data 结构应为 { company_name: 'xxx' }
|
// 添加客户,data 结构应为 { company_name: 'xxx' }
|
||||||
addCustomerName(data) {
|
addCustomerName(data) {
|
||||||
return request.post('/addcustomername', data)
|
return adminRequest.post('/addcustomername', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
addEmployee(data) {
|
addEmployee(data) {
|
||||||
return request.post('/addemployee', data) // 对应后端新增员工
|
return adminRequest.post('/addemployee', data)
|
||||||
},
|
},
|
||||||
deleteEmployee(empId) {
|
|
||||||
return request.delete(`/deleteemployee/${empId}`) // 对应后端删除员工
|
|
||||||
},
|
|
||||||
deleteCustomer(customerId) {
|
|
||||||
return request.delete(`/deletecustomer/${customerId}`) // 删除客户单位
|
|
||||||
}
|
|
||||||
|
|
||||||
|
deleteEmployee(empId) {
|
||||||
|
return adminRequest.delete(`/deleteemployee/${empId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteCustomer(customerId) {
|
||||||
|
return adminRequest.delete(`/deletecustomer/${customerId}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,24 +15,26 @@
|
|||||||
|
|
||||||
<div class="input-group-row">
|
<div class="input-group-row">
|
||||||
<el-form-item label="运营人员">
|
<el-form-item label="运营人员">
|
||||||
<el-select v-model="localFilters.operator_name" placeholder="全部运营人员" clearable filterable
|
<el-select v-model="localFilters.operator_id" placeholder="全部运营人员" clearable filterable
|
||||||
class="wide-select" @change="handleSearch">
|
class="wide-select" @change="handleSearch">
|
||||||
<el-option v-for="op in operators" :key="op.id" :label="`${op.name} (${op.company})`"
|
<el-option-group v-for="group in groupedOperators" :key="group.company_name"
|
||||||
:value="op.name" />
|
:label="group.company_name">
|
||||||
|
<el-option v-for="op in group.operators" :key="op.id" :label="`${op.operator_name}`" :value="op.id" />
|
||||||
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div class="divider-wrapper">/</div>
|
<div class="divider-wrapper">/</div>
|
||||||
|
|
||||||
<el-form-item label="我方负责人">
|
<el-form-item label="我方牛马">
|
||||||
<el-select v-model="localFilters.staff_id" placeholder="全部负责人" clearable filterable class="wide-select"
|
<el-select v-model="localFilters.user_id" placeholder="全部牛马" clearable filterable class="wide-select"
|
||||||
@change="handleSearch">
|
@change="handleSearch">
|
||||||
<el-option v-for="staff in staffs" :key="staff.id" :label="staff.name" :value="staff.id" />
|
<el-option v-for="staff in staffs" :key="staff.id" :label="staff.username" :value="staff.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="关键词" class="search-keyword">
|
<el-form-item label="关键词" class="search-keyword">
|
||||||
<el-input v-model="localFilters.search" placeholder="搜索网站、运营、备注..." clearable @keyup.enter="handleSearch"
|
<el-input v-model="localFilters.search" placeholder="搜索网站、备注" clearable @keyup.enter="handleSearch"
|
||||||
class="wide-search-input">
|
class="wide-search-input">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, watch } from "vue";
|
import { reactive, watch, computed } from "vue";
|
||||||
import { Search, Sort } from "@element-plus/icons-vue";
|
import { Search, Sort } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -75,9 +77,51 @@ const props = defineProps({
|
|||||||
staffs: { type: Array, default: () => [] },
|
staffs: { type: Array, default: () => [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["filter-change", "reset"]);
|
const emit = defineEmits(["filter-change", "reset"]); // 先声明事件,后使用
|
||||||
|
|
||||||
|
const localFilters = reactive({ ...props.filters }); // ...(浅拷贝)
|
||||||
|
|
||||||
|
// 公司优先级排序(可以在这里调整顺序)
|
||||||
|
const getCompanyPriority = (companyName) => {
|
||||||
|
const priority = {
|
||||||
|
第一客户: 1,
|
||||||
|
第三客户: 2,
|
||||||
|
流玖电商: 3,
|
||||||
|
};
|
||||||
|
return priority[companyName] || 999;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分组并排序的运营人员
|
||||||
|
const groupedOperators = computed(() => {
|
||||||
|
// 1. 先按公司分组
|
||||||
|
const groups = {};
|
||||||
|
props.operators.forEach((op) => {
|
||||||
|
if (!groups[op.company_name]) {
|
||||||
|
groups[op.company_name] = [];
|
||||||
|
}
|
||||||
|
groups[op.company_name].push(op);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 对公司分组进行排序(按优先级 + 名称)
|
||||||
|
const sortedGroups = Object.entries(groups)
|
||||||
|
.map(([company_name, operators]) => ({
|
||||||
|
company_name,
|
||||||
|
operators: [...operators].sort((a, b) =>
|
||||||
|
a.operator_name.localeCompare(b.operator_name, "zh-CN")
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const priorityA = getCompanyPriority(a.company_name);
|
||||||
|
const priorityB = getCompanyPriority(b.company_name);
|
||||||
|
if (priorityA !== priorityB) {
|
||||||
|
return priorityA - priorityB;
|
||||||
|
}
|
||||||
|
return a.company_name.localeCompare(b.company_name, "zh-CN");
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedGroups;
|
||||||
|
});
|
||||||
|
|
||||||
const localFilters = reactive({ ...props.filters });
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.filters,
|
() => props.filters,
|
||||||
@@ -197,4 +241,12 @@ const setSort = (order) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 运营人员分组标题样式 */
|
||||||
|
:deep(.el-select-group__title) {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
destroy-on-close @close="handleClose">
|
destroy-on-close @close="handleClose">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="compact-form">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="compact-form">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-title">业务基础信息</div>
|
<div class="section-title">基础信息</div>
|
||||||
<el-form-item label="网站地址" prop="website">
|
<el-form-item label="网站地址" prop="website">
|
||||||
<el-input v-model="form.website" placeholder="请输入网站 URL" clearable>
|
<el-input v-model="form.website" placeholder="输入URL" clearable>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Link />
|
<Link />
|
||||||
@@ -16,34 +16,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section people-grid">
|
<div class="form-section people-grid">
|
||||||
<div class="section-title column-full">权责分配</div>
|
<div class="section-title column-full">人员选择</div>
|
||||||
|
|
||||||
<el-form-item label="客户单位 (外方)" prop="customer_id" class="client-item">
|
<el-form-item label="客户单位" prop="company_id" class="client-item">
|
||||||
<el-select v-model="form.customer_id" placeholder="请选择单位" style="width: 100%" filterable
|
<el-select v-model="form.company_id" placeholder="请选择单位" style="width: 100%" filterable
|
||||||
@change="handleCustomerChange">
|
@change="handleCustomerChange">
|
||||||
<el-option v-for="item in customers" :key="item.customer_id" :label="item.company_name"
|
<el-option v-for="item in companiesWithOperators.filter(c => c.id !== 0)" :key="item.id" :label="item.company_name"
|
||||||
:value="item.customer_id" />
|
:value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="运营人员 (外方)" prop="operator" class="client-item">
|
<el-form-item label="运营人员" prop="operator_id" class="client-item">
|
||||||
<el-select v-model="form.operator" placeholder="请选择人员" style="width: 100%" filterable
|
<el-select v-model="form.operator_id" placeholder="请选择人员" style="width: 100%" filterable
|
||||||
:disabled="!form.customer_id">
|
:disabled="!form.company_id">
|
||||||
<el-option v-for="item in filteredEmployees" :key="item.employee_id" :label="item.employee_name"
|
<el-option v-for="item in (companiesWithOperators.find(c => c.id === form.company_id)?.companyoperators || []).filter(op => op.id !== 0)" :key="item.id" :label="item.operator_name"
|
||||||
:value="item.employee_name" />
|
:value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="isEditing" label="负责人 (我方)" prop="staff_id" class="owner-item column-full">
|
<el-form-item label="我方牛马" prop="user_id" class="owner-item column-full">
|
||||||
<el-select v-model="form.staff_id" placeholder="请选择我方负责人" style="width: 100%" filterable>
|
<el-select v-model="form.user_id" placeholder="请选择牛马" style="width: 100%" filterable>
|
||||||
<el-option v-for="item in staffs" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option v-for="item in userStaffs" :key="item.id" :label="item.username" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<el-form-item label="当前进度">
|
<el-form-item label="进度">
|
||||||
<el-radio-group v-model="form.is_complete" class="modern-radio-group">
|
<el-radio-group v-model="form.status" class="modern-radio-group">
|
||||||
<el-radio-button label="未分配">未分配</el-radio-button>
|
<el-radio-button label="未分配">未分配</el-radio-button>
|
||||||
<el-radio-button label="进行中">进行中</el-radio-button>
|
<el-radio-button label="进行中">进行中</el-radio-button>
|
||||||
<el-radio-button label="已完成">已完成</el-radio-button>
|
<el-radio-button label="已完成">已完成</el-radio-button>
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注说明" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写相关的补充信息或注意事项..." resize="none" />
|
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写注意事项" resize="none" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -77,10 +77,9 @@ const emit = defineEmits(["update:modelValue", "submit"]);
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: Boolean,
|
modelValue: Boolean,
|
||||||
formData: Object,
|
formData: Object,
|
||||||
customers: Array,
|
userStaffs: Array,
|
||||||
customerEmployees: Array,
|
|
||||||
staffs: Array,
|
|
||||||
isEditing: Boolean,
|
isEditing: Boolean,
|
||||||
|
companiesWithOperators: Array,
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 响应式变量 ---
|
// --- 响应式变量 ---
|
||||||
@@ -91,10 +90,10 @@ const submitting = ref(false);
|
|||||||
// 核心表单对象:customer_id 已包含在内
|
// 核心表单对象:customer_id 已包含在内
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
website: "",
|
website: "",
|
||||||
operator: "",
|
company_id: null,
|
||||||
customer_id: null,
|
operator_id: null,
|
||||||
staff_id: null,
|
user_id: null,
|
||||||
is_complete: "未分配",
|
status: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,24 +101,14 @@ const form = reactive({
|
|||||||
const rules = computed(() => {
|
const rules = computed(() => {
|
||||||
const baseRules = {
|
const baseRules = {
|
||||||
website: [{ required: true, message: "请输入网站地址", trigger: "blur" }],
|
website: [{ required: true, message: "请输入网站地址", trigger: "blur" }],
|
||||||
customer_id: [{ required: true, message: "请选择客户单位", trigger: "change" }],
|
company_id: [{ required: true, message: "请选择客户单位", trigger: "change" }],
|
||||||
operator: [{ required: true, message: "请选择运营人员", trigger: "change" }],
|
|
||||||
};
|
};
|
||||||
if (props.isEditing) {
|
|
||||||
baseRules.staff_id = [{ required: true, message: "请选择负责人", trigger: "change" }];
|
|
||||||
}
|
|
||||||
return baseRules;
|
return baseRules;
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 逻辑:根据选择的单位过滤人员 ---
|
|
||||||
const filteredEmployees = computed(() => {
|
|
||||||
if (!form.customer_id) return [];
|
|
||||||
return props.customerEmployees.filter((emp) => emp.customer_id === form.customer_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 单位切换时,重置运营人员选择
|
// 单位切换时,重置运营人员选择
|
||||||
const handleCustomerChange = () => {
|
const handleCustomerChange = () => {
|
||||||
form.operator = "";
|
form.operator_id = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 监听弹窗与回显 ---
|
// --- 监听弹窗与回显 ---
|
||||||
@@ -134,13 +123,13 @@ watch(
|
|||||||
// 深度合并数据
|
// 深度合并数据
|
||||||
Object.assign(form, props.formData);
|
Object.assign(form, props.formData);
|
||||||
|
|
||||||
// 回显补充:如果传入的 formData 没有带 customer_id,通过 operator 反查补全
|
// 回显补充:如果传入的 formData 没有带 company_id,通过 operator_id 反查补全
|
||||||
if (!form.customer_id && props.formData.operator) {
|
if (!form.company_id && props.formData.operator_id) {
|
||||||
const employee = props.customerEmployees.find(
|
const employee = props.companiesWithOperators.flatMap(c => c.companyoperators).find(
|
||||||
(e) => e.employee_name === props.formData.operator
|
(e) => e.id === props.formData.operator_id
|
||||||
);
|
);
|
||||||
if (employee) {
|
if (employee) {
|
||||||
form.customer_id = employee.customer_id;
|
form.company_id = employee.customer_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,10 +148,10 @@ const resetForm = () => {
|
|||||||
if (formRef.value) formRef.value.resetFields();
|
if (formRef.value) formRef.value.resetFields();
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
website: "",
|
website: "",
|
||||||
operator: "",
|
company_id: null,
|
||||||
customer_id: null,
|
operator_id: null,
|
||||||
staff_id: null,
|
user_id: null,
|
||||||
is_complete: "未分配",
|
status: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -178,10 +167,17 @@ const handleSubmit = async () => {
|
|||||||
// 复制提交数据,如果是新增,确保 staff_id 为空
|
// 复制提交数据,如果是新增,确保 staff_id 为空
|
||||||
const submitData = { ...form };
|
const submitData = { ...form };
|
||||||
|
|
||||||
|
// 核心修复:如果状态是”已完结”,确保 lock_time 不会被意外修改。
|
||||||
|
// 显式地使用表单打开前的原始 lock_time 值。
|
||||||
|
if (submitData.status === “已完结”) {
|
||||||
|
// 使用 props.formData.lock_time 来保证是原始值
|
||||||
|
submitData.lock_time = props.formData.lock_time || null;
|
||||||
|
}
|
||||||
|
|
||||||
delete submitData.create_time;
|
delete submitData.create_time;
|
||||||
delete submitData.update_time;
|
delete submitData.update_time;
|
||||||
if (!props.isEditing) {
|
if (!props.isEditing) {
|
||||||
submitData.staff_id = null;
|
submitData.user_id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("submit", submitData);
|
emit("submit", submitData);
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-card class="compact-card" shadow="never">
|
<el-card class="compact-card" shadow="never">
|
||||||
<el-table :data="tickets" v-loading="loading" header-cell-class-name="table-header" style="width: 100%">
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
|
:data="tickets"
|
||||||
|
v-loading="loading"
|
||||||
|
header-cell-class-name="table-header"
|
||||||
|
style="width: 100%"
|
||||||
|
@selection-change="$emit('selection-change', $event)"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="50" />
|
||||||
<el-table-column prop="website" label="网站地址" min-width="180">
|
<el-table-column prop="website" label="网站地址" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" :href="row.website" target="_blank" class="site-link">
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
:href="formatUrl(row.website)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="site-link"
|
||||||
|
>
|
||||||
{{ row.website }}
|
{{ row.website }}
|
||||||
</el-link>
|
</el-link>
|
||||||
</template>
|
</template>
|
||||||
@@ -32,17 +45,28 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="company_name"
|
||||||
|
label="所属客户"
|
||||||
|
min-width="120"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.company_name || "未关联" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="协同人员" width="180">
|
<el-table-column label="协同人员" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="staff-wrapper">
|
<div class="staff-wrapper">
|
||||||
<div class="staff-item client">
|
<div class="staff-item client">
|
||||||
<span class="staff-role">运营</span>
|
<span class="staff-role">运营</span>
|
||||||
<span class="staff-name">{{ row.operator?.trim() || '未指派' }}</span>
|
<span class="staff-name">{{ row.operator_name?.trim() || "未指派" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="staff-item internal">
|
<div class="staff-item internal">
|
||||||
<span class="staff-role">负责</span>
|
<span class="staff-role">负责</span>
|
||||||
<span class="staff-name">
|
<span class="staff-name">
|
||||||
{{ row.principalStaff?.name || '未指派' }}
|
{{ row.username?.trim() || "未指派" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,17 +75,22 @@
|
|||||||
|
|
||||||
<el-table-column label="备注" min-width="150">
|
<el-table-column label="备注" min-width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tooltip v-if="row.remark" effect="dark" :content="row.remark" placement="top">
|
<el-tooltip
|
||||||
|
v-if="row.remark"
|
||||||
|
effect="dark"
|
||||||
|
:content="row.remark"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<span class="remark-text">{{ row.remark }}</span>
|
<span class="remark-text">{{ row.remark }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<span v-else class="empty-text">-</span>
|
<span v-else class="empty-text">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="is_complete" label="状态" width="90" align="center">
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="status-badge" :style="getStatusStyle(row.is_complete)">
|
<span class="status-badge" :style="getStatusStyle(row.status)">
|
||||||
{{ row.is_complete }}
|
{{ statusLabel(row.status) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -73,21 +102,17 @@
|
|||||||
type="success"
|
type="success"
|
||||||
size="small"
|
size="small"
|
||||||
effect="light"
|
effect="light"
|
||||||
>已锁定</el-tag>
|
>已锁定</el-tag
|
||||||
<el-tag
|
>
|
||||||
v-else
|
<el-tag v-else type="info" size="small" effect="plain">未锁定</el-tag>
|
||||||
type="info"
|
|
||||||
size="small"
|
|
||||||
effect="plain"
|
|
||||||
>未锁定</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="时间节点" width="160">
|
<el-table-column label="时间节点" width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="time-wrapper">
|
<div class="time-wrapper">
|
||||||
<span>创建:{{ formatTime(row.create_time) }}</span>
|
<span>创建:{{ formatDateTime(row.create_time) }}</span>
|
||||||
<span class="update-label">更新:{{ formatTime(row.update_time) }}</span>
|
<span class="update-label">更新:{{ formatDateTime(row.update_time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -113,40 +138,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { formatUrl, formatDateTime } from "@/utils/format";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
tickets: { type: Array, default: () => [] },
|
tickets: { type: Array, default: () => [] },
|
||||||
loading: { type: Boolean, default: false }
|
loading: { type: Boolean, default: false },
|
||||||
})
|
});
|
||||||
|
|
||||||
defineEmits(['edit', 'delete'])
|
defineEmits(["edit", "delete", "selection-change"]);
|
||||||
|
|
||||||
const formatTime = (time) => {
|
const tableRef = ref(null);
|
||||||
if (!time) return '-';
|
|
||||||
if (typeof time === 'string' && time.includes('-')) return time;
|
|
||||||
|
|
||||||
const date = new Date(time);
|
defineExpose({
|
||||||
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
clearSelection() {
|
||||||
const d = date.getDate().toString().padStart(2, '0');
|
tableRef.value?.clearSelection();
|
||||||
const h = date.getHours().toString().padStart(2, '0');
|
},
|
||||||
const i = date.getMinutes().toString().padStart(2, '0');
|
});
|
||||||
return `${m}-${d} ${h}:${i}`;
|
|
||||||
}
|
const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" };
|
||||||
|
|
||||||
|
const statusLabel = (status) => statusMap[status] || "未知";
|
||||||
|
|
||||||
const getStatusStyle = (status) => {
|
const getStatusStyle = (status) => {
|
||||||
const styles = {
|
const styles = {
|
||||||
"未分配": { color: '#909399', background: '#f4f4f5' },
|
0: { color: "#909399", background: "#f4f4f5" },
|
||||||
"进行中": { color: '#409EFF', background: '#ecf5ff' },
|
1: { color: "#409EFF", background: "#ecf5ff" },
|
||||||
"已完成": { color: '#67C23A', background: '#f0f9eb' },
|
2: { color: "#67C23A", background: "#f0f9eb" },
|
||||||
"已完结": { color: '#e6a23c', background: '#fdf6ec' }
|
3: { color: "#e6a23c", background: "#fdf6ec" },
|
||||||
}
|
};
|
||||||
return styles[status] || styles["未分配"];
|
return styles[status] || styles[0];
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.progress-container {
|
.progress-container {
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 调整进度条内部文字大小 */
|
/* 调整进度条内部文字大小 */
|
||||||
:deep(.el-progress__text) {
|
:deep(.el-progress__text) {
|
||||||
font-size: 11px !important;
|
font-size: 11px !important;
|
||||||
@@ -158,6 +187,7 @@ const getStatusStyle = (status) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.staff-item {
|
.staff-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -166,6 +196,7 @@ const getStatusStyle = (status) => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.staff-role {
|
.staff-role {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -176,19 +207,34 @@ const getStatusStyle = (status) => {
|
|||||||
background: #909399;
|
background: #909399;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.staff-name {
|
.staff-name {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
.staff-item.client { background: #f4f4f5; color: #909399; border: 1px solid #e9e9eb; }
|
|
||||||
.staff-item.internal { background: #ecf5ff; color: #409eff; border: 1px solid #d9ecff; }
|
.staff-item.client {
|
||||||
.staff-item.internal .staff-role { background: #409eff; }
|
background: #f4f4f5;
|
||||||
|
color: #909399;
|
||||||
|
border: 1px solid #e9e9eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staff-item.internal {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
border: 1px solid #d9ecff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staff-item.internal .staff-role {
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
.data-num-text {
|
.data-num-text {
|
||||||
font-family: 'Monaco', monospace;
|
font-family: "Monaco", monospace;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remark-text {
|
.remark-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
@@ -197,9 +243,17 @@ const getStatusStyle = (status) => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.empty-text { color: #c0c4cc; font-size: 12px; }
|
|
||||||
|
|
||||||
.compact-card { border-radius: 8px; border: 1px solid #ebeef5; }
|
.empty-text {
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
@@ -208,12 +262,20 @@ const getStatusStyle = (status) => {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border-radius: 11px;
|
border-radius: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-wrapper {
|
.time-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
.update-label { color: #909399; }
|
|
||||||
:deep(.table-header) { background-color: #fafafa !important; font-weight: 600; }
|
.update-label {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.table-header) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,25 +3,36 @@
|
|||||||
<el-container class="full-screen-container">
|
<el-container class="full-screen-container">
|
||||||
<el-aside width="200px" class="siderbar">
|
<el-aside width="200px" class="siderbar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="logo-container">
|
<!-- 修复:将 logo 区域包装成一个正确的外部链接 -->
|
||||||
<img src="@/assets/logo.jpg" alt="方舟网络" class="logo-image" />
|
<a
|
||||||
<div class="text-content">
|
href="https://www.example.com"
|
||||||
<h2 class="company-name">方舟网络</h2>
|
target="_blank"
|
||||||
<p class="system-name">工单管理系统</p>
|
rel="noopener noreferrer"
|
||||||
|
class="logo-link"
|
||||||
|
>
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="@/assets/logo.jpg" alt="方舟网络" class="logo-image" />
|
||||||
|
<div class="text-content">
|
||||||
|
<h2 class="company-name">方舟网络</h2>
|
||||||
|
<p class="system-name">工单管理系统</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-menu default-active="/" class="sidebar-menu" router>
|
<el-menu default-active="/" class="sidebar-menu" router>
|
||||||
<el-menu-item index="/">
|
<el-menu-item index="/">
|
||||||
<el-icon><icon-menu /></el-icon>
|
<el-icon><icon-menu /></el-icon>
|
||||||
<span>工单管理</span>
|
<span>工单管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/stats">
|
<el-menu-item index="/stats">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<document />
|
<document />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>数据统计</span>
|
<span>数据统计</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item index="/customers">
|
<el-menu-item index="/customers">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<User />
|
<User />
|
||||||
@@ -66,6 +77,8 @@
|
|||||||
import { reactive, onMounted } from "vue";
|
import { reactive, onMounted } from "vue";
|
||||||
import { Menu as IconMenu, Document, User } from "@element-plus/icons-vue";
|
import { Menu as IconMenu, Document, User } from "@element-plus/icons-vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import { resetRouter } from "@/router";
|
||||||
|
import { userApi } from "@/api/ticket";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const user = reactive({
|
const user = reactive({
|
||||||
@@ -82,10 +95,19 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
localStorage.clear();
|
// 1. 调用后端接口销毁 session
|
||||||
sessionStorage.clear();
|
try {
|
||||||
router.push("/login");
|
await userApi.logout();
|
||||||
|
} catch (e) {
|
||||||
|
// 即使后端接口异常,也要完成前端的清理
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem("user_info");
|
||||||
|
sessionStorage.removeItem("user_info");
|
||||||
|
|
||||||
|
resetRouter();
|
||||||
|
router.push({ name: "login" });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -119,6 +141,13 @@ const handleLogout = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 修复:为新添加的链接添加样式 */
|
||||||
|
.logo-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,105 +1,220 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus' // 引入提示组件
|
import { ElMessage } from 'element-plus' // 引入提示组件
|
||||||
|
|
||||||
|
|
||||||
|
// 公开路由,所有用户都可以访问
|
||||||
|
export const publicRoutes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('@/views/LoginView.vue'),
|
||||||
|
meta: { isPublic: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 动态路由,根据角色进行添加
|
||||||
|
export const dynamicRoutes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'admin_layout',
|
||||||
|
component: () => import('@/layouts/MainLayout.vue'),
|
||||||
|
meta: { role: 'admin' },
|
||||||
|
redirect: { name: 'home' }, // 添加默认重定向
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('@/views/TicketManagement.vue'),
|
||||||
|
meta: { title: '工单管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'stats',
|
||||||
|
name: 'stats',
|
||||||
|
component: () => import('@/views/StatsView.vue'),
|
||||||
|
meta: { title: '数据统计' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customers',
|
||||||
|
name: 'customers',
|
||||||
|
component: () => import('@/views/CustomersManagement.vue'),
|
||||||
|
meta: { title: '客户管理' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
name: 'user_workdesk',
|
||||||
|
component: () => import('@/views/UserView.vue'),
|
||||||
|
meta: { role: 'user', title: '工作台' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/client',
|
||||||
|
name: 'client_monitor',
|
||||||
|
component: () => import('@/views/ClientView.vue'),
|
||||||
|
meta: { role: 'client', title: '客户监控' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 创建路由实例
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: publicRoutes
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: () => import('../layouts/MainLayout.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'home',
|
|
||||||
component: () => import('../views/TicketManagement.vue'),
|
|
||||||
// 标记需要 admin 权限
|
|
||||||
meta: { requiresAuth: true, role: 'admin' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'stats',
|
|
||||||
name: 'stats',
|
|
||||||
component: () => import('../views/StatsView.vue'),
|
|
||||||
meta: { requiresAuth: true, role: 'admin' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'customers',
|
|
||||||
name: 'customers',
|
|
||||||
component: () => import('../views/CustomersManagement.vue'),
|
|
||||||
meta: { requiresAuth: true, role: 'admin' }
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/user',
|
|
||||||
name: 'user_workdesk',
|
|
||||||
component: () => import('../views/UserView.vue'),
|
|
||||||
// 标记只需要普通用户权限
|
|
||||||
meta: { requiresAuth: true, role: 'user' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/client',
|
|
||||||
name: 'client_monitor',
|
|
||||||
component: () => import('../views/ClientView.vue'),
|
|
||||||
// 标记只需要普通用户权限
|
|
||||||
meta: { requiresAuth: true, role: 'client' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
name: 'login',
|
|
||||||
component: () => import('../views/LoginView.vue')
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 全局前置守卫
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
|
||||||
|
|
||||||
// 获取用户信息以判断角色
|
// ======工具函数======
|
||||||
const userInfoStr = localStorage.getItem('user_info') || sessionStorage.getItem('user_info');
|
|
||||||
const userInfo = userInfoStr ? JSON.parse(userInfoStr) : null;
|
|
||||||
|
|
||||||
// 简单的角色判定逻辑:如果用户名为 admin 则视为管理员,否则为普通用户
|
/* 获取用户信息 */
|
||||||
// 你也可以根据后端返回的具体的 role 字段来判断
|
function getUserInfo() {
|
||||||
const userRole = userInfo?.username === 'admin' ? 'admin' : 'user';
|
try {
|
||||||
|
const userInfoStr = localStorage.getItem('user_info') || sessionStorage.getItem('user_info')
|
||||||
|
return userInfoStr ? JSON.parse(userInfoStr) : null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析用户信息失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isRequiresAuth = to.matched.some(record => record.meta.requiresAuth);
|
/* 角色首页映射 */
|
||||||
|
const roleHomeMap = {
|
||||||
|
admin: { name: 'home' },
|
||||||
|
user: { name: 'user_workdesk' },
|
||||||
|
client: { name: 'client_monitor' }
|
||||||
|
};
|
||||||
|
|
||||||
// 1. 未登录拦截
|
|
||||||
if (isRequiresAuth && !token) {
|
/* 获取角色的默认路由 */
|
||||||
next({ name: 'login', query: { redirect: to.fullPath } });
|
function getRoleHomeRoute(role) {
|
||||||
return;
|
return roleHomeMap[role?.toLowerCase()] || { path: '/login' }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ======动态路由管理======
|
||||||
|
|
||||||
|
let hasAddedRoutes = false; // 标记是否已添加动态路由
|
||||||
|
|
||||||
|
/* 添加动态路由 */
|
||||||
|
function addDynamicRoutes(role) {
|
||||||
|
try {
|
||||||
|
// 过滤当前角色可以访问的路由
|
||||||
|
const accessibleRoutes = dynamicRoutes.filter(route => {
|
||||||
|
return !route.meta?.role || route.meta.role === role;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加路由
|
||||||
|
accessibleRoutes.forEach(route => {
|
||||||
|
router.addRoute(route);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加404路由
|
||||||
|
router.addRoute({
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('@/views/404View.vue'),
|
||||||
|
meta: { title: 404 }
|
||||||
|
});
|
||||||
|
|
||||||
|
hasAddedRoutes = true;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('动态添加路由失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 重置路由 */
|
||||||
|
|
||||||
|
export function resetRouter() {
|
||||||
|
// 移除动态添加的路由
|
||||||
|
dynamicRoutes.forEach(route => {
|
||||||
|
if (route.name && router.hasRoute(route.name)) {
|
||||||
|
router.removeRoute(route.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除404路由
|
||||||
|
if (router.hasRoute('NotFound')) {
|
||||||
|
router.removeRoute('NotFound');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 已登录状态下的逻辑
|
hasAddedRoutes = false;
|
||||||
if (token) {
|
|
||||||
// 如果想去登录页,根据角色重定向到对应首页
|
// 清除用户信息
|
||||||
if (to.name === 'login') {
|
localStorage.removeItem('user_info');
|
||||||
userRole === 'admin' ? next({ name: 'home' }) : next({ name: 'user_workdesk' });
|
sessionStorage.removeItem('user_info');
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ======路由守卫======
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
const userInfo = getUserInfo();
|
||||||
|
const isLoggedIn = !!(userInfo && userInfo.role); // 判断是否登录(双重逻辑非)
|
||||||
|
|
||||||
|
// 1.访问登录页
|
||||||
|
if (to.name === 'login') {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
// 已登录用户,重定向到角色首页
|
||||||
|
const homeRoute = getRoleHomeRoute(userInfo.role);
|
||||||
|
return next({ name: homeRoute.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 核心修复:权限校验 ---
|
// 未登录
|
||||||
// 检查目标路由是否定义了需要的角色
|
return next();
|
||||||
const requiredRole = to.meta.role;
|
|
||||||
|
|
||||||
if (requiredRole && requiredRole !== userRole) {
|
|
||||||
// 如果是员工想进管理员页面
|
|
||||||
if (userRole === 'user' && requiredRole === 'admin') {
|
|
||||||
ElMessage.error('权限不足,无法访问管理后台');
|
|
||||||
next({ name: 'user_workdesk' }); // 踢回员工工作台
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 如果是管理员想进员工页面 (通常允许,如果不允许也可以踢回 /)
|
|
||||||
if (userRole === 'admin' && requiredRole === 'user') {
|
|
||||||
next({ name: 'home' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 正常放行
|
// 2.未登录访问受保护的页面
|
||||||
next();
|
if (!isLoggedIn) {
|
||||||
|
hasAddedRoutes = false; // 确保未添加动态路由
|
||||||
|
|
||||||
|
// 不是首页才进行提示
|
||||||
|
if (to.path !== '/') {
|
||||||
|
ElMessage.info('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前路径,登录后跳回
|
||||||
|
return next({
|
||||||
|
name: 'login',
|
||||||
|
query: { redirect: to.fullPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 3.已登录,动态添加路由
|
||||||
|
if (!hasAddedRoutes) {
|
||||||
|
const success = addDynamicRoutes(userInfo.role);
|
||||||
|
if (!success) {
|
||||||
|
// 添加失败,清空状态,重新登录
|
||||||
|
resetRouter();
|
||||||
|
ElMessage.error('加载用户权限失败,请重新登录');
|
||||||
|
return next({ name: 'login' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用新注册的路由重新匹配原始导航路径(排除404通配)
|
||||||
|
const resolved = router.resolve(to);
|
||||||
|
if (resolved.matched.length > 0 && resolved.name !== 'NotFound') {
|
||||||
|
return next({ ...to, replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路径无匹配(如非Admin用户访问 /),回退到角色首页
|
||||||
|
const homeRoute = getRoleHomeRoute(userInfo.role);
|
||||||
|
return next({ name: homeRoute.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 已登录且路由已添加,放行
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ======全局后置守卫======
|
||||||
|
router.afterEach((to) => {
|
||||||
|
// 设置页面标题
|
||||||
|
if (to.meta?.title) {
|
||||||
|
document.title = `方舟工单系统 - ${to.meta.title}`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router
|
export default router;
|
||||||
|
|||||||
49
src/utils/format.js
Normal file
49
src/utils/format.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 补全 URL 协议头,防止无效跳转
|
||||||
|
*/
|
||||||
|
export function formatUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return '#'
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) return url
|
||||||
|
return `http://${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将日期字符串或时间戳转换为 YYYY-MM-DD 格式
|
||||||
|
*/
|
||||||
|
export function formatDate(time) {
|
||||||
|
if (!time || time === '0') return '-'
|
||||||
|
|
||||||
|
if (typeof time === 'string' && time.includes('-')) {
|
||||||
|
return time.slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Number(time)
|
||||||
|
if (!isNaN(timestamp) && timestamp > 0) {
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const d = date.getDate().toString().padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将时间转换为 MM-DD HH:mm 格式
|
||||||
|
*/
|
||||||
|
export function formatDateTime(time) {
|
||||||
|
if (!time) return '-'
|
||||||
|
if (typeof time === 'string' && time.includes('-')) return time
|
||||||
|
|
||||||
|
const date = new Date(time)
|
||||||
|
if (isNaN(date.getTime())) return '-'
|
||||||
|
|
||||||
|
const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const d = date.getDate().toString().padStart(2, '0')
|
||||||
|
const h = date.getHours().toString().padStart(2, '0')
|
||||||
|
const i = date.getMinutes().toString().padStart(2, '0')
|
||||||
|
return `${m}-${d} ${h}:${i}`
|
||||||
|
}
|
||||||
@@ -1,72 +1,52 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import router from '@/router'
|
import router, { resetRouter } from '@/router'
|
||||||
|
|
||||||
const service = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 10000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 标记位,防止重复弹窗
|
|
||||||
let isRelogging = false
|
let isRelogging = false
|
||||||
|
|
||||||
// 请求拦截器
|
function createService(basePath) {
|
||||||
service.interceptors.request.use(
|
const service = axios.create({
|
||||||
config => {
|
baseURL: `/api/${basePath}`,
|
||||||
// 自动获取 Token(兼顾两种存储方式)
|
timeout: 10000,
|
||||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token')
|
withCredentials: true,
|
||||||
if (token) {
|
})
|
||||||
config.headers['Authorization'] = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
error => Promise.reject(error)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 响应拦截器
|
service.interceptors.response.use(
|
||||||
service.interceptors.response.use(
|
response => response.data,
|
||||||
|
|
||||||
// 接口请求返回状态码是200,进入第一个
|
|
||||||
response => {
|
|
||||||
const token = response.headers['Authorization'];
|
|
||||||
if (token) {
|
|
||||||
localStorage.setItem('token', token);
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
const resData = error.response?.data;
|
|
||||||
const status = error.response?.status;
|
|
||||||
console.log("返回状态码", status);
|
|
||||||
|
|
||||||
|
|
||||||
// 1. 判定是否为 Token 相关的认证错误
|
error => {
|
||||||
// 包含 500 状态码且消息含有 token 字样(如 decode 失败、过期等)
|
const resData = error.response?.data
|
||||||
if (status === 401) {
|
const status = error.response?.status
|
||||||
|
|
||||||
if (!isRelogging) {
|
// session过期处理
|
||||||
isRelogging = true; // 锁定
|
if (status === 401) {
|
||||||
|
if (!isRelogging) {
|
||||||
|
isRelogging = true
|
||||||
|
|
||||||
localStorage.removeItem('token');
|
resetRouter()
|
||||||
// 清除用户信息,防止 UserView.vue 的 computed 报错
|
|
||||||
localStorage.removeItem('user_info');
|
|
||||||
|
|
||||||
ElMessage.error('登录已失效,请重新登录');
|
ElMessage.error('登录已过期,请重新登录')
|
||||||
|
|
||||||
router.push('/login').then(() => {
|
router.push({
|
||||||
// 2. 延迟解锁,确保页面跳转期间的所有并发请求都已返回并被此拦截器捕获
|
name: 'login',
|
||||||
setTimeout(() => {
|
query: { redirect: router.currentRoute.value.fullPath }
|
||||||
isRelogging = false;
|
}).finally(() => {
|
||||||
}, 3000);
|
isRelogging = false
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
return new Promise(() => { })
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(() => { });
|
const msg = resData?.message || '服务器响应异常'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const msg = resData?.message || '服务器响应异常';
|
return service
|
||||||
ElMessage.error(msg);
|
}
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
export const adminRequest = createService('admin')
|
||||||
);
|
export const indexRequest = createService('index')
|
||||||
export default service
|
export default adminRequest
|
||||||
37
src/views/404View.vue
Normal file
37
src/views/404View.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-found-container">
|
||||||
|
<h1>404 - 页面未找到</h1>
|
||||||
|
<p>您要查找的页面不存在或您没有权限访问。</p>
|
||||||
|
<el-button type="primary" @click="goHome">返回首页</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
const userInfoStr =
|
||||||
|
localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
|
||||||
|
const userRole = userInfoStr ? JSON.parse(userInfoStr).role : null;
|
||||||
|
|
||||||
|
const roleHomeMap = {
|
||||||
|
admin: { name: "home" },
|
||||||
|
user: { name: "user_workdesk" },
|
||||||
|
client: { name: "client_monitor" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const homeRoute = roleHomeMap[userRole];
|
||||||
|
|
||||||
|
// 如果用户角色有对应的首页,则跳转;否则,跳转到登录页。
|
||||||
|
router.push(homeRoute || { name: "login" });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.not-found-container {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<el-table-column label="更新日期" width="140" align="center">
|
<el-table-column label="更新日期" width="140" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span class="date-text">{{
|
<span class="date-text">{{
|
||||||
formatDateOnly(
|
formatDate(
|
||||||
scope.row.lock_time || scope.row.update_time || scope.row.create_time
|
scope.row.lock_time || scope.row.update_time || scope.row.create_time
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -143,8 +143,9 @@ import {
|
|||||||
CircleCheck,
|
CircleCheck,
|
||||||
Monitor,
|
Monitor,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
import { ticketApi } from "@/api/ticket";
|
import { ticketApi, userApi } from "@/api/ticket";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
import { formatDate } from "@/utils/format";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -259,26 +260,18 @@ const getStatusClass = (status) => {
|
|||||||
return map[status] || "";
|
return map[status] || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDateOnly = (ts) => {
|
|
||||||
if (!ts || ts === "0" || ts === "") return "-";
|
|
||||||
const date = !isNaN(ts)
|
|
||||||
? new Date(String(ts).length === 10 ? ts * 1000 : parseInt(ts))
|
|
||||||
: new Date(ts);
|
|
||||||
return isNaN(date.getTime())
|
|
||||||
? "-"
|
|
||||||
: `${date.getFullYear()}-${(date.getMonth() + 1)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageChange = (p) => {
|
const handlePageChange = (p) => {
|
||||||
page.value = p;
|
page.value = p;
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
localStorage.clear();
|
try {
|
||||||
sessionStorage.clear();
|
await userApi.logout();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
localStorage.removeItem("user_info");
|
||||||
|
sessionStorage.removeItem("user_info");
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="brand-text">
|
<div class="brand-text">
|
||||||
<h1 class="title">方舟工单</h1>
|
<h1 class="title">方舟工单</h1>
|
||||||
<span class="sub-text">ARK TICKET SYSTEM v3.0</span>
|
<span class="sub-text">ARK TICKET SYSTEM v4.0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" class="ark-form">
|
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" class="ark-form">
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<div class="input-prefix">ID</div>
|
<div class="input-prefix">ID</div>
|
||||||
<el-input v-model="loginForm.username" placeholder="工员编号" class="ark-input" />
|
<el-input v-model="loginForm.username" placeholder="员工编号" class="ark-input" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
@@ -64,17 +64,17 @@ import { ref, reactive } from "vue";
|
|||||||
import { DataBoard } from "@element-plus/icons-vue";
|
import { DataBoard } from "@element-plus/icons-vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { userApi } from "@/api/ticket";
|
import { userApi } from "@/api/ticket";
|
||||||
import { useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const loginForm = reactive({ username: "", password: "" });
|
const loginForm = reactive({ username: "", password: "" });
|
||||||
const rememberMe = ref(false);
|
const rememberMe = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loginFormRef = ref(null);
|
const loginFormRef = ref(null);
|
||||||
|
|
||||||
const loginRules = reactive({
|
const loginRules = reactive({
|
||||||
username: [{ required: true, message: "请输入工员编号", trigger: "blur" }],
|
username: [{ required: true, message: "请输入员工编号", trigger: "blur" }],
|
||||||
password: [{ required: true, message: "请输入密钥", trigger: "blur" }],
|
password: [{ required: true, message: "请输入密钥", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,52 +85,32 @@ const handleLogin = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
const res = await userApi.login(loginForm.username, loginForm.password);
|
const res = await userApi.login(loginForm.username, loginForm.password);
|
||||||
// console.log(res)
|
|
||||||
// 注意:这里由于你之前的封装,可能需要判断 res.data.code 还是 res.code
|
|
||||||
// 如果用了拦截器,直接判断 res.code
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const token = res.data.token;
|
// ThinkPHP 会自动通过 Set-Cookie 下发 session,浏览器自动接管,无需手动存 token
|
||||||
const userInfo = JSON.stringify(res.data.user_info);
|
const userInfo = res.data;
|
||||||
const role = res.data.user_info.role;
|
const userInfoStr = JSON.stringify(userInfo);
|
||||||
|
|
||||||
// console.log(role)
|
// 用户信息存前端,仅用于显示用户名和角色路由判断
|
||||||
|
const storage = rememberMe.value ? localStorage : sessionStorage;
|
||||||
|
storage.setItem("user_info", userInfoStr);
|
||||||
|
|
||||||
// --- 核心逻辑开始 ---
|
ElMessage({
|
||||||
if (rememberMe.value) {
|
message: "身份验证通过,方舟系统加载中...",
|
||||||
// 勾选了:存入 localStorage,实现持久化
|
type: "success",
|
||||||
localStorage.setItem("token", token);
|
duration: 300,
|
||||||
localStorage.setItem("user_info", userInfo);
|
});
|
||||||
} else {
|
|
||||||
// 没勾选:存入 sessionStorage,关闭标签页即失效
|
|
||||||
sessionStorage.setItem("token", token);
|
|
||||||
sessionStorage.setItem("user_info", userInfo);
|
|
||||||
}
|
|
||||||
// --- 核心逻辑结束 ---
|
|
||||||
|
|
||||||
ElMessage.success("身份验证通过,方舟系统加载中...");
|
// 触发路由守卫,自动处理跳转
|
||||||
|
await router.push(route.query.redirect || "/");
|
||||||
// 3. 根据角色执行路由跳转
|
|
||||||
setTimeout(() => {
|
|
||||||
switch (role) {
|
|
||||||
case "admin":
|
|
||||||
router.push("/"); // 管理员跳转至主页
|
|
||||||
break;
|
|
||||||
case "user":
|
|
||||||
router.push("/user"); // 跳转至 UserView (请确保路由配置中有此路径)
|
|
||||||
break;
|
|
||||||
case "client":
|
|
||||||
router.push("/client"); // 跳转至 ClientView (请确保路由配置中有此路径)
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ElMessage.warning("未识别的角色权限,请联系管理员");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || "登录失败");
|
ElMessage.error(res.message || "登录失败,请检查账号和密码");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Login Error:", error);
|
if (error.name !== "ValidationError") {
|
||||||
|
ElMessage.error("登录请求异常,请稍后重试");
|
||||||
|
// console.error("登录错误:", error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,18 @@
|
|||||||
<div class="left">
|
<div class="left">
|
||||||
<el-button type="primary" @click="handleAdd" :icon="Plus">新增任务</el-button>
|
<el-button type="primary" @click="handleAdd" :icon="Plus">新增任务</el-button>
|
||||||
</div>
|
</div>
|
||||||
<TicketFilter :operators="operatorsForFilter" :staffs="staffList" :filters="filters"
|
<TicketFilter :operators="operatorsForFilter" :staffs="userStaffList" :filters="filters"
|
||||||
@filter-change="handleFilterChange" @reset="handleFilterReset" />
|
@filter-change="handleFilterChange" @reset="handleFilterReset" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TicketList :tickets="tickets" :loading="loading" @edit="handleEdit" @delete="handleDelete" />
|
<div class="batch-bar" v-if="selectedTickets.length">
|
||||||
|
<span class="batch-info">已选 <strong>{{ selectedTickets.length }}</strong> 个工单</span>
|
||||||
|
<el-button type="warning" @click="handleBatchFinish">一键已完结</el-button>
|
||||||
|
<el-button link type="info" @click="ticketListRef.clearSelection()">取消选择</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TicketList ref="ticketListRef" :tickets="tickets" :loading="loading" @edit="handleEdit" @delete="handleDelete"
|
||||||
|
@selection-change="val => selectedTickets = val" />
|
||||||
|
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||||
@@ -66,14 +73,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :customers="customerList"
|
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :companiesWithOperators="companywithoperatorsList"
|
||||||
:customer-employees="allCustomerEmployees" :staffs="staffList" :is-editing="isEditing"
|
:user-staffs="userStaffList" :is-editing="isEditing" @submit="handleFormSubmit" />
|
||||||
@submit="handleFormSubmit" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, computed } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
@@ -85,7 +91,7 @@ import {
|
|||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
// API 引入
|
// API 引入
|
||||||
import { ticketApi, customerApi } from "@/api/ticket";
|
import { ticketApi } from "@/api/ticket";
|
||||||
|
|
||||||
// 组件引入
|
// 组件引入
|
||||||
import TicketFilter from "@/components/ticket/TicketFilter.vue";
|
import TicketFilter from "@/components/ticket/TicketFilter.vue";
|
||||||
@@ -94,14 +100,16 @@ import TicketForm from "@/components/ticket/TicketForm.vue";
|
|||||||
|
|
||||||
// --- 响应式状态 ---
|
// --- 响应式状态 ---
|
||||||
const tickets = ref([]);
|
const tickets = ref([]);
|
||||||
const operators = ref([]);
|
const userStaffList = ref([]);
|
||||||
const staffList = ref([]);
|
const companywithoperatorsList = ref([]);
|
||||||
const customerList = ref([]);
|
|
||||||
const allCustomerEmployees = ref([]);
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showAddDialog = ref(false);
|
const showAddDialog = ref(false);
|
||||||
const isEditing = ref(false);
|
const isEditing = ref(false);
|
||||||
const currentTicket = ref({});
|
const currentTicket = ref({});
|
||||||
|
const selectedTickets = ref([]);
|
||||||
|
const ticketListRef = ref(null);
|
||||||
|
|
||||||
|
const operatorsForFilter = ref([]);
|
||||||
|
|
||||||
const allStats = reactive({
|
const allStats = reactive({
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -119,54 +127,56 @@ const pagination = reactive({
|
|||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
status: null,
|
status: null,
|
||||||
operator_name: null,
|
operator_id: null,
|
||||||
staff_id: null,
|
user_id: null,
|
||||||
search: null,
|
search: null,
|
||||||
sort_order: "desc",
|
sort_order: "desc",
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 核心数据加载方法 ---
|
// --- 核心数据加载方法 ---
|
||||||
|
/* 获取运营人员数据的方法*/
|
||||||
|
|
||||||
// 在 TicketManagement.vue 中利用 computed 提取
|
const fetchOperatorsForFilter = async () => {
|
||||||
const operatorsForFilter = computed(() => {
|
|
||||||
// 返回包含 ID、姓名和公司名的对象数组
|
|
||||||
return allCustomerEmployees.value.map((emp) => {
|
|
||||||
// 在 allCustomers 中找到该员工所属的公司名
|
|
||||||
const company = customerList.value.find((c) => c.customer_id === emp.customer_id);
|
|
||||||
return {
|
|
||||||
id: emp.employee_id,
|
|
||||||
name: emp.employee_name,
|
|
||||||
company: company ? company.company_name : "未知单位",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载客户单位及对应员工数据
|
|
||||||
* 根据你提供的接口返回结构进行了适配
|
|
||||||
*/
|
|
||||||
const loadCustomerData = async () => {
|
|
||||||
try {
|
try {
|
||||||
const res = await customerApi.getCustomers();
|
const response = await ticketApi.getOperatorsNew();
|
||||||
// 适配点:根据返回的 code: 200 逻辑进行判断
|
if (response.code === 200) {
|
||||||
if (res.code === 200) {
|
// 将新的嵌套数据结构转换为原来的扁平格式
|
||||||
// 适配点:直接从 res 中获取 customers 和 employees 数组
|
operatorsForFilter.value = response.data.flatMap(company =>
|
||||||
customerList.value = res.customers || [];
|
company.companyoperators.map(op => ({
|
||||||
allCustomerEmployees.value = res.employees || [];
|
id: op.id,
|
||||||
|
operator_name: op.operator_name,
|
||||||
|
company_name: company.company_name
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取客户数据失败:", error);
|
console.error("获取运营人员数据失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载客户单位and运营人员
|
||||||
|
*/
|
||||||
|
const loadCompanyWithOperatorsData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await ticketApi.getOperatorsNew();
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
companywithoperatorsList.value = res.data || [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取客户及其运营人员数据失败:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载我方员工列表
|
* 加载我方员工列表
|
||||||
*/
|
*/
|
||||||
const loadStaffData = async () => {
|
const loadUserStaffData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await ticketApi.getStaffs();
|
const res = await ticketApi.getUserStaffs();
|
||||||
if (res.code) {
|
if (res.code === 200) {
|
||||||
staffList.value = res.data;
|
userStaffList.value = res.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取我方员工列表失败:", error);
|
console.error("获取我方员工列表失败:", error);
|
||||||
@@ -178,13 +188,26 @@ const loadStaffData = async () => {
|
|||||||
*/
|
*/
|
||||||
const initGlobalStats = async () => {
|
const initGlobalStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await ticketApi.getticketsstatuscount();
|
const response = await ticketApi.getticketdashboardstats();
|
||||||
if (response.code) {
|
if (response.code === 200) {
|
||||||
allStats.total = response.data["total"];
|
allStats.total = response.data.total;
|
||||||
allStats.pending = response.data["pending"];
|
const statusCounts = response.data.status_counts || [];
|
||||||
allStats.ongoing = response.data["ongoing"];
|
statusCounts.forEach((item) => {
|
||||||
allStats.completed = response.data["completed"];
|
switch (item.status) {
|
||||||
allStats.is_completed = response.data["is_completed"];
|
case 0:
|
||||||
|
allStats.pending = item.count;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
allStats.ongoing = item.count;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
allStats.completed = item.count;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
allStats.is_completed = item.count;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("统计全局数据失败:", error);
|
console.error("统计全局数据失败:", error);
|
||||||
@@ -224,18 +247,20 @@ const refreshAll = () => {
|
|||||||
const handleFilterChange = (newFilters) => {
|
const handleFilterChange = (newFilters) => {
|
||||||
Object.assign(filters, newFilters);
|
Object.assign(filters, newFilters);
|
||||||
pagination.page = 1;
|
pagination.page = 1;
|
||||||
|
selectedTickets.value = [];
|
||||||
loadTickets();
|
loadTickets();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterReset = () => {
|
const handleFilterReset = () => {
|
||||||
Object.assign(filters, {
|
Object.assign(filters, {
|
||||||
status: null,
|
status: null,
|
||||||
operator_name: null,
|
operator_id: null,
|
||||||
staff_id: null,
|
user_id: null,
|
||||||
search: null,
|
search: null,
|
||||||
sort_order: "desc",
|
sort_order: "desc",
|
||||||
});
|
});
|
||||||
pagination.page = 1;
|
pagination.page = 1;
|
||||||
|
selectedTickets.value = [];
|
||||||
loadTickets();
|
loadTickets();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,7 +314,6 @@ const handleDelete = async (id) => {
|
|||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
});
|
});
|
||||||
const res = await ticketApi.deleteTicket(id);
|
const res = await ticketApi.deleteTicket(id);
|
||||||
// console.log('删除',res)
|
|
||||||
if (res.code) {
|
if (res.code) {
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
refreshAll();
|
refreshAll();
|
||||||
@@ -299,15 +323,28 @@ const handleDelete = async (id) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载所有运营人员
|
const handleBatchFinish = async () => {
|
||||||
const loadOperators = async () => {
|
const ids = selectedTickets.value.map((t) => t.id);
|
||||||
try {
|
try {
|
||||||
const response = await ticketApi.getOperators();
|
await ElMessageBox.confirm(
|
||||||
if (response.code) {
|
`确认将选中的 ${ids.length} 个工单一键已完结?`,
|
||||||
operators.value = response.data;
|
"批量操作",
|
||||||
|
{ type: "warning", confirmButtonText: "确认已完结" }
|
||||||
|
);
|
||||||
|
const res = await ticketApi.batchUpdateTicket({
|
||||||
|
ids,
|
||||||
|
status: "已完结",
|
||||||
|
remark: "批量结单",
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(res.data?.msg || `成功完结 ${ids.length} 个工单`);
|
||||||
|
ticketListRef.value.clearSelection();
|
||||||
|
refreshAll();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "批量操作失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取运营人员筛选列表失败:", error);
|
if (error !== "cancel") ElMessage.error("批量操作失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -315,17 +352,18 @@ const handleSizeChange = (val) => {
|
|||||||
pagination.limit = val;
|
pagination.limit = val;
|
||||||
loadTickets();
|
loadTickets();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCurrentChange = (val) => {
|
const handleCurrentChange = (val) => {
|
||||||
pagination.page = val;
|
pagination.page = val;
|
||||||
loadTickets();
|
loadTickets();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
fetchOperatorsForFilter();
|
||||||
loadTickets();
|
loadTickets();
|
||||||
initGlobalStats();
|
initGlobalStats();
|
||||||
loadOperators();
|
loadUserStaffData();
|
||||||
loadStaffData();
|
loadCompanyWithOperatorsData();
|
||||||
loadCustomerData(); // 级联数据加载
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -445,6 +483,22 @@ onMounted(() => {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #fdf6ec;
|
||||||
|
border: 1px solid #faecd8;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -85,12 +85,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||||
<el-table-column
|
<el-table-column prop="website" label="网站名称" min-width="150">
|
||||||
prop="website"
|
<template #default="{ row }">
|
||||||
label="网站名称"
|
<el-link
|
||||||
min-width="150"
|
type="primary"
|
||||||
show-overflow-tooltip
|
:href="formatUrl(row.website)"
|
||||||
/>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{{ row.website }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="数据量" width="150" align="center">
|
<el-table-column label="数据量" width="150" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -255,10 +261,11 @@
|
|||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span class="pure-date">
|
<span class="pure-date">
|
||||||
{{
|
{{
|
||||||
(["已完成", "已完结"].includes(currentStatus)
|
formatDate(
|
||||||
? scope.row.lock_time
|
["已完成", "已完结"].includes(currentStatus)
|
||||||
: scope.row.create_time
|
? scope.row.lock_time
|
||||||
)?.slice(0, 10) || "-"
|
: scope.row.create_time
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -319,7 +326,9 @@ import {
|
|||||||
Checked,
|
Checked,
|
||||||
Loading,
|
Loading,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
import { ticketApi } from "@/api/ticket";
|
import { ticketApi, userApi } from "@/api/ticket";
|
||||||
|
import { resetRouter } from "@/router";
|
||||||
|
import { formatUrl, formatDate } from "@/utils/format";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -377,7 +386,6 @@ const fetchTickets = async () => {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("员工id", realStaffInfo.value.id);
|
|
||||||
params.staff_id = realStaffInfo.value.id;
|
params.staff_id = realStaffInfo.value.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,10 +519,16 @@ const handleCancel = (row) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
ElMessageBox.confirm("确定退出当前账号?", "提示", { type: "warning" }).then(() => {
|
ElMessageBox.confirm("确定退出当前账号?", "提示", { type: "warning" }).then(async () => {
|
||||||
localStorage.clear();
|
try {
|
||||||
sessionStorage.clear();
|
await userApi.logout();
|
||||||
router.push("/login");
|
} catch (e) {}
|
||||||
|
|
||||||
|
localStorage.removeItem("user_info");
|
||||||
|
sessionStorage.removeItem("user_info");
|
||||||
|
|
||||||
|
resetRouter();
|
||||||
|
router.push({ name: "login" });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 8080, // 前端启动的端口
|
port: 8080, // 前端启动的端口
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { // 所有以api开头的请求都会被代理
|
'/api': {
|
||||||
target: 'http://localhost',
|
target: 'http://localhost',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user