减少冗余代码

This commit is contained in:
2026-05-14 15:41:00 +08:00
parent ae4cf4540b
commit fa0c718c8b
15 changed files with 802 additions and 439 deletions

View File

@@ -1,6 +1,5 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"

View File

@@ -1,78 +1,80 @@
import request from '@/utils/request'
import { adminRequest, indexRequest } from '@/utils/request'
// ==================== 前台接口(/api/index) ====================
export const userApi = {
// 登录接口
login(username, password) {
// 这里的 data 会被 axios 放在请求体 (Body) 中发送
return request.post('/login', {
username,
password
})
return indexRequest.post('/login', { username, password })
},
// 退出登录
logout() {
return request.post('/logout')
return indexRequest.post('/logout')
}
}
// ==================== 后台接口(/api/admin) ====================
export const ticketApi = {
//获取工单各个完成状态的统计数量
getticketsstatuscount(){
return request.get('/getticketstatuscount')
// 获取工单各完成状态的统计数量
getticketdashboardstats() {
return adminRequest.get('/getticketdashboardstats')
},
// 获取工单列表
getTickets(params) {
return request.get('/ticketsfind', { params })
return adminRequest.get('/ticketsfind', { params })
},
// 新增工单
addTicket(data) {
return request.post('/addticket', data)
return adminRequest.post('/addticket', data)
},
// 更新工单
updateTicket(id, data) {
return request.put(`/ticket/${id}`, data)
return adminRequest.put(`/ticket/${id}`, data)
},
// 删除工单
deleteTicket(id) {
return request.delete(`/ticket/${id}`)
return adminRequest.delete(`/ticket/${id}`)
},
// 获取运营人员列表
getOperators() {
return request.get('/operators')
// 获取运营人员列表(按公司分组)
getOperatorsNew() {
return adminRequest.get('/getOperatorList')
},
// 获取员工列表
getStaffs(params) {
return request.get('/getStaffList', { params })
}
getUserStaffs() {
return adminRequest.get('/getUserStaffList')
},
// 批量更新工单
batchUpdateTicket(data) {
return adminRequest.put('/tickets/batch-update', data)
},
}
export const customerApi = {
getCustomers() {
return request.get('/customers')
return adminRequest.get('/customers')
},
// 添加客户,data 结构应为 { company_name: 'xxx' }
addCustomerName(data) {
return request.post('/addcustomername', data)
return adminRequest.post('/addcustomername', 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}`)
}
}

View File

@@ -15,24 +15,26 @@
<div class="input-group-row">
<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">
<el-option v-for="op in operators" :key="op.id" :label="`${op.name} (${op.company})`"
:value="op.name" />
<el-option-group v-for="group in groupedOperators" :key="group.company_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-form-item>
<div class="divider-wrapper">/</div>
<el-form-item label="我方负责人">
<el-select v-model="localFilters.staff_id" placeholder="全部负责人" clearable filterable class="wide-select"
<el-form-item label="我方牛马">
<el-select v-model="localFilters.user_id" placeholder="全部牛马" clearable filterable class="wide-select"
@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-form-item>
<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">
<template #prefix>
<el-icon>
@@ -66,7 +68,7 @@
</template>
<script setup>
import { reactive, watch } from "vue";
import { reactive, watch, computed } from "vue";
import { Search, Sort } from "@element-plus/icons-vue";
const props = defineProps({
@@ -75,9 +77,51 @@ const props = defineProps({
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(
() => props.filters,
@@ -197,4 +241,12 @@ const setSort = (order) => {
width: 100%;
}
}
/* 运营人员分组标题样式 */
:deep(.el-select-group__title) {
background-color: #f5f7fa;
font-weight: 600;
color: #303133;
padding-left: 12px;
}
</style>

View File

@@ -3,9 +3,9 @@
destroy-on-close @close="handleClose">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="compact-form">
<div class="form-section">
<div class="section-title">业务基础信息</div>
<div class="section-title">基础信息</div>
<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>
<el-icon>
<Link />
@@ -16,34 +16,34 @@
</div>
<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-select v-model="form.customer_id" placeholder="请选择单位" style="width: 100%" filterable
<el-form-item label="客户单位" prop="company_id" class="client-item">
<el-select v-model="form.company_id" placeholder="请选择单位" style="width: 100%" filterable
@change="handleCustomerChange">
<el-option v-for="item in customers" :key="item.customer_id" :label="item.company_name"
:value="item.customer_id" />
<el-option v-for="item in companiesWithOperators.filter(c => c.id !== 0)" :key="item.id" :label="item.company_name"
:value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="运营人员 (外方)" prop="operator" class="client-item">
<el-select v-model="form.operator" placeholder="请选择人员" style="width: 100%" filterable
:disabled="!form.customer_id">
<el-option v-for="item in filteredEmployees" :key="item.employee_id" :label="item.employee_name"
:value="item.employee_name" />
<el-form-item label="运营人员" prop="operator_id" class="client-item">
<el-select v-model="form.operator_id" placeholder="请选择人员" style="width: 100%" filterable
:disabled="!form.company_id">
<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.id" />
</el-select>
</el-form-item>
<el-form-item v-if="isEditing" label="负责人 (我方)" prop="staff_id" class="owner-item column-full">
<el-select v-model="form.staff_id" placeholder="请选择我方负责人" style="width: 100%" filterable>
<el-option v-for="item in staffs" :key="item.id" :label="item.name" :value="item.id" />
<el-form-item label="我方牛马" prop="user_id" class="owner-item column-full">
<el-select v-model="form.user_id" placeholder="请选择牛马" style="width: 100%" filterable>
<el-option v-for="item in userStaffs" :key="item.id" :label="item.username" :value="item.id" />
</el-select>
</el-form-item>
</div>
<div class="form-section">
<el-form-item label="当前进度">
<el-radio-group v-model="form.is_complete" class="modern-radio-group">
<el-form-item label="进度">
<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>
@@ -51,8 +51,8 @@
</el-radio-group>
</el-form-item>
<el-form-item label="备注说明" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写相关的补充信息或注意事项..." resize="none" />
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写注意事项" resize="none" />
</el-form-item>
</div>
</el-form>
@@ -77,10 +77,9 @@ const emit = defineEmits(["update:modelValue", "submit"]);
const props = defineProps({
modelValue: Boolean,
formData: Object,
customers: Array,
customerEmployees: Array,
staffs: Array,
userStaffs: Array,
isEditing: Boolean,
companiesWithOperators: Array,
});
// --- 响应式变量 ---
@@ -91,10 +90,10 @@ const submitting = ref(false);
// 核心表单对象:customer_id 已包含在内
const form = reactive({
website: "",
operator: "",
customer_id: null,
staff_id: null,
is_complete: "未分配",
company_id: null,
operator_id: null,
user_id: null,
status: 0,
remark: "",
});
@@ -102,24 +101,14 @@ const form = reactive({
const rules = computed(() => {
const baseRules = {
website: [{ required: true, message: "请输入网站地址", trigger: "blur" }],
customer_id: [{ required: true, message: "请选择客户单位", trigger: "change" }],
operator: [{ required: true, message: "请选择运营人员", trigger: "change" }],
company_id: [{ required: true, message: "请选择客户单位", trigger: "change" }],
};
if (props.isEditing) {
baseRules.staff_id = [{ required: true, message: "请选择负责人", trigger: "change" }];
}
return baseRules;
});
// --- 逻辑:根据选择的单位过滤人员 ---
const filteredEmployees = computed(() => {
if (!form.customer_id) return [];
return props.customerEmployees.filter((emp) => emp.customer_id === form.customer_id);
});
// 单位切换时,重置运营人员选择
const handleCustomerChange = () => {
form.operator = "";
form.operator_id = null;
};
// --- 监听弹窗与回显 ---
@@ -134,13 +123,13 @@ watch(
// 深度合并数据
Object.assign(form, props.formData);
// 回显补充:如果传入的 formData 没有带 customer_id,通过 operator 反查补全
if (!form.customer_id && props.formData.operator) {
const employee = props.customerEmployees.find(
(e) => e.employee_name === props.formData.operator
// 回显补充:如果传入的 formData 没有带 company_id,通过 operator_id 反查补全
if (!form.company_id && props.formData.operator_id) {
const employee = props.companiesWithOperators.flatMap(c => c.companyoperators).find(
(e) => e.id === props.formData.operator_id
);
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();
Object.assign(form, {
website: "",
operator: "",
customer_id: null,
staff_id: null,
is_complete: "未分配",
company_id: null,
operator_id: null,
user_id: null,
status: 0,
remark: "",
});
};
@@ -178,10 +167,17 @@ const handleSubmit = async () => {
// 复制提交数据,如果是新增,确保 staff_id 为空
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.update_time;
if (!props.isEditing) {
submitData.staff_id = null;
submitData.user_id = null;
}
emit("submit", submitData);

View File

@@ -1,10 +1,23 @@
<template>
<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">
<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 }}
</el-link>
</template>
@@ -32,17 +45,28 @@
</template>
</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">
<template #default="{ row }">
<div class="staff-wrapper">
<div class="staff-item client">
<span class="staff-role">运营</span>
<span class="staff-name">{{ row.operator?.trim() || '未指派' }}</span>
<span class="staff-name">{{ row.operator_name?.trim() || "未指派" }}</span>
</div>
<div class="staff-item internal">
<span class="staff-role">负责</span>
<span class="staff-name">
{{ row.principalStaff?.name || '未指派' }}
{{ row.username?.trim() || "未指派" }}
</span>
</div>
</div>
@@ -51,17 +75,22 @@
<el-table-column label="备注" min-width="150">
<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>
</el-tooltip>
<span v-else class="empty-text">-</span>
</template>
</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 }">
<span class="status-badge" :style="getStatusStyle(row.is_complete)">
{{ row.is_complete }}
<span class="status-badge" :style="getStatusStyle(row.status)">
{{ statusLabel(row.status) }}
</span>
</template>
</el-table-column>
@@ -73,21 +102,17 @@
type="success"
size="small"
effect="light"
>已锁定</el-tag>
<el-tag
v-else
type="info"
size="small"
effect="plain"
>未锁定</el-tag>
>已锁定</el-tag
>
<el-tag v-else type="info" size="small" effect="plain">未锁定</el-tag>
</template>
</el-table-column>
<el-table-column label="时间节点" width="160">
<template #default="{ row }">
<div class="time-wrapper">
<span>创建:{{ formatTime(row.create_time) }}</span>
<span class="update-label">更新:{{ formatTime(row.update_time) }}</span>
<span>创建:{{ formatDateTime(row.create_time) }}</span>
<span class="update-label">更新:{{ formatDateTime(row.update_time) }}</span>
</div>
</template>
</el-table-column>
@@ -113,40 +138,44 @@
</template>
<script setup>
import { ref } from "vue";
import { formatUrl, formatDateTime } from "@/utils/format";
defineProps({
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) => {
if (!time) return '-';
if (typeof time === 'string' && time.includes('-')) return time;
const tableRef = ref(null);
const date = new Date(time);
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}`;
}
defineExpose({
clearSelection() {
tableRef.value?.clearSelection();
},
});
const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" };
const statusLabel = (status) => statusMap[status] || "未知";
const getStatusStyle = (status) => {
const styles = {
"未分配": { color: '#909399', background: '#f4f4f5' },
"进行中": { color: '#409EFF', background: '#ecf5ff' },
"已完成": { color: '#67C23A', background: '#f0f9eb' },
"已完结": { color: '#e6a23c', background: '#fdf6ec' }
}
return styles[status] || styles["未分配"];
}
0: { color: "#909399", background: "#f4f4f5" },
1: { color: "#409EFF", background: "#ecf5ff" },
2: { color: "#67C23A", background: "#f0f9eb" },
3: { color: "#e6a23c", background: "#fdf6ec" },
};
return styles[status] || styles[0];
};
</script>
<style scoped>
.progress-container {
padding-right: 10px;
}
/* 调整进度条内部文字大小 */
:deep(.el-progress__text) {
font-size: 11px !important;
@@ -158,6 +187,7 @@ const getStatusStyle = (status) => {
flex-direction: column;
gap: 4px;
}
.staff-item {
display: flex;
align-items: center;
@@ -166,6 +196,7 @@ const getStatusStyle = (status) => {
border-radius: 4px;
overflow: hidden;
}
.staff-role {
width: 34px;
height: 100%;
@@ -176,19 +207,34 @@ const getStatusStyle = (status) => {
background: #909399;
color: #fff;
}
.staff-name {
padding-left: 6px;
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.internal .staff-role { background: #409eff; }
.staff-item.client {
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 {
font-family: 'Monaco', monospace;
font-family: "Monaco", monospace;
font-weight: bold;
color: #409eff;
}
.remark-text {
font-size: 12px;
color: #606266;
@@ -197,9 +243,17 @@ const getStatusStyle = (status) => {
text-overflow: ellipsis;
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 {
display: inline-block;
padding: 0 8px;
@@ -208,12 +262,20 @@ const getStatusStyle = (status) => {
font-size: 11px;
border-radius: 11px;
}
.time-wrapper {
display: flex;
flex-direction: column;
font-size: 11px;
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>

View File

@@ -3,6 +3,13 @@
<el-container class="full-screen-container">
<el-aside width="200px" class="siderbar">
<div class="sidebar-header">
<!-- 修复:将 logo 区域包装成一个正确的外部链接 -->
<a
href="https://www.example.com"
target="_blank"
rel="noopener noreferrer"
class="logo-link"
>
<div class="logo-container">
<img src="@/assets/logo.jpg" alt="方舟网络" class="logo-image" />
<div class="text-content">
@@ -10,18 +17,22 @@
<p class="system-name">工单管理系统</p>
</div>
</div>
</a>
</div>
<el-menu default-active="/" class="sidebar-menu" router>
<el-menu-item index="/">
<el-icon><icon-menu /></el-icon>
<span>工单管理</span>
</el-menu-item>
<el-menu-item index="/stats">
<el-icon>
<document />
</el-icon>
<span>数据统计</span>
</el-menu-item>
<el-menu-item index="/customers">
<el-icon>
<User />
@@ -66,6 +77,8 @@
import { reactive, onMounted } from "vue";
import { Menu as IconMenu, Document, User } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
import { resetRouter } from "@/router";
import { userApi } from "@/api/ticket";
const router = useRouter();
const user = reactive({
@@ -82,10 +95,19 @@ onMounted(() => {
}
});
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
router.push("/login");
const handleLogout = async () => {
// 1. 调用后端接口销毁 session
try {
await userApi.logout();
} catch (e) {
// 即使后端接口异常,也要完成前端的清理
}
localStorage.removeItem("user_info");
sessionStorage.removeItem("user_info");
resetRouter();
router.push({ name: "login" });
};
</script>
@@ -119,6 +141,13 @@ const handleLogout = () => {
align-items: center;
}
/* 修复:为新添加的链接添加样式 */
.logo-link {
text-decoration: none;
color: inherit;
display: block;
}
.logo-container {
display: flex;
align-items: center;

View File

@@ -1,105 +1,220 @@
import { createRouter, createWebHistory } from 'vue-router'
import { ElMessage } from 'element-plus' // 引入提示组件
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// 公开路由,所有用户都可以访问
export const publicRoutes = [
{
path: '/login',
name: 'login',
component: () => import('@/views/LoginView.vue'),
meta: { isPublic: true }
}
]
// 动态路由,根据角色进行添加
export const dynamicRoutes = [
{
path: '/',
component: () => import('../layouts/MainLayout.vue'),
name: 'admin_layout',
component: () => import('@/layouts/MainLayout.vue'),
meta: { role: 'admin' },
redirect: { name: 'home' }, // 添加默认重定向
children: [
{
path: '',
name: 'home',
component: () => import('../views/TicketManagement.vue'),
// 标记需要 admin 权限
meta: { requiresAuth: true, role: 'admin' }
component: () => import('@/views/TicketManagement.vue'),
meta: { title: '工单管理' }
},
{
path: 'stats',
name: 'stats',
component: () => import('../views/StatsView.vue'),
meta: { requiresAuth: true, role: 'admin' }
component: () => import('@/views/StatsView.vue'),
meta: { title: '数据统计' }
},
{
path: 'customers',
name: 'customers',
component: () => import('../views/CustomersManagement.vue'),
meta: { requiresAuth: true, role: 'admin' }
},
component: () => import('@/views/CustomersManagement.vue'),
meta: { title: '客户管理' }
}
]
},
{
path: '/user',
name: 'user_workdesk',
component: () => import('../views/UserView.vue'),
// 标记只需要普通用户权限
meta: { requiresAuth: true, role: 'user' }
component: () => import('@/views/UserView.vue'),
meta: { role: 'user', title: '工作台' }
},
{
path: '/client',
name: 'client_monitor',
component: () => import('../views/ClientView.vue'),
// 标记只需要普通用户权限
meta: { requiresAuth: true, role: 'client' }
},
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue')
},
component: () => import('@/views/ClientView.vue'),
meta: { role: 'client', title: '客户监控' }
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: publicRoutes
})
// 全局前置守卫
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 字段来判断
const userRole = userInfo?.username === 'admin' ? 'admin' : 'user';
const isRequiresAuth = to.matched.some(record => record.meta.requiresAuth);
// 1. 未登录拦截
if (isRequiresAuth && !token) {
next({ name: 'login', query: { redirect: to.fullPath } });
return;
}
// 2. 已登录状态下的逻辑
if (token) {
// 如果想去登录页,根据角色重定向到对应首页
if (to.name === 'login') {
userRole === 'admin' ? next({ name: 'home' }) : next({ name: 'user_workdesk' });
return;
}
// --- 核心修复:权限校验 ---
// 检查目标路由是否定义了需要的角色
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;
/* 获取用户信息 */
function getUserInfo() {
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 roleHomeMap = {
admin: { name: 'home' },
user: { name: 'user_workdesk' },
client: { name: 'client_monitor' }
};
/* 获取角色的默认路由 */
function getRoleHomeRoute(role) {
return roleHomeMap[role?.toLowerCase()] || { path: '/login' }
}
// 3. 正常放行
next();
// ======动态路由管理======
let hasAddedRoutes = false; // 标记是否已添加动态路由
/* 添加动态路由 */
function addDynamicRoutes(role) {
try {
// 过滤当前角色可以访问的路由
const accessibleRoutes = dynamicRoutes.filter(route => {
return !route.meta?.role || route.meta.role === role;
});
export default router
// 添加路由
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');
}
hasAddedRoutes = false;
// 清除用户信息
localStorage.removeItem('user_info');
sessionStorage.removeItem('user_info');
}
// ======路由守卫======
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();
}
// 2.未登录访问受保护的页面
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;

49
src/utils/format.js Normal file
View 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}`
}

View File

@@ -1,72 +1,52 @@
import axios from 'axios'
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
// 请求拦截器
service.interceptors.request.use(
config => {
// 自动获取 Token(兼顾两种存储方式)
const token = localStorage.getItem('token') || sessionStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
function createService(basePath) {
const service = axios.create({
baseURL: `/api/${basePath}`,
timeout: 10000,
withCredentials: true,
})
service.interceptors.response.use(
response => response.data,
error => {
const resData = error.response?.data
const status = error.response?.status
// session过期处理
if (status === 401) {
if (!isRelogging) {
isRelogging = true
resetRouter()
ElMessage.error('登录已过期,请重新登录')
router.push({
name: 'login',
query: { redirect: router.currentRoute.value.fullPath }
}).finally(() => {
isRelogging = false
})
}
return new Promise(() => { })
}
const msg = resData?.message || '服务器响应异常'
ElMessage.error(msg)
return Promise.reject(error)
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
service.interceptors.response.use(
// 接口请求返回状态码是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 相关的认证错误
// 包含 500 状态码且消息含有 token 字样(如 decode 失败、过期等)
if (status === 401) {
if (!isRelogging) {
isRelogging = true; // 锁定
localStorage.removeItem('token');
// 清除用户信息,防止 UserView.vue 的 computed 报错
localStorage.removeItem('user_info');
ElMessage.error('登录已失效,请重新登录');
router.push('/login').then(() => {
// 2. 延迟解锁,确保页面跳转期间的所有并发请求都已返回并被此拦截器捕获
setTimeout(() => {
isRelogging = false;
}, 3000);
});
return service
}
return new Promise(() => { });
}
const msg = resData?.message || '服务器响应异常';
ElMessage.error(msg);
return Promise.reject(error);
}
);
export default service
export const adminRequest = createService('admin')
export const indexRequest = createService('index')
export default adminRequest

37
src/views/404View.vue Normal file
View 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>

View File

@@ -114,7 +114,7 @@
<el-table-column label="更新日期" width="140" align="center">
<template #default="scope">
<span class="date-text">{{
formatDateOnly(
formatDate(
scope.row.lock_time || scope.row.update_time || scope.row.create_time
)
}}</span>
@@ -143,8 +143,9 @@ import {
CircleCheck,
Monitor,
} from "@element-plus/icons-vue";
import { ticketApi } from "@/api/ticket";
import { ticketApi, userApi } from "@/api/ticket";
import { ElMessage } from "element-plus";
import { formatDate } from "@/utils/format";
const router = useRouter();
const loading = ref(false);
@@ -259,26 +260,18 @@ const getStatusClass = (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) => {
page.value = p;
window.scrollTo({ top: 0, behavior: "smooth" });
};
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
const handleLogout = async () => {
try {
await userApi.logout();
} catch (e) {}
localStorage.removeItem("user_info");
sessionStorage.removeItem("user_info");
router.push("/login");
};

View File

@@ -17,7 +17,7 @@
</div>
<div class="brand-text">
<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>
@@ -29,7 +29,7 @@
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" class="ark-form">
<el-form-item prop="username">
<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 prop="password">
@@ -64,17 +64,17 @@ import { ref, reactive } from "vue";
import { DataBoard } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { userApi } from "@/api/ticket";
import { useRouter } from "vue-router";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
const loginForm = reactive({ username: "", password: "" });
const rememberMe = ref(false);
const loading = ref(false);
const loginFormRef = ref(null);
const loginRules = reactive({
username: [{ required: true, message: "请输入工员编号", trigger: "blur" }],
username: [{ required: true, message: "请输入员工编号", trigger: "blur" }],
password: [{ required: true, message: "请输入密钥", trigger: "blur" }],
});
@@ -85,52 +85,32 @@ const handleLogin = async () => {
loading.value = true;
const res = await userApi.login(loginForm.username, loginForm.password);
// console.log(res)
// 注意:这里由于你之前的封装,可能需要判断 res.data.code 还是 res.code
// 如果用了拦截器,直接判断 res.code
if (res.code === 200) {
const token = res.data.token;
const userInfo = JSON.stringify(res.data.user_info);
const role = res.data.user_info.role;
// ThinkPHP 会自动通过 Set-Cookie 下发 session,浏览器自动接管,无需手动存 token
const userInfo = res.data;
const userInfoStr = JSON.stringify(userInfo);
// console.log(role)
// 用户信息存前端,仅用于显示用户名和角色路由判断
const storage = rememberMe.value ? localStorage : sessionStorage;
storage.setItem("user_info", userInfoStr);
// --- 核心逻辑开始 ---
if (rememberMe.value) {
// 勾选了:存入 localStorage,实现持久化
localStorage.setItem("token", token);
localStorage.setItem("user_info", userInfo);
ElMessage({
message: "身份验证通过,方舟系统加载中...",
type: "success",
duration: 300,
});
// 触发路由守卫,自动处理跳转
await router.push(route.query.redirect || "/");
} else {
// 没勾选:存入 sessionStorage,关闭标签页即失效
sessionStorage.setItem("token", token);
sessionStorage.setItem("user_info", userInfo);
}
// --- 核心逻辑结束 ---
ElMessage.success("身份验证通过,方舟系统加载中...");
// 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 {
ElMessage.error(res.message || "登录失败");
ElMessage.error(res.message || "登录失败,请检查账号和密码");
}
} catch (error) {
console.error("Login Error:", error);
if (error.name !== "ValidationError") {
ElMessage.error("登录请求异常,请稍后重试");
// console.error("登录错误:", error);
}
} finally {
loading.value = false;
}

View File

@@ -53,11 +53,18 @@
<div class="left">
<el-button type="primary" @click="handleAdd" :icon="Plus">新增任务</el-button>
</div>
<TicketFilter :operators="operatorsForFilter" :staffs="staffList" :filters="filters"
<TicketFilter :operators="operatorsForFilter" :staffs="userStaffList" :filters="filters"
@filter-change="handleFilterChange" @reset="handleFilterReset" />
</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">
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
@@ -66,14 +73,13 @@
</div>
</div>
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :customers="customerList"
:customer-employees="allCustomerEmployees" :staffs="staffList" :is-editing="isEditing"
@submit="handleFormSubmit" />
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :companiesWithOperators="companywithoperatorsList"
:user-staffs="userStaffList" :is-editing="isEditing" @submit="handleFormSubmit" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
Plus,
@@ -85,7 +91,7 @@ import {
} from "@element-plus/icons-vue";
// API 引入
import { ticketApi, customerApi } from "@/api/ticket";
import { ticketApi } from "@/api/ticket";
// 组件引入
import TicketFilter from "@/components/ticket/TicketFilter.vue";
@@ -94,14 +100,16 @@ import TicketForm from "@/components/ticket/TicketForm.vue";
// --- 响应式状态 ---
const tickets = ref([]);
const operators = ref([]);
const staffList = ref([]);
const customerList = ref([]);
const allCustomerEmployees = ref([]);
const userStaffList = ref([]);
const companywithoperatorsList = ref([]);
const loading = ref(false);
const showAddDialog = ref(false);
const isEditing = ref(false);
const currentTicket = ref({});
const selectedTickets = ref([]);
const ticketListRef = ref(null);
const operatorsForFilter = ref([]);
const allStats = reactive({
total: 0,
@@ -119,54 +127,56 @@ const pagination = reactive({
const filters = reactive({
status: null,
operator_name: null,
staff_id: null,
operator_id: null,
user_id: null,
search: null,
sort_order: "desc",
});
// --- 核心数据加载方法 ---
/* 获取运营人员数据的方法*/
// 在 TicketManagement.vue 中利用 computed 提取
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 () => {
const fetchOperatorsForFilter = async () => {
try {
const res = await customerApi.getCustomers();
// 适配点:根据返回的 code: 200 逻辑进行判断
if (res.code === 200) {
// 适配点:直接从 res 中获取 customers 和 employees 数组
customerList.value = res.customers || [];
allCustomerEmployees.value = res.employees || [];
const response = await ticketApi.getOperatorsNew();
if (response.code === 200) {
// 将新的嵌套数据结构转换为原来的扁平格式
operatorsForFilter.value = response.data.flatMap(company =>
company.companyoperators.map(op => ({
id: op.id,
operator_name: op.operator_name,
company_name: company.company_name
}))
);
}
} 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 {
const res = await ticketApi.getStaffs();
if (res.code) {
staffList.value = res.data;
const res = await ticketApi.getUserStaffs();
if (res.code === 200) {
userStaffList.value = res.data;
}
} catch (error) {
console.error("获取我方员工列表失败:", error);
@@ -178,13 +188,26 @@ const loadStaffData = async () => {
*/
const initGlobalStats = async () => {
try {
const response = await ticketApi.getticketsstatuscount();
if (response.code) {
allStats.total = response.data["total"];
allStats.pending = response.data["pending"];
allStats.ongoing = response.data["ongoing"];
allStats.completed = response.data["completed"];
allStats.is_completed = response.data["is_completed"];
const response = await ticketApi.getticketdashboardstats();
if (response.code === 200) {
allStats.total = response.data.total;
const statusCounts = response.data.status_counts || [];
statusCounts.forEach((item) => {
switch (item.status) {
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) {
console.error("统计全局数据失败:", error);
@@ -224,18 +247,20 @@ const refreshAll = () => {
const handleFilterChange = (newFilters) => {
Object.assign(filters, newFilters);
pagination.page = 1;
selectedTickets.value = [];
loadTickets();
};
const handleFilterReset = () => {
Object.assign(filters, {
status: null,
operator_name: null,
staff_id: null,
operator_id: null,
user_id: null,
search: null,
sort_order: "desc",
});
pagination.page = 1;
selectedTickets.value = [];
loadTickets();
};
@@ -289,7 +314,6 @@ const handleDelete = async (id) => {
cancelButtonText: "取消",
});
const res = await ticketApi.deleteTicket(id);
// console.log('删除',res)
if (res.code) {
ElMessage.success("删除成功");
refreshAll();
@@ -299,15 +323,28 @@ const handleDelete = async (id) => {
}
};
// 加载所有运营人员
const loadOperators = async () => {
const handleBatchFinish = async () => {
const ids = selectedTickets.value.map((t) => t.id);
try {
const response = await ticketApi.getOperators();
if (response.code) {
operators.value = response.data;
await ElMessageBox.confirm(
`确认将选中的 ${ids.length} 个工单一键已完结?`,
"批量操作",
{ 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) {
console.error("获取运营人员筛选列表失败:", error);
if (error !== "cancel") ElMessage.error("批量操作失败");
}
};
@@ -315,17 +352,18 @@ const handleSizeChange = (val) => {
pagination.limit = val;
loadTickets();
};
const handleCurrentChange = (val) => {
pagination.page = val;
loadTickets();
};
onMounted(() => {
fetchOperatorsForFilter();
loadTickets();
initGlobalStats();
loadOperators();
loadStaffData();
loadCustomerData(); // 级联数据加载
loadUserStaffData();
loadCompanyWithOperatorsData();
});
</script>
@@ -445,6 +483,22 @@ onMounted(() => {
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 {
margin-top: 20px;
display: flex;

View File

@@ -85,12 +85,18 @@
</template>
<el-table-column prop="id" label="ID" width="70" align="center" />
<el-table-column
prop="website"
label="网站名称"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="website" label="网站名称" min-width="150">
<template #default="{ row }">
<el-link
type="primary"
: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">
<template #default="scope">
@@ -255,10 +261,11 @@
<template #default="scope">
<span class="pure-date">
{{
(["已完成", "已完结"].includes(currentStatus)
formatDate(
["已完成", "已完结"].includes(currentStatus)
? scope.row.lock_time
: scope.row.create_time
)?.slice(0, 10) || "-"
)
}}
</span>
</template>
@@ -319,7 +326,9 @@ import {
Checked,
Loading,
} 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 loading = ref(false);
@@ -377,7 +386,6 @@ const fetchTickets = async () => {
loading.value = false;
return;
}
console.log("员工id", realStaffInfo.value.id);
params.staff_id = realStaffInfo.value.id;
}
@@ -511,10 +519,16 @@ const handleCancel = (row) => {
};
const handleLogout = () => {
ElMessageBox.confirm("确定退出当前账号?", "提示", { type: "warning" }).then(() => {
localStorage.clear();
sessionStorage.clear();
router.push("/login");
ElMessageBox.confirm("确定退出当前账号?", "提示", { type: "warning" }).then(async () => {
try {
await userApi.logout();
} catch (e) {}
localStorage.removeItem("user_info");
sessionStorage.removeItem("user_info");
resetRouter();
router.push({ name: "login" });
});
};

View File

@@ -19,9 +19,10 @@ export default defineConfig({
server: {
port: 8080, // 前端启动的端口
proxy: {
'/api': { // 所有以api开头的请求都会被代理
'/api': {
target: 'http://localhost',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
}