feat: 引入 ECharts 统一图表并完善员工画像标签分页
后端优化员工行为画像服务和辅助函数,完善系统设置模型和 配置持久化,前端引入 ECharts 替换所有图表组件实现统一 渲染,新增员工画像标签分页器和数字员工工作记录组件,优 化工作台响应式布局和登录页过渡动画,完善预算中心和数字 员工页面样式细节。
This commit is contained in:
332
web/src/components/audit/DigitalEmployeeWorkRecords.vue
Normal file
332
web/src/components/audit/DigitalEmployeeWorkRecords.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<section class="digital-work-records">
|
||||
<header class="work-records-head">
|
||||
<div>
|
||||
<h3>工作记录</h3>
|
||||
<p>查看数字员工近期执行记录、状态和结果摘要。</p>
|
||||
</div>
|
||||
|
||||
<div class="work-records-kpis" aria-label="工作记录统计">
|
||||
<article class="work-record-kpi">
|
||||
<span>日志总数</span>
|
||||
<strong>{{ totalCount }}</strong>
|
||||
</article>
|
||||
<article class="work-record-kpi success">
|
||||
<span>成功数量</span>
|
||||
<strong>{{ successCount }}</strong>
|
||||
</article>
|
||||
<article class="work-record-kpi danger">
|
||||
<span>失败数量</span>
|
||||
<strong>{{ failedCount }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="work-records-toolbar">
|
||||
<span>{{ loading ? '正在同步工作记录' : `当前展示 ${visibleRuns.length} 条记录` }}</span>
|
||||
<button type="button" :disabled="loading" @click="loadWorkRecords(true)">
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap work-records-table-wrap" :class="{ 'is-empty': !loading && !runs.length }">
|
||||
<div v-if="loading && !runs.length" class="table-state">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
title="工作记录同步中"
|
||||
message="正在读取数字员工近期执行记录"
|
||||
icon="mdi mdi-clipboard-text-clock-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="table-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<strong>工作记录加载失败</strong>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
|
||||
<table v-else-if="runs.length" class="digital-work-records-table">
|
||||
<colgroup>
|
||||
<col class="col-time">
|
||||
<col class="col-module">
|
||||
<col class="col-source">
|
||||
<col class="col-status">
|
||||
<col class="col-summary">
|
||||
<col class="col-trace">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>执行时间</th>
|
||||
<th>工作模块</th>
|
||||
<th>触发来源</th>
|
||||
<th>状态</th>
|
||||
<th>摘要</th>
|
||||
<th>Run ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="run in visibleRuns"
|
||||
:key="run.run_id"
|
||||
class="work-record-row"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@click="openWorkRecordDetail(run)"
|
||||
@keydown.enter.prevent="openWorkRecordDetail(run)"
|
||||
>
|
||||
<td>{{ formatWorkRecordDateTime(run.started_at) }}</td>
|
||||
<td>{{ resolveWorkRecordModuleLabel(run) }}</td>
|
||||
<td>{{ resolveWorkRecordSourceLabel(run.source) }}</td>
|
||||
<td>
|
||||
<div class="work-record-status-stack">
|
||||
<span class="status-pill" :class="resolveWorkRecordStatusTone(run)">
|
||||
{{ resolveWorkRecordStatusLabel(run) }}
|
||||
</span>
|
||||
<span>{{ resolveWorkRecordStatusNote(run) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="work-record-summary-cell">
|
||||
<strong>{{ resolveWorkRecordTitle(run) }}</strong>
|
||||
<span>{{ formatWorkRecordSummary(run.result_summary) }}</span>
|
||||
<em>{{ resolveWorkRecordSummaryMeta(run) }}</em>
|
||||
</td>
|
||||
<td class="work-record-trace-cell">{{ run.run_id }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-else class="work-records-empty">
|
||||
当前还没有数字员工工作记录。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="work-record-detail">
|
||||
<div
|
||||
v-if="detailOpen"
|
||||
class="work-record-detail-mask"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="工作记录详情"
|
||||
@click.self="closeWorkRecordDetail"
|
||||
>
|
||||
<aside class="work-record-detail-panel">
|
||||
<header class="work-record-detail-head">
|
||||
<div>
|
||||
<span>工作记录详情</span>
|
||||
<h3>{{ selectedRunDetail ? resolveWorkRecordTitle(selectedRunDetail) : '工作记录' }}</h3>
|
||||
</div>
|
||||
<button type="button" aria-label="关闭工作记录详情" @click="closeWorkRecordDetail">
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="detailLoading" class="work-record-detail-state">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
title="详情加载中"
|
||||
message="正在读取该次工作记录的完整执行信息"
|
||||
icon="mdi mdi-clipboard-text-search-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="detailError" class="work-record-detail-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<strong>工作记录详情加载失败</strong>
|
||||
<p>{{ detailError }}</p>
|
||||
<button type="button" @click="reloadSelectedDetail">重新加载</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedRunDetail" class="work-record-detail-body">
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>基本信息</h4>
|
||||
<span class="status-pill" :class="resolveWorkRecordStatusTone(selectedRunDetail)">
|
||||
{{ resolveWorkRecordStatusLabel(selectedRunDetail) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="work-record-info-grid">
|
||||
<div><span>Run ID</span><strong>{{ selectedRunDetail.run_id }}</strong></div>
|
||||
<div><span>工作模块</span><strong>{{ resolveWorkRecordModuleLabel(selectedRunDetail) }}</strong></div>
|
||||
<div><span>触发来源</span><strong>{{ resolveWorkRecordSourceLabel(selectedRunDetail.source) }}</strong></div>
|
||||
<div><span>开始时间</span><strong>{{ formatWorkRecordDateTime(selectedRunDetail.started_at) }}</strong></div>
|
||||
<div><span>结束时间</span><strong>{{ formatWorkRecordDateTime(selectedRunDetail.finished_at) }}</strong></div>
|
||||
<div><span>状态说明</span><strong>{{ resolveWorkRecordStatusNote(selectedRunDetail) }}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>执行摘要</h4>
|
||||
<span>{{ resolveWorkRecordSummaryMeta(selectedRunDetail) }}</span>
|
||||
</div>
|
||||
<p class="work-record-result-text">
|
||||
{{ selectedRunDetail.result_summary || '暂无执行摘要。' }}
|
||||
</p>
|
||||
<p v-if="selectedRunDetail.error_message" class="work-record-error-text">
|
||||
{{ selectedRunDetail.error_message }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>工具调用</h4>
|
||||
<span>{{ (selectedRunDetail.tool_calls || []).length }} 条</span>
|
||||
</div>
|
||||
<div v-if="(selectedRunDetail.tool_calls || []).length" class="work-record-tool-list">
|
||||
<article v-for="toolCall in selectedRunDetail.tool_calls" :key="toolCall.id">
|
||||
<strong>{{ toolCall.tool_name }}</strong>
|
||||
<span>{{ toolCall.tool_type || 'tool' }} · {{ toolCall.status || 'unknown' }}</span>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="work-record-inline-empty">当前暂无工具调用明细。</div>
|
||||
</section>
|
||||
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>执行上下文</h4>
|
||||
<span>JSON</span>
|
||||
</div>
|
||||
<pre class="work-record-code-block">{{ formatJson(selectedRunDetail.route_json) }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
||||
import { fetchAgentRunDetail, fetchAgentRuns } from '../../services/agentAssets.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import { AGENT_RUN_POLL_INTERVAL_MS } from '../../utils/agentRunMonitor.js'
|
||||
import {
|
||||
formatWorkRecordDateTime,
|
||||
formatWorkRecordSummary,
|
||||
resolveWorkRecordModuleLabel,
|
||||
resolveWorkRecordSourceLabel,
|
||||
resolveWorkRecordStatusLabel,
|
||||
resolveWorkRecordStatusNote,
|
||||
resolveWorkRecordStatusTone,
|
||||
resolveWorkRecordSummaryMeta,
|
||||
resolveWorkRecordTitle
|
||||
} from '../../views/scripts/digitalEmployeeWorkRecordsModel.js'
|
||||
|
||||
defineOptions({
|
||||
name: 'DigitalEmployeeWorkRecords'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['summary-change'])
|
||||
|
||||
const { toast } = useToast()
|
||||
const runs = ref([])
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref('')
|
||||
const selectedRunId = ref('')
|
||||
const selectedRunDetail = ref(null)
|
||||
let pollTimer = 0
|
||||
|
||||
const totalCount = computed(() => runs.value.length)
|
||||
const successCount = computed(() => runs.value.filter((run) => run.status === 'succeeded').length)
|
||||
const failedCount = computed(() => runs.value.filter((run) => run.status === 'failed').length)
|
||||
const visibleRuns = computed(() => runs.value.slice(0, 100))
|
||||
|
||||
async function loadWorkRecords(showToast = false) {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const payload = await fetchAgentRuns({ agent: 'hermes', limit: 100 })
|
||||
runs.value = Array.isArray(payload) ? payload : []
|
||||
emit('summary-change', {
|
||||
total: totalCount.value,
|
||||
succeeded: successCount.value,
|
||||
failed: failedCount.value
|
||||
})
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.message || '工作记录加载失败,请稍后重试。'
|
||||
if (showToast) {
|
||||
toast(errorMessage.value)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatJson(value) {
|
||||
try {
|
||||
return JSON.stringify(value || {}, null, 2)
|
||||
} catch {
|
||||
return String(value || '')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkRecordDetail(runId) {
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
try {
|
||||
selectedRunDetail.value = await fetchAgentRunDetail(runId)
|
||||
} catch (error) {
|
||||
detailError.value = error?.message || '工作记录详情加载失败,请稍后重试。'
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openWorkRecordDetail(run) {
|
||||
const runId = String(run?.run_id || '').trim()
|
||||
if (!runId) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedRunId.value = runId
|
||||
selectedRunDetail.value = run
|
||||
detailOpen.value = true
|
||||
void loadWorkRecordDetail(runId)
|
||||
}
|
||||
|
||||
function reloadSelectedDetail() {
|
||||
if (!selectedRunId.value) {
|
||||
return
|
||||
}
|
||||
void loadWorkRecordDetail(selectedRunId.value)
|
||||
}
|
||||
|
||||
function closeWorkRecordDetail() {
|
||||
detailOpen.value = false
|
||||
detailError.value = ''
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling()
|
||||
pollTimer = window.setInterval(() => {
|
||||
loadWorkRecords(false)
|
||||
}, AGENT_RUN_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) {
|
||||
window.clearInterval(pollTimer)
|
||||
pollTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadWorkRecords(false).catch(() => {})
|
||||
startPolling()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/digital-employee-work-records.css"></style>
|
||||
Reference in New Issue
Block a user