Files
vue-tickets/src/views/UserView.vue
2026-05-14 15:41:00 +08:00

771 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="user-view-container">
<div class="user-header-card">
<div class="header-main">
<div class="welcome-container">
<div class="welcome-section">
<h2 class="greeting">{{ welcomeMessage }},{{ currentUser.username }}</h2>
<p class="sub-text">欢迎回到你的垃圾堆,开启牛马的一天。</p>
</div>
<div class="identity-badges">
<el-space :size="8">
<el-tag
v-if="realStaffInfo"
type="warning"
effect="plain"
round
size="small"
>
<el-icon>
<User />
</el-icon>
牛马身份: {{ realStaffInfo.name }}
</el-tag>
<el-tag v-else type="danger" round size="small"
>未录入员工表,无权接单</el-tag
>
</el-space>
</div>
</div>
</div>
<div class="header-action">
<el-button type="danger" plain @click="handleLogout">
<el-icon> <SwitchButton /> </el-icon>&nbsp;退出登录
</el-button>
</div>
</div>
<div class="status-tabs-wrapper">
<el-radio-group
v-model="currentStatus"
@change="handleStatusChange"
class="custom-radio-group"
>
<el-radio-button label="未分配">
<el-icon>
<Notification />
</el-icon>
待分配任务
</el-radio-button>
<el-radio-button label="进行中">
<el-icon>
<Loading v-if="loading" />
<Timer v-else />
</el-icon>
我的进行中
</el-radio-button>
<el-radio-button label="已完成">
<el-icon>
<CircleCheck />
</el-icon>
我的已完成
</el-radio-button>
<el-radio-button label="已完结">
<el-icon>
<Checked />
</el-icon>
我的已完结
</el-radio-button>
</el-radio-group>
</div>
<el-table
:data="paginatedTickets"
v-loading="loading"
border
style="width: 100%; margin-top: 20px"
class="custom-table"
>
<template #empty>
<el-empty :image-size="120" :description="emptyText">
<el-button type="primary" plain @click="fetchTickets">刷新列表</el-button>
</el-empty>
</template>
<el-table-column prop="id" label="ID" width="70" align="center" />
<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">
<div class="cell-content-card">
<div class="data-display-area">
<span
v-if="scope.row.datanum && scope.row.datanum !== '0'"
class="display-val"
>{{ scope.row.datanum }}</span
>
<span v-else class="no-data-placeholder">-</span>
</div>
<el-popover
v-if="currentStatus !== '未分配'"
placement="top"
:width="200"
trigger="click"
@show="prepareEdit(scope.row, 'datanum')"
>
<template #reference>
<el-button type="primary" size="small" link class="edit-link-btn">
{{
scope.row.datanum && scope.row.datanum !== "0"
? "[ 修改数据 ]"
: "[ 输入数据 ]"
}}
</el-button>
</template>
<div class="popover-content">
<div class="pop-header">录入站点数据量</div>
<el-input
v-model="editValue"
placeholder="请输入数值"
size="small"
clearable
/>
<div class="pop-footer">
<el-button
type="primary"
size="small"
@click="submitEdit(scope.row, 'datanum')"
>确认提交</el-button
>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column
v-if="currentStatus === '进行中'"
label="任务进度"
width="150"
align="center"
>
<template #default="scope">
<div class="cell-content-card">
<div class="data-display-area" style="width: 100%; padding: 0 10px">
<el-progress
:percentage="Number(scope.row.progress || 0)"
:status="Number(scope.row.progress) === 100 ? 'success' : ''"
:stroke-width="12"
striped
striped-flow
/>
</div>
<el-popover
v-if="currentStatus === '进行中'"
placement="top"
:width="240"
trigger="click"
@show="prepareEdit(scope.row, 'progress')"
>
<template #reference>
<el-button type="primary" size="small" link class="edit-link-btn">
[ 更新进度 ]
</el-button>
</template>
<div class="popover-content">
<div class="pop-header">调整当前进度 (%)</div>
<el-radio-group
v-model="editValue"
size="small"
class="progress-radio-group"
>
<el-radio-button :label="0">0</el-radio-button>
<el-radio-button :label="30">30</el-radio-button>
<el-radio-button :label="50">50</el-radio-button>
<el-radio-button :label="70">70</el-radio-button>
<el-radio-button :label="99">99</el-radio-button>
</el-radio-group>
<div class="pop-footer">
<el-button
type="primary"
size="small"
@click="submitEdit(scope.row, 'progress')"
>确定</el-button
>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column prop="operator" label="运营" width="120" align="center" />
<el-table-column label="备注" min-width="150" align="center">
<template #default="scope">
<div class="cell-content-card">
<div class="data-display-area">
<div
v-if="scope.row.remark"
class="display-remark"
:title="scope.row.remark"
>
{{ scope.row.remark }}
</div>
<span v-else class="no-data-placeholder">-</span>
</div>
<el-popover
v-if="currentStatus !== '未分配'"
placement="top"
:width="250"
trigger="click"
@show="prepareEdit(scope.row, 'remark')"
>
<template #reference>
<el-button type="warning" size="small" link class="edit-link-btn">
{{ scope.row.remark ? "[ 修改备注 ]" : "[ 输入备注 ]" }}
</el-button>
</template>
<div class="popover-content">
<div class="pop-header">录入工单备注</div>
<el-input
v-model="editValue"
type="textarea"
:rows="3"
placeholder="请输入备注"
size="small"
/>
<div class="pop-footer">
<el-button
type="primary"
size="small"
@click="submitEdit(scope.row, 'remark')"
>提交保存</el-button
>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column
:label="['已完成', '已完结'].includes(currentStatus) ? '锁定日期' : '创建日期'"
width="140"
align="center"
>
<template #default="scope">
<span class="pure-date">
{{
formatDate(
["已完成", "已完结"].includes(currentStatus)
? scope.row.lock_time
: scope.row.create_time
)
}}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="190" fixed="right" align="center">
<template #default="scope">
<el-button
v-if="scope.row.is_complete === '未分配'"
type="primary"
size="small"
:disabled="!realStaffInfo"
@click="handleAccept(scope.row)"
>立即接单</el-button
>
<template
v-if="
scope.row.is_complete === '进行中' &&
String(scope.row.staff_id) === String(realStaffInfo?.id)
"
>
<el-button type="success" size="small" @click="handleFinish(scope.row)"
>完成任务</el-button
>
<el-button type="warning" plain size="small" @click="handleCancel(scope.row)"
>取消接单</el-button
>
</template>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="filteredTickets.length"
:page-sizes="[15, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import {
User,
SwitchButton,
Timer,
Notification,
CircleCheck,
Checked,
Loading,
} from "@element-plus/icons-vue";
import { ticketApi, userApi } from "@/api/ticket";
import { resetRouter } from "@/router";
import { formatUrl, formatDate } from "@/utils/format";
const router = useRouter();
const loading = ref(false);
const allTickets = ref([]);
const currentStatus = ref("未分配");
const realStaffInfo = ref(null);
const editValue = ref("");
const currentPage = ref(1);
const pageSize = ref(15);
// js计算时间
const welcomeMessage = computed(() => {
const hour = new Date().getHours();
if (hour < 6) return "凌晨好";
if (hour < 9) return "早上好";
if (hour < 12) return "上午好";
if (hour < 14) return "中午好";
if (hour < 18) return "下午好";
return "晚上好";
});
// 获取当前用户信息
const currentUser = computed(() => {
const info = localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
return info ? JSON.parse(info) : { id: null, username: "未知" };
});
// 获取工单信息
const fetchTickets = async () => {
loading.value = true;
// 优化:不再获取所有工单,而是根据当前视图的状态和用户ID进行筛选
try {
const statusMap = {
未分配: 0,
进行中: 1,
已完成: 2,
已完结: 3,
};
const statusValue =
statusMap[currentStatus.value] !== undefined
? statusMap[currentStatus.value]
: currentStatus.value;
const params = {
status: statusValue,
limit: 1000, // 仍然获取足够的数据以支持前端分页
};
// 对于非“未分配”的视图,只拉取当前登录员工的工单
if (currentStatus.value !== "未分配") {
if (!realStaffInfo.value?.id) {
allTickets.value = []; // 如果没有员工信息,则清空列表
loading.value = false;
return;
}
params.staff_id = realStaffInfo.value.id;
}
const res = await ticketApi.getTickets(params);
if (res.code === 200 || res.code === true) {
allTickets.value = res.data.data || [];
} else {
allTickets.value = [];
ElMessage.error(res.msg || "获取列表失败");
}
} catch (e) {
allTickets.value = [];
ElMessage.error("获取列表失败");
} finally {
loading.value = false;
}
};
// 过滤工单
const filteredTickets = computed(() => {
// 优化:由于 fetchTickets 已经从后端获取了经过筛选的数据,
// allTickets 本身就是过滤后的列表,无需再在客户端进行过滤。
return allTickets.value;
});
const paginatedTickets = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredTickets.value.slice(start, end);
});
const handleSizeChange = (val) => {
pageSize.value = val;
currentPage.value = 1;
};
const handleCurrentChange = (val) => {
currentPage.value = val;
};
const handleStatusChange = async () => {
currentPage.value = 1;
await fetchTickets();
};
const prepareEdit = (row, field) => {
editValue.value = row[field] || "";
};
const submitEdit = async (row, field) => {
try {
const payload = {
datanum: field === "datanum" ? String(editValue.value) : String(row.datanum || ""),
remark: field === "remark" ? editValue.value : row.remark || "",
progress:
field === "progress" ? Number(editValue.value) : Number(row.progress || 0),
};
const res = await ticketApi.updateTicket(row.id, payload);
if (res.code === 200 || res.code === true) {
row[field] = editValue.value;
ElMessage.success("更新成功");
document.body.click();
}
} catch (e) {
ElMessage.error("操作失败");
}
};
// 点击接受工单
const handleAccept = (row) => {
ElMessageBox.confirm(`确认接手工单 #${row.id}?`).then(async () => {
try {
const res = await ticketApi.updateTicket(row.id, {
staff_id: realStaffInfo.value.id,
is_complete: "进行中",
progress: 0,
});
if (res.code === 200 || res.code === true) {
ElMessage.success("接单成功");
fetchTickets();
}
} catch (e) {
ElMessage.error("操作失败");
}
});
};
// 点击完成
const handleFinish = (row) => {
ElMessageBox.confirm("确认已完成任务?").then(async () => {
try {
const currentTimestamp = Math.floor(Date.now() / 1000);
// 关键修改:在提交时增加 progress: 100
const res = await ticketApi.updateTicket(row.id, {
is_complete: "已完成",
lock_time: String(currentTimestamp),
progress: 100, // 默认改为100
});
if (res.code === 200 || res.code === true) {
ElMessage.success("任务已完成,进度已更新为100%");
fetchTickets();
}
} catch (e) {
ElMessage.error("操作失败");
}
});
};
// 新增:取消接单
const handleCancel = (row) => {
ElMessageBox.confirm(
`确认要取消接手此工单 #${row.id} 吗?工单将退回未分配列表。`,
"取消确认",
{ type: "warning" }
).then(async () => {
try {
const res = await ticketApi.updateTicket(row.id, {
staff_id: null,
is_complete: "未分配",
progress: 0,
});
if (res.code === 200 || res.code === true) {
ElMessage.success("已取消接单,工单已退回!");
fetchTickets();
} else {
ElMessage.error(res.msg || "操作失败");
}
} catch (e) {
ElMessage.error("操作失败");
}
});
};
const handleLogout = () => {
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" });
});
};
onMounted(async () => {
try {
const res = await ticketApi.getStaffs();
if (res.code === 200 || res.code === true) {
const match = (res.data || []).find((s) => s.name === currentUser.value.username);
if (match) realStaffInfo.value = match;
}
} catch (e) {}
await fetchTickets();
});
</script>
<style scoped>
/* 容器布局 */
.header-main {
display: flex;
flex-direction: column;
justify-content: center;
}
.welcome-container {
display: flex;
flex-direction: column;
gap: 8px;
/* 这里统一控制欢迎语、副标题、标签之间的垂直间距 */
}
/* 清除标题默认的垂直 margin,防止撑开间距 */
.greeting {
margin: 0;
color: #1a1a1a;
font-size: 22px;
line-height: 1.2;
}
/* 副标题紧贴标题 */
.sub-text {
margin: 4px 0 0 0;
/* 仅顶部保留微小间距 */
color: #8c8c8c;
font-size: 13px;
}
/* 标签组位置微调 */
.identity-badges {
margin-top: 4px;
/* 让标签组靠近上方的文字 */
display: flex;
align-items: center;
}
/* 针对 el-tag 内部图标和文字的微调 */
:deep(.el-tag) {
display: inline-flex;
align-items: center;
border-style: dashed;
/* 既然是牛马,边框可以搞成虚线更有自嘲感 */
}
.user-view-container {
padding: 24px;
background-color: #f0f2f5;
min-height: 100vh;
}
.user-header-card {
background: white;
padding: 24px;
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.greeting {
margin: 0 0 8px 0;
color: #1a1a1a;
font-size: 24px;
}
.status-tabs-wrapper {
background: white;
padding: 8px;
border-radius: 8px;
display: inline-block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.custom-radio-group :deep(.el-radio-button__inner) {
border: none !important;
border-radius: 6px !important;
margin: 0 4px;
padding: 10px 20px;
display: flex;
align-items: center;
gap: 6px;
}
.custom-table {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
}
.cell-content-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 60px;
}
.data-display-area {
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.display-val {
font-weight: 700;
color: #409eff;
font-size: 15px;
}
.display-remark {
font-size: 12px;
color: #595959;
max-width: 180px;
line-height: 1.4;
background: #f5f7fa;
padding: 4px 10px;
border-radius: 4px;
text-align: center;
}
.no-data-placeholder {
color: #dcdfe6;
font-size: 12px;
}
.pure-date {
color: #606266;
font-weight: 500;
font-family: monospace;
font-size: 14px;
}
.edit-link-btn {
font-size: 12px;
height: auto;
padding: 0;
opacity: 0.7;
}
.edit-link-btn:hover {
opacity: 1;
text-decoration: underline;
}
.pagination-container {
margin-top: 24px;
display: flex;
justify-content: flex-end;
padding: 10px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.progress-radio-group {
display: flex;
justify-content: center;
margin: 10px 0;
}
.progress-radio-group :deep(.el-radio-button__inner) {
padding: 8px 10px !important;
}
:deep(.el-table__row) {
height: 100px;
}
:deep(.el-table .cell) {
overflow: visible !important;
}
.pop-header {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #434343;
text-align: center;
}
.pop-footer {
margin-top: 12px;
text-align: right;
}
/* 1. 强行修正进度条内部容器的对齐方式 */
:deep(.el-progress-bar__innerText) {
display: flex !important;
align-items: center !important;
/* 垂直居中 */
justify-content: center !important;
/* 水平居中 */
height: 100% !important;
line-height: 1 !important;
/* 移除默认的基线对齐 */
vertical-align: middle !important;
}
/* 2. 针对 100% 时出现的那个 svg 图标进行位移修正 */
:deep(.el-progress-bar__innerText svg),
:deep(.el-progress-bar__innerText .el-icon) {
margin: 0 !important;
/* 关键:利用相对定位向上平移 1px,抵消视觉上的下沉 */
position: relative !important;
top: -1px !important;
/* 稍微调整图标大小,让它在 10px 的进度条里更协调 */
width: 0.8em;
height: 0.8em;
}
/* 3. 如果还是觉得整体偏,可以给进度条容器加一个微调 */
:deep(.el-progress) {
display: flex;
align-items: center;
}
</style>