历史任务页面开发

This commit is contained in:
aylvn 2025-03-16 14:58:07 +08:00
parent ae053d6e27
commit 00bf070dbd
8 changed files with 407 additions and 24 deletions

View File

@ -6,22 +6,16 @@
<Refresh /> <Refresh />
</el-icon> </el-icon>
</el-button> </el-button>
<el-button type="primary" @click="toAddPage" style="vertical-align: middle;">
<el-icon :size="20" class="pr-4">
<Plus />
</el-icon>
新增
</el-button>
<el-button type="danger" class="ml-10" @click="delSelected" :disabled="selectedRows.length == 0"> <el-button type="danger" class="ml-10" @click="delSelected" :disabled="selectedRows.length == 0">
<el-icon :size="20" class="pr-4"> <el-icon :size="20" class="pr-4">
<Delete /> <Delete />
</el-icon> </el-icon>
删除 {{ t('delete') }}
</el-button> </el-button>
</div> </div>
<div v-show="advSearch"> <div v-show="advSearch">
<el-button @click="resetSearch">重置</el-button> <el-button @click="resetSearch"> {{ t('reset') }}</el-button>
<el-button type="primary" @click="search">查询</el-button> <el-button type="primary" @click="search"> {{ t('search') }}</el-button>
</div> </div>
<div> <div>
<el-input v-model="searchForm.kw" @input="baseSearch" clearable v-show="!advSearch" class="mr-8" /> <el-input v-model="searchForm.kw" @input="baseSearch" clearable v-show="!advSearch" class="mr-8" />
@ -54,7 +48,9 @@
<script setup> <script setup>
import { Refresh, Search, Grid, Plus, Delete } from '@element-plus/icons-vue' import { Refresh, Search, Grid, Plus, Delete } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
advSearch: { advSearch: {
default: false default: false
@ -69,9 +65,6 @@ const props = defineProps({
}, },
selectedRows: { selectedRows: {
default: [] default: []
},
addable: {
default: false
} }
}) })
@ -82,7 +75,6 @@ const emits = defineEmits([
'checkTableColumn', 'checkTableColumn',
'delSelected', 'delSelected',
'resetSearch', 'resetSearch',
'toAddPage',
]) ])
const baseSearch = () => { const baseSearch = () => {
@ -111,9 +103,6 @@ const resetSearch = () => {
emits('resetSearch') emits('resetSearch')
} }
const toAddPage = () => {
emits('toAddPage')
}
</script> </script>
<style scoped> <style scoped>

View File

@ -1,4 +1,21 @@
export default { export default {
add: "Add",
edit: "Edit",
delete: "Delete",
search: "Search",
reset: "Reset",
confirm: "Confirm",
cancel: "Cancel",
save: "Save",
submit: "Submit",
export: "Export",
import: "Import",
copy: "Copy",
paste: "Paste",
cut: "Cut",
createdDt: "Created Date",
updatedDt: "Updated Date",
menu: { menu: {
task: "Task", task: "Task",
history: "History", history: "History",
@ -13,6 +30,8 @@ export default {
switchModel: "Switch Model", switchModel: "Switch Model",
step: "Step", step: "Step",
promptInputPlaceHolder: "Please Input Task Prompt", promptInputPlaceHolder: "Please Input Task Prompt",
promptInput: "Prompt Input",
promptInputKw: "Prompt Input",
clearCacheSuccess: "Clear cache success", clearCacheSuccess: "Clear cache success",
openManusAgiTips: "The above content is generated by OpenManus for reference only", openManusAgiTips: "The above content is generated by OpenManus for reference only",
taskStatus: { taskStatus: {

View File

@ -1,4 +1,21 @@
export default { export default {
add: "新增",
edit: "编辑",
delete: "删除",
search: "搜索",
reset: "重置",
confirm: "确认",
cancel: "取消",
save: "保存",
submit: "提交",
export: "导出",
import: "导入",
copy: "复制",
paste: "粘贴",
cut: "剪切",
createdDt: "创建时间",
updatedDt: "更新时间",
menu: { menu: {
task: "任务", task: "任务",
history: "历史记录", history: "历史记录",
@ -12,6 +29,8 @@ export default {
user: '用户', user: '用户',
step: "步骤", step: "步骤",
promptInputPlaceHolder: "请输入任务提示词", promptInputPlaceHolder: "请输入任务提示词",
promptInput: "提示词输入",
promptInputKw: "提示词关键字",
clearCacheSuccess: "清理缓存成功", clearCacheSuccess: "清理缓存成功",
openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴", openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴",
taskStatus: { taskStatus: {

View File

@ -14,16 +14,25 @@ const router = createRouter({
children: [ children: [
{ {
path: 'task', path: 'task',
component: () => import('@/views/main/Task.vue'), component: () => import('@/views/task/TaskIndex.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
title: "任务", title: "任务列表",
index: 0
}
},
{
path: 'task/:id',
component: () => import('@/views/task/TaskInfo.vue'),
meta: {
keepAlive: false,
title: "任务信息",
index: 0 index: 0
} }
}, },
{ {
path: 'history', path: 'history',
component: () => import('@/views/main/Home.vue'), component: () => import('@/views/task/History.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
title: "历史记录", title: "历史记录",

View File

@ -0,0 +1,348 @@
<template :lang="i18n.locale">
<div class="main-content">
<el-card>
<template #header>
<div class="adv-search" :class="advSearch ? 'expand' : ''">
<div class="card-row-wrap">
<div class="card-row-item">
<el-text tag="label">taskId:</el-text>
<el-input v-model="searchForm.taskId" placeholder="taskId" maxlength="50" show-word-limit />
</div>
<div class="card-row-item">
<el-text>{{ t('promptInputKw') }}:</el-text>
<el-input v-model="searchForm.promptInput" :placeholder="t('promptInputKw')" />
</div>
<div class="card-row-item">
<el-text>{{ t('taskStatus.name') }}:</el-text>
<el-select clearable v-model="searchForm.taskStatus">
<el-option v-for="opt in taskStatusOpts" :key="opt.key" :value="opt.value" :label="t(opt.label)" />
</el-select>
</div>
</div>
</div>
<TableTools :advSearch="advSearch" :searchForm="searchForm" :tableColumns="tableColumns"
:selectedRows="selectedRows" @baseSearch="baseSearch" @search="search" @advSearchSwitch="advSearchSwitch"
@delSelected="delSelected" @resetSearch="resetSearch" @checkTableColumn="checkTableColumn" />
</template>
<el-table ref="tableRef" @selection-change="handleSelectionChange" :data="pageInfo.list" stripe border
style="width: 100%" highlight-current-row max-height="760" :cell-style="{ textAlign: 'center' }"
:header-cell-style="{ textAlign: 'center' }">
<el-table-column type="selection" width="55" />
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="taskId" label="TaskId" width="300">
<template #default="scope">
<el-link @click="toTaskInfo(scope.row.taskId)" type="primary" class="h-20">
{{ scope.row.taskId }}
</el-link>
</template>
</el-table-column>
<el-table-column v-for="col in showTableColumns" :prop=col.prop :label="col.label" :width="col.width"
:minWidth="col.minWidth" :showOverflowTooltip="col.showOverflowTooltip" />
</el-table>
<el-pagination v-model:current-page="pageInfo.pageNum" v-model:page-size="pageInfo.pageSize"
:total="pageInfo.total" layout="total, prev, pager, next" />
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const utils = inject('utils')
const config = useConfig()
const { t } = useI18n()
const tableRef = ref()
const selectedRows = ref([])
//
const advSearch = ref(false)
function advSearchSwitch() {
advSearch.value = !advSearch.value
}
const pageInfo = reactive({
pageNum: 1,
pageSize: 10,
total: 0,
pages: 1,
list: []
})
watch(() => pageInfo.pageNum, () => {
//
search()
})
watch(() => pageInfo.pageSize, () => {
//
search()
})
const searchForm = reactive({
kw: null,
taskId: null,
promptInput: null,
taskStatus: null
})
const taskStatusOpts = reactive([
{ key: "success", value: "success", label: "taskStatus.success" },
{ key: "failed", value: "failed", label: "taskStatus.failed" },
{ key: "running", value: "running", label: "taskStatus.running" },
{ key: "terminated", value: "terminated", label: "taskStatus.terminated" }
])
const tableColumns = ref([
{ prop: "prompt", label: t('promptInput'), isShow: true, showOverflowTooltip: true, minWidth: 300, sortable: true },
{ prop: "statusDesc", label: t('taskStatus.name'), isShow: true, width: 160 },
{ prop: "createdDt", label: t('createdDt'), isShow: true, width: 160 }
])
const showTableColumns = computed(() => {
return tableColumns.value.filter(item => item.isShow)
})
//
const taskHistory = computed(() => {
return config.taskHistory
})
//
const baseSearch = utils.debounce(() => {
const kw = searchForm.kw
utils.clearProps(searchForm)
searchForm.kw = kw
search()
}, 500)
//
// search
function search() {
searchForm.pageNum = pageInfo.pageNum
searchForm.pageSize = pageInfo.pageSize
console.log("search searchForm:", searchForm, pageInfo)
const filteredTaskList = taskHistory.value.filter(taskInfo => {
if (utils.notBlank(searchForm.kw)) {
if (!taskInfo.prompt.includes(searchForm.kw) && !taskInfo.taskId.includes(searchForm.kw)) {
return false
}
return true
}
if (utils.notBlank(searchForm.taskId) && taskInfo.taskId != searchForm.taskId) {
return false
}
if (utils.notBlank(searchForm.promptInput) && !taskInfo.prompt.includes(searchForm.promptInput)) {
return false
}
if (utils.notBlank(searchForm.taskStatus) && taskInfo.status != searchForm.taskStatus) {
return false
}
return true
})
//
pageInfo.total = filteredTaskList.length
//
const startIndex = (pageInfo.pageNum - 1) * pageInfo.pageSize
const endIndex = startIndex + pageInfo.pageSize
pageInfo.list = filteredTaskList.slice(startIndex, endIndex)
//
pageInfo.list.forEach(item => {
item.statusDesc = t('taskStatus.' + item.status)
})
}
const handleSelectionChange = (val) => {
selectedRows.value = val
}
//
function delSelected() {
if (selectedRows.value.length == 0) {
utils.pop("请选择要删除的数据!")
return
}
selectedRows.value.forEach(item => {
for (let i = 0; i < taskHistory.value.length; i++) {
if (taskHistory.value[i].taskId == item.taskId) {
taskHistory.value.splice(i, 1)
i--
}
}
})
baseSearch()
}
// ()
let listener = null
//
onMounted(() => {
listener = (event) => {
if (event.key === 'Enter') {
search()
}
}
window.addEventListener('keyup', listener)
console.log("onMounted pageInfo:", pageInfo)
search()
})
//
onBeforeUnmount(() => {
window.removeEventListener('keyup', listener)
})
function checkTableColumn(isCheck, prop) {
console.log("checkTableColumn:", isCheck, prop)
tableColumns.value.forEach(item => {
if (item.prop == prop) {
item.isShow = isCheck
}
})
}
function resetSearch() {
utils.clearProps(searchForm)
searchForm.openStatus = "OPEN"
}
function toTaskInfo(taskId) {
console.log("toTaskInfo:", taskId)
}
</script>
<style scoped>
.output-area {
flex-grow: 1;
}
.dialog-user {
display: flex;
justify-content: center;
align-items: space-between;
margin-bottom: 16px;
}
.dialog-user .blank {
flex-grow: 1;
}
.dialog-user .content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: end;
border-radius: 12px;
background-color: var(--el-fg-color);
}
.dialog-user .title {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 6px 16px;
font-size: 15px;
}
.dialog-user .prompt {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 0px 16px 6px 16px;
}
.dialog {
width: 100%;
}
.dialog-ai {
margin-bottom: 16px;
background-color: var(--el-fg-color);
border-radius: 12px;
}
.dialog-ai .title {
margin: 6px 12px;
font-size: 15px;
}
.input-area {
flex-grow: 0;
width: 100%;
max-height: 180px;
padding-left: 80px;
padding-right: 80px;
padding-top: 12px;
padding-bottom: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input-box {
width: 100%;
border-radius: 16px;
background-color: var(--el-fg-color);
display: flex;
justify-content: center;
align-items: center;
}
.input-style {
width: 100%;
padding-top: 12px;
padding-bottom: 12px;
}
.input-style :deep(.el-textarea__inner) {
outline: none;
border: none;
resize: none;
box-shadow: none;
}
.add-file-area {
margin-left: 16px;
margin-right: 8px;
}
.send-area {
margin-left: 8px;
margin-right: 16px;
}
.tips {
color: var(--el-text-color-secondary);
font-size: 12px;
padding-top: 10px;
}
.sub-step-time {
color: var(--el-text-color-secondary);
font-size: 12px;
}
</style>

View File

@ -49,7 +49,7 @@
</div> </div>
<div> <div>
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text> <el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
<el-text>{{ t(taskInfo.status) }}</el-text> <el-text>{{ taskInfo.status }}</el-text>
</div> </div>
</div> </div>
@ -87,7 +87,6 @@ import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue' import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config' import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const utils = inject('utils') const utils = inject('utils')
const config = useConfig() const config = useConfig()
@ -124,7 +123,7 @@ const buildEventSource = (taskId) => {
// //
loading.value = false loading.value = false
eventSource.value.close() eventSource.value.close()
taskInfo.value.status = "failed" taskInfo.value.status = "taskStatus.failed"
utils.pop("任务执行失败", "error") utils.pop("任务执行失败", "error")
} }
@ -143,7 +142,7 @@ const handleEvent = (event, type) => {
console.log('task completed'); console.log('task completed');
loading.value = false loading.value = false
eventSource.value.close() eventSource.value.close()
taskInfo.value.status = "success" taskInfo.value.status = "taskStatus.success"
utils.pop("任务已完成", "success") utils.pop("任务已完成", "success")
return return
} }
@ -288,7 +287,7 @@ function stop() {
console.log("stop") console.log("stop")
loading.value = false loading.value = false
eventSource.value.close() eventSource.value.close()
taskInfo.value.status = "terminated" taskInfo.value.status = "taskStatus.terminated"
utils.pop("用户终止任务", "error") utils.pop("用户终止任务", "error")
} }