最新修改,初版

This commit is contained in:
2026-02-25 09:38:25 +08:00
parent 1f5bfba3b3
commit 0378198f55
15 changed files with 1691 additions and 824 deletions

409
src/views/UserView.vue Normal file
View File

@@ -0,0 +1,409 @@
<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" show-overflow-tooltip />
<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 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="100">100</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">
{{ formatDateOnly(['已完成', '已完结'].includes(currentStatus) ? scope.row.lock_time : scope.row.create_time) }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" 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>
<el-button v-if="scope.row.is_complete === '进行中' && String(scope.row.staff_id) === String(realStaffInfo?.id)" type="success" size="small" @click="handleFinish(scope.row)">完成任务</el-button>
</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 } from '@/api/ticket'
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)
const formatDateOnly = (timestamp) => {
if (!timestamp || timestamp === '0' || timestamp === '') return '-';
let date = !isNaN(timestamp) ? new Date(String(timestamp).length === 10 ? timestamp * 1000 : parseInt(timestamp)) : new Date(timestamp);
if (isNaN(date.getTime())) return '-';
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
}
// 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
try {
const res = await ticketApi.getTickets({ page: 1, limit: 1000 })
// console.log(res)
if (res.code === 200 || res.code === true) allTickets.value = res.data.data
} catch (e) { ElMessage.error('获取列表失败') }
finally { loading.value = false }
}
// 过滤工单
const filteredTickets = computed(() => {
return allTickets.value.filter(ticket => {
if (ticket.is_complete !== currentStatus.value) return false
if (currentStatus.value !== '未分配') return String(ticket.staff_id) === String(realStaffInfo.value?.id)
return true
})
})
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 = () => { currentPage.value = 1; 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 handleLogout = () => {
ElMessageBox.confirm('确定退出当前账号?', '提示', { type: 'warning' }).then(() => {
localStorage.clear(); sessionStorage.clear(); router.push('/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) {}
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>