555 lines
14 KiB
Vue
555 lines
14 KiB
Vue
<template>
|
||
<div class="app-container account-page ops-page">
|
||
<div class="page-header">
|
||
<div>
|
||
<div class="page-header__title">BOSS 账号管理</div>
|
||
<div class="page-header__desc">统一管理账号登录状态、Worker 在线状态和任务执行进度</div>
|
||
</div>
|
||
<div class="page-header__actions ops-actions">
|
||
<el-button type="primary" icon="plus" class="ops-btn" @click="handleOpenDialog()">
|
||
新增账号环境
|
||
</el-button>
|
||
<el-button icon="refresh" class="ops-btn" @click="fetchData()">刷新数据</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overview-grid">
|
||
<el-card
|
||
v-for="card in overviewCards"
|
||
:key="card.label"
|
||
shadow="hover"
|
||
class="overview-card ops-card"
|
||
:class="card.tone"
|
||
>
|
||
<div class="overview-card__icon">
|
||
<el-icon><component :is="card.icon" /></el-icon>
|
||
</div>
|
||
<div class="overview-card__content">
|
||
<p>{{ card.label }}</p>
|
||
<h3>{{ card.value }}</h3>
|
||
<span>{{ card.sub }}</span>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
|
||
<el-card shadow="hover" class="table-panel ops-card">
|
||
<div class="table-panel__toolbar ops-toolbar">
|
||
<div class="table-tip">系统每 15 秒自动刷新账号与 Worker 状态</div>
|
||
<el-tag type="info" effect="light">已展开任务:{{ expandedRowKeys.length }}</el-tag>
|
||
</div>
|
||
|
||
<el-table
|
||
ref="dataTableRef"
|
||
v-loading="loading"
|
||
:data="roleList"
|
||
stripe
|
||
highlight-current-row
|
||
row-key="id"
|
||
border
|
||
empty-text="暂无账号数据"
|
||
:expanded-row-keys="expandedRowKeys"
|
||
class="account-table ops-table"
|
||
@expand-change="handleExpandChange"
|
||
>
|
||
<el-table-column type="expand">
|
||
<template #default="scope">
|
||
<ViewTheTaskListDetails
|
||
v-if="expandedRowKeys.includes(scope.row.id)"
|
||
ref="taskListRef"
|
||
:boss-id="scope.row.id"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="环境名称" prop="browser_name" min-width="150" />
|
||
<el-table-column label="账号ID" prop="id" width="110" />
|
||
<el-table-column label="电脑标识" prop="worker_id" min-width="150" />
|
||
<el-table-column label="登录昵称" prop="boss_username" min-width="120" />
|
||
<el-table-column label="电脑名称" prop="worker_name" min-width="120">
|
||
<template #default="scope">
|
||
<span>{{ scope.row.worker_name || '--' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="是否登录 BOSS" prop="is_logged_in" width="130" align="center">
|
||
<template #default="scope">
|
||
<el-tag
|
||
:type="scope.row.is_logged_in ? 'success' : 'danger'"
|
||
effect="light"
|
||
class="status-tag ops-status-tag"
|
||
>
|
||
{{ scope.row.is_logged_in ? '在线' : '离线' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="Worker 状态" prop="worker_online" width="120" align="center">
|
||
<template #default="scope">
|
||
<el-tag
|
||
:type="scope.row.worker_online ? 'success' : 'danger'"
|
||
effect="light"
|
||
class="status-tag ops-status-tag"
|
||
>
|
||
{{ scope.row.worker_online ? '在线' : '离线' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="任务状态" prop="current_task_status" width="110" align="center">
|
||
<template #default="scope">
|
||
<el-tag
|
||
v-if="scope.row.current_task_status"
|
||
:type="getTaskStatusMeta(scope.row.current_task_status).type"
|
||
effect="light"
|
||
class="status-tag ops-status-tag"
|
||
>
|
||
{{ getTaskStatusMeta(scope.row.current_task_status).label }}
|
||
</el-tag>
|
||
<span v-else>--</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="更新时间" prop="updated_at" min-width="170">
|
||
<template #default="scope">
|
||
{{ scope.row?.updated_at ? formatISOToDateTime(scope.row.updated_at) : '--' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column fixed="right" label="操作" width="360">
|
||
<template #default="scope">
|
||
<div class="action-group ops-actions">
|
||
<el-button
|
||
type="warning"
|
||
size="small"
|
||
class="ops-btn"
|
||
@click="onApiTasksAdd(scope.row.id, 'boss_reply', {})"
|
||
>
|
||
回复
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
class="ops-btn"
|
||
@click="onApiTasksAdd(scope.row.id, 'check_login', {})"
|
||
>
|
||
检查登录
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
size="small"
|
||
class="ops-btn"
|
||
@click="openRecruitDialog(scope.row.id)"
|
||
>
|
||
招聘
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
link
|
||
icon="edit"
|
||
@click="handleExpandTask(scope.row.id)"
|
||
>
|
||
{{ expandedRowKeys.includes(scope.row.id) ? '收起任务' : '展开任务' }}
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
size="small"
|
||
link
|
||
icon="delete"
|
||
@click="onUserDeleteDepartment(scope.row.id)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { RolePageVO } from '@/api/system/role-api'
|
||
import { functionDialogBox } from '@/utils/functionDialogBox'
|
||
import BoosAccountForm from './components/BoosAccountForm.vue'
|
||
import RecruitTaskForm from './components/RecruitTaskForm.vue'
|
||
import { BusinessEditApplication } from '@/api/calibration/applicationForSealApproval'
|
||
import { ApiAccounts, ApiAccountsAdd, ApiAccountsDelete } from '@/api/BoosAccountManagement'
|
||
import ViewTheTaskListDetails from './components/ViewTheTaskListDetails.vue'
|
||
import { ApiTasksAdd } from '@/api/TaskManagement'
|
||
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
|
||
import { createTimer } from '@/utils/TimerManager'
|
||
import { User, Monitor, ChatDotRound, Message } from '@element-plus/icons-vue'
|
||
|
||
defineOptions({
|
||
name: 'Role',
|
||
inheritAttrs: false
|
||
})
|
||
|
||
const { startTimer, stopTimer } = createTimer()
|
||
const dataTableRef = ref()
|
||
const taskListRef = ref()
|
||
const loading = ref(false)
|
||
const roleList = ref<RolePageVO[]>([])
|
||
const expandedRowKeys = ref<Array<string | number>>([])
|
||
|
||
const TASK_STATUS_MAP: Record<
|
||
string,
|
||
{
|
||
label: string
|
||
type: 'primary' | 'success' | 'warning' | 'info' | 'danger'
|
||
}
|
||
> = {
|
||
pending: { label: '待派发', type: 'info' },
|
||
dispatched: { label: '已派发', type: 'warning' },
|
||
running: { label: '执行中', type: 'primary' },
|
||
success: { label: '成功', type: 'success' },
|
||
failed: { label: '失败', type: 'danger' }
|
||
}
|
||
|
||
const getTaskStatusMeta = (status: string) =>
|
||
TASK_STATUS_MAP[status] || {
|
||
label: '--',
|
||
type: 'info'
|
||
}
|
||
|
||
const accountStats = computed(() => {
|
||
const list = roleList.value || []
|
||
return {
|
||
total: list.length,
|
||
loggedIn: list.filter((item: any) => Boolean(item.is_logged_in)).length,
|
||
workerOnline: list.filter((item: any) => Boolean(item.worker_online)).length,
|
||
runningTasks: list.filter((item: any) => item.current_task_status === 'running').length
|
||
}
|
||
})
|
||
|
||
const overviewCards = computed(() => [
|
||
{
|
||
label: '账号总数',
|
||
value: accountStats.value.total,
|
||
sub: `已登录:${accountStats.value.loggedIn}`,
|
||
icon: User,
|
||
tone: 'tone-1'
|
||
},
|
||
{
|
||
label: '在线 Worker',
|
||
value: accountStats.value.workerOnline,
|
||
sub: `总 Worker:${accountStats.value.total}`,
|
||
icon: Monitor,
|
||
tone: 'tone-2'
|
||
},
|
||
{
|
||
label: '运行中任务',
|
||
value: accountStats.value.runningTasks,
|
||
sub: '任务状态实时同步',
|
||
icon: ChatDotRound,
|
||
tone: 'tone-3'
|
||
},
|
||
{
|
||
label: '待处理账号',
|
||
value: Math.max(accountStats.value.total - accountStats.value.loggedIn, 0),
|
||
sub: '建议优先检查登录',
|
||
icon: Message,
|
||
tone: 'tone-4'
|
||
}
|
||
])
|
||
|
||
function fetchData(silent = false) {
|
||
if (!silent) {
|
||
loading.value = true
|
||
}
|
||
ApiAccounts()
|
||
.then((res: any) => {
|
||
roleList.value = res.data || []
|
||
})
|
||
.finally(() => {
|
||
if (!silent) {
|
||
loading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
function handleOpenDialog(data: any = null) {
|
||
if (data) {
|
||
functionDialogBox(
|
||
BoosAccountForm,
|
||
{
|
||
newData: data
|
||
},
|
||
{
|
||
title: '编辑申请用印',
|
||
width: '900',
|
||
ok(value: any) {
|
||
handleSubmit({ id: data.id, ...value })
|
||
}
|
||
}
|
||
)
|
||
} else {
|
||
functionDialogBox(
|
||
BoosAccountForm,
|
||
{},
|
||
{
|
||
title: '绑定账号到电脑',
|
||
width: '900',
|
||
ok(value: any) {
|
||
handleSubmit(value)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
function handleSubmit(data: any) {
|
||
loading.value = true
|
||
const roleId = data.id
|
||
if (roleId) {
|
||
BusinessEditApplication(data)
|
||
.then(() => {
|
||
ElMessage.success('修改成功')
|
||
fetchData()
|
||
})
|
||
.finally(() => (loading.value = false))
|
||
} else {
|
||
ApiAccountsAdd(data)
|
||
.then(() => {
|
||
ElMessage.success('操作成功')
|
||
fetchData()
|
||
})
|
||
.finally(() => (loading.value = false))
|
||
}
|
||
}
|
||
|
||
const onUserDeleteDepartment = (id: string) => {
|
||
ElMessageBox.confirm('确认删除已选中的数据项?', '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(
|
||
() => {
|
||
loading.value = true
|
||
ApiAccountsDelete(id)
|
||
.then(() => {
|
||
ElMessage.success('删除成功')
|
||
fetchData()
|
||
})
|
||
.finally(() => (loading.value = false))
|
||
},
|
||
() => {
|
||
ElMessage.info('已取消删除')
|
||
}
|
||
)
|
||
}
|
||
|
||
const openRecruitDialog = (id: string) => {
|
||
functionDialogBox(
|
||
RecruitTaskForm,
|
||
{
|
||
accountId: id
|
||
},
|
||
{
|
||
title: '招聘参数',
|
||
width: '620',
|
||
ok(value: any) {
|
||
onApiTasksAdd(id, 'boss_recruit', value || {})
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
const onApiTasksAdd = (id: string, task_type: string, params: any = {}) => {
|
||
loading.value = true
|
||
ApiTasksAdd({ params, task_type, boss_id: id })
|
||
.then((res: any) => {
|
||
ElMessage.success(res.msg)
|
||
fetchData()
|
||
refreshTaskList()
|
||
})
|
||
.finally(() => (loading.value = false))
|
||
}
|
||
|
||
function handleExpandChange(_row: any, expandedRows: any[]) {
|
||
expandedRowKeys.value = expandedRows.map((item) => item.id)
|
||
}
|
||
|
||
const handleExpandTask = (rowId: string | number) => {
|
||
const targetRow = roleList.value?.find((item: any) => item.id === rowId)
|
||
if (targetRow && dataTableRef.value) {
|
||
dataTableRef.value.toggleRowExpansion(targetRow)
|
||
}
|
||
}
|
||
|
||
const refreshTaskList = () => {
|
||
taskListRef.value?.fetchData()
|
||
}
|
||
|
||
onMounted(() => {
|
||
// fetchData()
|
||
startTimer(() => {
|
||
fetchData(true)
|
||
}, 15000)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopTimer()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.account-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
gap: 14px;
|
||
flex-wrap: wrap;
|
||
|
||
&__title {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #1f2d3d;
|
||
letter-spacing: 0.2px;
|
||
}
|
||
|
||
&__desc {
|
||
margin-top: 4px;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
&__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
.overview-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
|
||
.overview-card {
|
||
border: 1px solid #ebeef5;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
:deep(.el-card__body) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
}
|
||
|
||
&__icon {
|
||
width: 46px;
|
||
height: 46px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
color: #fff;
|
||
font-size: 20px;
|
||
}
|
||
|
||
&__content {
|
||
p {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
h3 {
|
||
margin: 6px 0 4px;
|
||
font-size: 28px;
|
||
color: #111827;
|
||
line-height: 1;
|
||
}
|
||
|
||
span {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tone-1 .overview-card__icon {
|
||
background: linear-gradient(135deg, #7f7fd5 0%, #9156e8 100%);
|
||
}
|
||
|
||
.tone-2 .overview-card__icon {
|
||
background: linear-gradient(135deg, #56bafd 0%, #16e9fd 100%);
|
||
}
|
||
|
||
.tone-3 .overview-card__icon {
|
||
background: linear-gradient(135deg, #f199ee 0%, #ef6b8b 100%);
|
||
}
|
||
|
||
.tone-4 .overview-card__icon {
|
||
background: linear-gradient(135deg, #36d1dc 0%, #5b86e5 100%);
|
||
}
|
||
|
||
.table-panel {
|
||
border: 1px solid #ebeef5;
|
||
|
||
:deep(.el-card__body) {
|
||
padding: 14px 16px 16px;
|
||
}
|
||
|
||
&__toolbar {
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
.table-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.account-table {
|
||
:deep(.el-table__header th) {
|
||
background: #f6f8fc;
|
||
}
|
||
|
||
:deep(.el-table__row:hover > td) {
|
||
background: #f5f9ff !important;
|
||
}
|
||
}
|
||
|
||
.action-group {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.status-tag {
|
||
min-width: 62px;
|
||
justify-content: center;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.page-header {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.page-header__title {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.page-header__actions,
|
||
.table-panel__toolbar {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
flex-wrap: wrap;
|
||
}
|
||
}
|
||
</style>
|