This commit is contained in:
张成
2025-11-24 13:23:42 +08:00
commit 5d7444cd65
156 changed files with 50653 additions and 0 deletions

View File

View File

@@ -0,0 +1,415 @@
<template>
<div class="commands-list-container commands-body">
<!-- 头部导航 -->
<div class="commands-header">
<Button type="default" icon="ios-arrow-back" class="back-btn" @click="handleBack">
返回任务列表
</Button>
<div class="commands-title">
<Icon type="ios-list-box" size="20" />
<span class="title-text">{{ title }}</span>
</div>
</div>
<!-- 内容区域 -->
<tables :columns="columns" :value="data" :loading="loading"></tables>
<!-- 查看完整内容弹窗 -->
<Modal
v-model="showDetailModal"
title="查看完整内容"
width="800"
:mask-closable="true"
>
<div class="detail-content">
<pre class="detail-pre">{{ detailContent }}</pre>
</div>
<div slot="footer">
<Button @click="showDetailModal = false">关闭</Button>
</div>
</Modal>
</div>
</template>
<script>
export default {
name: 'CommandsList',
props: {
// 是否显示
visible: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: '指令列表'
},
// 加载状态
loading: {
type: Boolean,
default: false
},
// 数据
data: {
type: Array,
default: () => []
}
},
data() {
return {
showDetailModal: false,
detailContent: '',
columns: [
{ title: 'ID', key: 'id', align: 'center' },
{ title: '序号', key: 'sequence', align: 'center' },
{
title: '指令类型',
key: 'command_type',
align: 'center',
type: 'template',
render: (h, params) => {
return h('Tag', {
props: {
color: this.getCommandTypeColor(params.row.command_type)
}
}, params.row.command_type)
}
},
{ title: '指令名称', key: 'command_name' },
{
title: '指令参数',
key: 'command_params',
type: 'template',
width: 250,
render: (h, params) => {
if (!params.row.command_params) {
return h('span', '-')
}
return h('div', {
class: 'json-content',
style: {
cursor: 'pointer'
},
on: {
dblclick: () => {
this.showDetail('指令参数', params.row.command_params)
}
}
}, [
h('pre', this.formatJson(params.row.command_params))
])
}
},
{
title: '状态',
key: 'status',
align: 'center',
type: 'template',
render: (h, params) => {
return h('Tag', {
props: {
color: this.getStatusColor(params.row.status)
}
}, this.getStatusText(params.row.status))
}
},
{
title: '执行结果',
key: 'result',
type: 'template',
width: 300,
render: (h, params) => {
if (!params.row.result) {
return h('span', '-')
}
return h('div', {
class: 'json-content',
style: {
cursor: 'pointer'
},
on: {
dblclick: () => {
this.showDetail('执行结果', params.row.result)
}
}
}, [
h('pre', this.formatJson(params.row.result))
])
}
},
{
title: '错误信息',
key: 'error_message',
type: 'template',
render: (h, params) => {
if (!params.row.error_message) {
return h('span', '-')
}
return h('div', {
class: 'error-content'
}, params.row.error_message)
}
},
{ title: '重试次数', key: 'retry_count', align: 'center' },
{ title: '执行时长(s)', key: 'execution_time', align: 'center' },
{
title: '创建时间',
key: 'create_time',
type: 'template',
render: (h, params) => {
return h('span', this.formatDateTime(params.row.create_time))
}
},
{
title: '更新时间',
key: 'last_modify_time',
type: 'template',
render: (h, params) => {
return h('span', this.formatDateTime(params.row.last_modify_time))
}
}
]
}
},
methods: {
// 返回按钮点击
handleBack() {
this.$emit('back')
},
// 获取指令类型颜色
getCommandTypeColor(type) {
const colorMap = {
'browser': 'blue',
'search': 'cyan',
'fill': 'geekblue',
'click': 'purple',
'wait': 'orange',
'screenshot': 'magenta',
'extract': 'green'
}
return colorMap[type] || 'default'
},
// 获取状态颜色
getStatusColor(status) {
const colorMap = {
'pending': 'default',
'running': 'blue',
'success': 'success',
'failed': 'error',
'skipped': 'warning'
}
return colorMap[status] || 'default'
},
// 获取状态文本
getStatusText(status) {
const textMap = {
'pending': '待执行',
'running': '执行中',
'success': '成功',
'failed': '失败',
'skipped': '已跳过'
}
return textMap[status] || status
},
// 格式化JSON
formatJson(str) {
if (!str) return ''
try {
const obj = typeof str === 'string' ? JSON.parse(str) : str
return JSON.stringify(obj, null, 2)
} catch {
return str
}
},
// 格式化日期时间
formatDateTime(datetime) {
if (!datetime) return '-'
const date = new Date(datetime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
// 显示完整内容
showDetail(title, content) {
this.detailContent = this.formatJson(content)
this.showDetailModal = true
}
}
}
</script>
<style scoped>
/* 容器 - 撑满父元素 */
.commands-list-container {
width: 100%;
display: flex;
flex-direction: column;
background: #fff;
height: 100%;
}
/* 头部导航 */
.commands-header {
display: flex;
align-items: center;
padding: 16px 20px;
background: #fff;
border-bottom: 1px solid #e8eaec;
flex-shrink: 0;
}
.back-btn {
margin-right: 16px;
}
.commands-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 500;
color: #333;
}
.commands-title>>>.ivu-icon {
margin-right: 8px;
color: #2d8cf0;
}
.title-text {
color: #17233d;
}
/* JSON内容显示 */
.commands-body>>>.json-content {
font-size: 12px;
line-height: 1.5;
height: 120px;
overflow: hidden;
display: block;
position: relative;
transition: background-color 0.2s;
}
.commands-body>>>.json-content:hover {
background: #f0f0f0;
}
.commands-body>>>.json-content pre {
margin: 0;
padding: 8px;
background: #f8f8f9;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
color: #515a6e;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 120px;
overflow: hidden;
display: block;
}
/* 错误信息显示 */
.error-content {
font-size: 12px;
line-height: 1.5;
color: #ed4014;
padding: 8px;
background: #fff1f0;
border-radius: 4px;
border-left: 3px solid #ed4014;
white-space: pre-wrap;
word-wrap: break-word;
}
/* 表格样式优化 */
.commands-body>>>.ivu-table {
font-size: 13px;
}
.commands-body>>>.ivu-table-wrapper {
overflow: visible;
}
.commands-body>>>.ivu-table-body {
overflow: visible;
}
.commands-body>>>.ivu-table th {
background: #f8f8f9;
font-weight: 600;
}
.commands-body>>>.ivu-table-stripe .ivu-table-body tr:nth-child(2n) td {
background-color: #fafafa;
}
.commands-body>>>.ivu-table td {
padding: 12px 8px;
vertical-align: top;
}
/* 确保表格单元格内容不会撑高 */
.commands-body>>>.ivu-table td .json-content {
max-height: 120px;
overflow: hidden;
display: block;
}
.commands-body>>>.ivu-table td .json-content pre {
max-height: 120px;
overflow: hidden;
display: block;
}
/* 滚动条美化 - 仅作用于外层容器 */
.commands-body::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.commands-body::-webkit-scrollbar-thumb {
background: #dcdee2;
border-radius: 4px;
}
.commands-body::-webkit-scrollbar-thumb:hover {
background: #b8bbbf;
}
.commands-body::-webkit-scrollbar-track {
background: #f8f8f9;
border-radius: 4px;
}
/* 弹窗内容样式 */
.detail-content {
max-height: 60vh;
overflow: auto;
}
.detail-pre {
margin: 0;
padding: 16px;
background: #f8f8f9;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
font-size: 13px;
line-height: 1.6;
color: #515a6e;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>

View File

@@ -0,0 +1,279 @@
# CommandsList 组件
## 📦 组件说明
`CommandsList.vue` 是一个通用的指令列表展示组件,专门用于在任务状态页面中展示任务的执行指令详情。
## ✨ 特性
-**撑满父元素** - 组件宽高自动撑满父容器100% width & height
-**自适应表格** - 表格列宽自动分配,无需手动设置每列宽度
-**返回导航** - 内置返回按钮,支持返回上级页面
-**加载状态** - 支持loading状态显示
-**美观样式** - 现代化UI设计带有斑马纹、边框等
-**数据格式化** - 自动格式化JSON、时间等数据
-**状态标签** - 彩色标签区分不同状态和类型
-**独立封装** - 完全独立的组件,可在其他页面复用
## 📋 Props 参数
| 参数名 | 类型 | 默认值 | 必填 | 说明 |
|--------|------|--------|------|------|
| visible | Boolean | false | 否 | 是否显示组件 |
| title | String | '指令列表' | 否 | 页面标题 |
| loading | Boolean | false | 否 | 数据加载状态 |
| data | Array | [] | 否 | 指令列表数据 |
## 🎯 Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| back | 无 | 点击返回按钮时触发 |
## 💻 使用示例
### 基础用法
```vue
<template>
<div class="page-container">
<!-- 列表页面 -->
<div v-show="!showDetail">
<Button @click="handleShowDetail">查看详情</Button>
</div>
<!-- 详情页面 - 使用 CommandsList 组件 -->
<CommandsList
v-show="showDetail"
:visible="showDetail"
:title="detailTitle"
:loading="detailLoading"
:data="detailData"
@back="handleBack" />
</div>
</template>
<script>
import CommandsList from './components/CommandsList.vue'
export default {
components: {
CommandsList
},
data() {
return {
showDetail: false,
detailTitle: '指令列表',
detailLoading: false,
detailData: []
}
},
methods: {
async handleShowDetail() {
this.showDetail = true
this.detailLoading = true
try {
// 获取数据
const res = await this.fetchData()
this.detailData = res.data
} catch (error) {
this.$Message.error('获取数据失败')
} finally {
this.detailLoading = false
}
},
handleBack() {
this.showDetail = false
}
}
}
</script>
<style scoped>
.page-container {
width: 100%;
height: 100%;
}
</style>
```
### 在 task_status.vue 中的实际应用
```vue
<template>
<div class="content-view">
<!-- 任务列表主页面 -->
<div v-show="!showCommandsView" class="task-list-view">
<!-- 任务列表内容 -->
</div>
<!-- 指令列表详情页面 -->
<CommandsList
v-show="showCommandsView"
:visible="showCommandsView"
:title="commandsModal.title"
:loading="commandsModal.loading"
:data="commandsModal.data"
@back="backToTaskList" />
</div>
</template>
<script>
import CommandsList from './components/CommandsList.vue'
export default {
components: {
CommandsList
},
data() {
return {
showCommandsView: false,
commandsModal: {
loading: false,
title: '指令列表',
data: []
}
}
},
methods: {
async showCommands(row) {
this.showCommandsView = true
this.commandsModal.loading = true
this.commandsModal.title = `指令列表 - 任务ID: ${row.id} (${row.taskName})`
try {
const res = await taskStatusServer.getCommands(row.id)
this.commandsModal.data = res.data || []
} catch (error) {
this.$Message.error('获取指令列表失败')
this.commandsModal.data = []
} finally {
this.commandsModal.loading = false
}
},
backToTaskList() {
this.showCommandsView = false
}
}
}
</script>
```
## 📊 数据格式
### 指令数据结构data 数组元素)
```javascript
{
id: 123, // 指令ID
sequence: 1, // 序号
command_type: 'browser', // 指令类型browser/search/fill/click/wait/screenshot/extract
command_name: '打开浏览器', // 指令名称
command_params: '{"url":"..."}', // 指令参数JSON字符串
status: 'success', // 状态pending/running/success/failed/skipped
result: '{"data":"..."}', // 执行结果JSON字符串
error_message: null, // 错误信息
retry_count: 0, // 重试次数
execution_time: 1.23, // 执行时长(秒)
create_time: '2025-10-29 10:00:00', // 创建时间
}
```
## 🎨 样式特性
### 布局特性
- **弹性布局** - 使用 flexbox 实现头部+内容区域布局
- **100%撑满** - 宽高100%,自动撑满父容器
- **自适应列宽** - 表格列宽自动分配,无需手动设置
### 视觉效果
- **现代化头部** - 带返回按钮和标题的导航栏
- **美观表格** - 斑马纹、边框、悬停效果
- **彩色标签** - 不同状态使用不同颜色区分
- **格式化显示** - JSON自动格式化代码高亮
- **错误高亮** - 错误信息红色背景突出显示
- **滚动优化** - 美化的滚动条样式
### 响应式
- 表格自动适应父容器宽度
- 内容区域独立滚动
- 头部固定不滚动
## 🔧 自定义样式
如果需要自定义样式,可以在父组件中使用深度选择器:
```vue
<style scoped>
/* 修改组件内部样式 */
.my-page >>> .commands-list-container {
background: #f5f5f5;
}
/* 修改表格样式 */
.my-page >>> .commands-body .ivu-table {
font-size: 14px;
}
</style>
```
## 📌 注意事项
1. **父容器要求** - 父容器必须有明确的宽高,否则组件无法正确撑满
2. **v-show控制** - 建议使用 `v-show` 而不是 `v-if`,以保持状态
3. **数据格式** - JSON字段支持字符串和对象两种格式组件会自动处理
4. **时间格式** - 时间字段应为标准的日期时间字符串
5. **状态映射** - 组件内部已定义状态和类型的颜色映射,可根据需要修改
## 🚀 特色功能
### 1. 自动JSON格式化
组件会自动格式化 `command_params``result` 字段的JSON数据提供更好的可读性。
### 2. 智能状态标签
根据不同的指令类型和状态,自动显示对应颜色的标签:
**指令类型颜色**
- browser - 蓝色
- search - 青色
- fill - 深蓝色
- click - 紫色
- wait - 橙色
- screenshot - 洋红色
- extract - 绿色
**状态颜色**
- pending待执行 - 灰色
- running执行中 - 蓝色
- success成功 - 绿色
- failed失败 - 红色
- skipped已跳过 - 橙色
### 3. 错误信息高亮
错误信息使用红色背景和左边框高亮显示,便于快速定位问题。
### 4. 时间格式化
自动格式化时间为 `YYYY-MM-DD HH:mm:ss` 格式,统一时间显示样式。
## 📝 更新日志
### v1.0.0 (2025-10-29)
- ✅ 初始版本发布
- ✅ 实现基础功能
- ✅ 移除所有列宽限制,实现表格自动撑满
- ✅ 优化样式和交互
- ✅ 完善组件文档
## 🤝 贡献
如需改进此组件,请遵循以下原则:
1. 保持组件的通用性和独立性
2. 确保样式不依赖外部全局样式
3. 维护良好的代码注释
4. 更新此文档说明
## 📄 许可
此组件为项目内部组件,仅供项目内使用。

View File

@@ -0,0 +1,324 @@
<template>
<div class="content-view">
<!-- 任务列表主页面 -->
<div v-show="!showCommandsView" class="task-list-view">
<div class="table-head-tool">
<Button type="primary" @click="showAddWarp">新增任务</Button>
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
<FormItem :label-width="20" class="flex">
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
:placeholder="seachTypePlaceholder">
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
</Select>
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
placeholder="请输入关键字" @on-search="query(1)" />
</FormItem>
<FormItem label="任务类型">
<Select v-model="gridOption.param.seachOption.taskType" style="width: 120px" clearable @on-change="query(1)">
<Option value="search">搜索岗位</Option>
<Option value="apply">投递简历</Option>
<Option value="chat">聊天回复</Option>
<Option value="follow">跟进</Option>
</Select>
</FormItem>
<FormItem label="任务状态">
<Select v-model="gridOption.param.seachOption.status" style="width: 120px" clearable @on-change="query(1)">
<Option value="pending">待执行</Option>
<Option value="running">执行中</Option>
<Option value="success">成功</Option>
<Option value="failed">失败</Option>
<Option value="cancelled">已取消</Option>
</Select>
</FormItem>
<FormItem>
<Button type="primary" @click="query(1)">查询</Button>
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
<Button type="default" @click="exportCsv">导出</Button>
</FormItem>
</Form>
</div>
<div class="table-body">
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
@changePage="query"></tables>
</div>
</div>
<!-- 指令列表详情页面 -->
<CommandsList
v-show="showCommandsView"
:visible="showCommandsView"
:title="commandsModal.title"
:loading="commandsModal.loading"
:data="commandsModal.data"
@back="backToTaskList" />
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
</div>
</template>
<script>
import taskStatusServer from '@/api/operation/task_status_server.js'
import CommandsList from './components/CommandsList.vue'
export default {
components: {
CommandsList
},
data() {
let rules = {}
rules["taskType"] = [{ required: true, message: '请选择任务类型', trigger: 'change' }]
rules["sn_code"] = [{ required: true, message: '请填写设备SN码', trigger: 'blur' }]
return {
seachTypes: [
{ key: 'taskName', value: '任务名称' },
{ key: 'sn_code', value: '设备SN码' },
{ key: 'id', value: '任务ID' }
],
showCommandsView: false, // 是否显示指令列表视图
gridOption: {
param: {
seachOption: {
key: 'taskName',
value: '',
taskType: null,
status: null
},
pageOption: {
page: 1,
pageSize: 20
}
},
data: [],
rules: rules
},
listColumns: [
{ title: '任务ID', key: 'id', minWidth: 180 },
{ title: '设备SN码', key: 'sn_code', minWidth: 120 },
{
title: '任务类型',
key: 'taskType',
minWidth: 100,
render: (h, params) => {
const typeMap = {
'search': { text: '搜索岗位', color: 'blue' },
'apply': { text: '投递简历', color: 'success' },
'chat': { text: '聊天回复', color: 'purple' },
'follow': { text: '跟进', color: 'orange' }
}
const type = typeMap[params.row.taskType] || { text: params.row.taskType, color: 'default' }
return h('Tag', { props: { color: type.color } }, type.text)
}
},
{ title: '任务名称', key: 'taskName', minWidth: 150 },
{
title: '任务状态',
key: 'status',
minWidth: 100,
render: (h, params) => {
const statusMap = {
'pending': { text: '待执行', color: 'default' },
'running': { text: '执行中', color: 'blue' },
'success': { text: '成功', color: 'success' },
'failed': { text: '失败', color: 'error' },
'cancelled': { text: '已取消', color: 'warning' }
}
const status = statusMap[params.row.status] || { text: params.row.status, color: 'default' }
return h('Tag', { props: { color: status.color } }, status.text)
}
},
{ title: '进度', key: 'progress', minWidth: 100 },
{ title: '开始时间', key: 'startTime', minWidth: 150 },
{ title: '结束时间', key: 'endTime', minWidth: 150 },
{ title: '创建时间', key: 'create_time', minWidth: 150 },
{
title: '操作',
key: 'action',
width: 360,
type: 'template',
render: (h, params) => {
let btns = [
{
title: '指令列表',
type: 'info',
click: () => {
this.showCommands(params.row)
},
},
{
title: '编辑',
type: 'primary',
click: () => {
this.showEditWarp(params.row)
},
}
]
if (params.row.status === 'failed') {
btns.push({
title: '重试',
type: 'success',
click: () => {
this.retryTask(params.row)
},
})
}
if (params.row.status === 'pending' || params.row.status === 'running') {
btns.push({
title: '取消',
type: 'warning',
click: () => {
this.cancelTask(params.row)
},
})
}
btns.push({
title: '删除',
type: 'error',
click: () => {
this.delConfirm(params.row)
},
})
return window.framework.uiTool.getBtn(h, btns)
},
}
],
editColumns: [
{ title: '设备SN码', key: 'sn_code', type: 'text', required: true },
{ title: '任务类型', key: 'taskType', type: 'select', required: true, options: [
{ value: 'search', label: '搜索岗位' },
{ value: 'apply', label: '投递简历' },
{ value: 'chat', label: '聊天回复' },
{ value: 'follow', label: '跟进' }
]},
{ title: '任务名称', key: 'taskName', type: 'text' },
{ title: '任务描述', key: 'taskDescription', type: 'textarea' },
{ title: '任务参数', key: 'taskParams', type: 'textarea' },
{ title: '优先级', key: 'priority', type: 'number' },
{ title: '最大重试次数', key: 'maxRetries', type: 'number' },
{ title: '计划执行时间', key: 'scheduledTime', type: 'datetime' }
],
commandsModal: {
loading: false,
title: '指令列表',
data: []
}
}
},
mounted() {
this.query(1)
},
methods: {
query(page) {
this.gridOption.param.pageOption.page = page
taskStatusServer.page(this.gridOption.param).then(res => {
this.gridOption.data = res.data.rows
this.gridOption.param.pageOption.total = res.data.count
})
},
showAddWarp() {
this.$refs.editModal.showModal()
},
showEditWarp(row) {
this.$refs.editModal.showModal(row)
},
cancelTask(row) {
this.$Modal.confirm({
title: '确认取消任务',
content: `确定要取消任务 "${row.taskName}" 吗?`,
onOk: async () => {
try {
await taskStatusServer.cancel(row)
this.$Message.success('任务取消成功!')
this.query(this.gridOption.param.pageOption.page)
} catch (error) {
this.$Message.error('任务取消失败')
}
}
})
},
retryTask(row) {
this.$Modal.confirm({
title: '确认重试任务',
content: `确定要重试任务 "${row.taskName}" 吗?`,
onOk: async () => {
try {
await taskStatusServer.retry(row)
this.$Message.success('任务重试成功!')
this.query(this.gridOption.param.pageOption.page)
} catch (error) {
this.$Message.error('任务重试失败')
}
}
})
},
delConfirm(row) {
window.framework.uiTool.delConfirm(async () => {
await taskStatusServer.del(row)
this.$Message.success('删除成功!')
this.query(1)
})
},
exportCsv() {
taskStatusServer.exportCsv(this.gridOption.param).then(res => {
window.framework.funTool.downloadFile(res, '任务状态.csv')
})
},
resetQuery() {
this.gridOption.param.seachOption = {
key: 'taskName',
value: '',
taskType: null,
status: null
}
this.query(1)
},
async showCommands(row) {
this.showCommandsView = true
this.commandsModal.loading = true
this.commandsModal.title = `指令列表 - 任务ID: ${row.id} (${row.taskName})`
try {
const res = await taskStatusServer.getCommands(row.id)
this.commandsModal.data = res.data || []
} catch (error) {
this.$Message.error('获取指令列表失败')
this.commandsModal.data = []
} finally {
this.commandsModal.loading = false
}
},
backToTaskList() {
this.showCommandsView = false
}
},
computed: {
seachTypePlaceholder() {
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
return selected ? selected.value : '请选择搜索类型'
}
}
}
</script>
<style scoped>
.ml10 {
margin-left: 10px;
}
.flex {
display: flex;
align-items: center;
}
/* 任务列表视图 */
.task-list-view {
width: 100%;
height: 100%;
}
</style>