Files
boss_font/src/views/BoosAccountManagement/index.vue
2026-03-06 17:13:06 +08:00

555 lines
14 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="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>