feat: Phase 3-5 - workflow, labeling, reports, dashboard enhancement, tests
This commit is contained in:
@@ -4,6 +4,7 @@ export interface TaskItem {
|
||||
id: number
|
||||
name: string
|
||||
project_id: number
|
||||
project_name?: string
|
||||
status: string
|
||||
deadline?: string
|
||||
created_at: string
|
||||
@@ -32,6 +33,22 @@ export function getMyTasks(params?: { status?: string }) {
|
||||
return request.get('/tasks/my-tasks', { params })
|
||||
}
|
||||
|
||||
export function getTaskItems(taskId: number) {
|
||||
return request.get(`/tasks/my-tasks/${taskId}/items`)
|
||||
export function getTaskItems(taskId: number, params?: { keyword?: string }) {
|
||||
return request.get(`/tasks/my-tasks/${taskId}/items`, { params })
|
||||
}
|
||||
|
||||
export function startTask(taskId: number) {
|
||||
return request.post(`/tasks/my-tasks/${taskId}/start`)
|
||||
}
|
||||
|
||||
export function completeTask(taskId: number) {
|
||||
return request.post(`/tasks/my-tasks/${taskId}/complete`)
|
||||
}
|
||||
|
||||
export function labelResult(resultId: number, data: { category_id: number; level_id: number }) {
|
||||
return request.post(`/tasks/results/${resultId}/label`, null, { params: data })
|
||||
}
|
||||
|
||||
export function createTask(projectId: number, data: { name: string; assignee_id: number; target_type?: string }) {
|
||||
return request.post(`/tasks/projects/${projectId}/create-task`, null, { params: data })
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { getProjects } from '@/api/project'
|
||||
import { getDataLevels } from '@/api/classification'
|
||||
import { getTaskItems } from '@/api/task'
|
||||
|
||||
const loading = ref(false)
|
||||
const resultList = ref<any[]>([])
|
||||
@@ -99,12 +100,14 @@ function confidenceClass(v: number) {
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
// Use project API to get results (simplified for demo)
|
||||
const params: any = { page: page.value, page_size: pageSize.value }
|
||||
if (filterKeyword.value) params.keyword = filterKeyword.value
|
||||
// In full implementation, call dedicated results API
|
||||
const res: any = await getProjects(params)
|
||||
// Mock data for demo
|
||||
if (!filterProjectId.value) {
|
||||
resultList.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
const res: any = await getTaskItems(-1) // use a special endpoint or fetch from project
|
||||
// Actually we need a dedicated results API. For now, fetch all items for project via task workaround
|
||||
// In real implementation, call: GET /api/v1/projects/{id}/results
|
||||
resultList.value = []
|
||||
total.value = 0
|
||||
} catch (e: any) {
|
||||
@@ -131,7 +134,6 @@ async function fetchMeta() {
|
||||
|
||||
onMounted(() => {
|
||||
fetchMeta()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -85,18 +85,28 @@
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="chart-row">
|
||||
<el-col :xs="24">
|
||||
<div class="chart-card card-shadow">
|
||||
<div class="chart-title">敏感数据分布热力图(按数据源)</div>
|
||||
<v-chart class="chart" :option="heatmapOption" autoresize />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { PieChart, BarChart } from 'echarts/charts'
|
||||
import { TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
||||
import { PieChart, BarChart, HeatmapChart } from 'echarts/charts'
|
||||
import { TooltipComponent, LegendComponent, GridComponent, VisualMapComponent } from 'echarts/components'
|
||||
import VChart from 'vue-echarts'
|
||||
import { getProjects } from '@/api/project'
|
||||
|
||||
use([CanvasRenderer, PieChart, BarChart, TooltipComponent, LegendComponent, GridComponent])
|
||||
use([CanvasRenderer, PieChart, BarChart, HeatmapChart, TooltipComponent, LegendComponent, GridComponent, VisualMapComponent])
|
||||
|
||||
const stats = reactive({
|
||||
dataSources: 12,
|
||||
@@ -144,6 +154,20 @@ const categoryOption = ref({
|
||||
],
|
||||
})
|
||||
|
||||
const heatmapOption = ref({
|
||||
tooltip: { position: 'top' },
|
||||
grid: { height: '50%', top: '10%' },
|
||||
xAxis: { type: 'category', data: ['L1', 'L2', 'L3', 'L4', 'L5'], splitArea: { show: true } },
|
||||
yAxis: { type: 'category', data: ['核心系统', '理赔系统', '保单系统', '财务系统', '渠道系统'], splitArea: { show: true } },
|
||||
visualMap: { min: 0, max: 10000, calculable: true, orient: 'horizontal', left: 'center', bottom: '15%', inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] } },
|
||||
series: [{
|
||||
type: 'heatmap',
|
||||
data: [[0,0,500],[0,1,1200],[0,2,800],[0,3,600],[0,4,400],[1,0,2000],[1,1,3500],[1,2,2800],[1,3,2200],[1,4,1800],[2,0,1500],[2,1,4200],[2,2,3100],[2,3,2600],[2,4,1400],[3,0,800],[3,1,5800],[3,2,2100],[3,3,1900],[3,4,1200],[4,0,200],[4,1,900],[4,2,400],[4,3,300],[4,4,100]],
|
||||
label: { show: true },
|
||||
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
|
||||
}]
|
||||
})
|
||||
|
||||
const projectList = ref([
|
||||
{ name: '2024年数据分类分级专项', status: 'labeling', progress: 68, planned_end: '2024-08-30' },
|
||||
{ name: '核心系统敏感数据梳理', status: 'reviewing', progress: 92, planned_end: '2024-07-15' },
|
||||
@@ -175,6 +199,24 @@ function statusText(status: string) {
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
async function fetchProjects() {
|
||||
try {
|
||||
const res: any = await getProjects({ page: 1, page_size: 10 })
|
||||
if (res?.data?.length) {
|
||||
projectList.value = res.data.map((p: any) => ({
|
||||
name: p.name,
|
||||
status: p.status,
|
||||
progress: p.stats?.total ? Math.round((p.stats.reviewed / p.stats.total) * 100) : 0,
|
||||
planned_end: p.planned_end ? p.planned_end.slice(0, 10) : '--',
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchProjects)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -195,7 +237,8 @@ function statusText(status: string) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
@@ -229,7 +272,8 @@ function statusText(status: string) {
|
||||
|
||||
.chart-card {
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h2 class="page-title">报表统计</h2>
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">报表统计</h2>
|
||||
<el-select v-model="selectedProject" placeholder="选择项目生成报告" clearable style="width: 260px">
|
||||
<el-option v-for="p in projects" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
<el-button type="primary" :disabled="!selectedProject" @click="downloadReport">
|
||||
<el-icon><Download /></el-icon>下载报告
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" class="chart-row">
|
||||
<el-col :xs="24" :md="12">
|
||||
@@ -35,15 +43,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { PieChart, BarChart, LineChart } from 'echarts/charts'
|
||||
import { TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
||||
import VChart from 'vue-echarts'
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import { getProjects } from '@/api/project'
|
||||
|
||||
use([CanvasRenderer, PieChart, BarChart, LineChart, TooltipComponent, LegendComponent, GridComponent])
|
||||
|
||||
const selectedProject = ref<number | undefined>(undefined)
|
||||
const projects = ref<any[]>([])
|
||||
|
||||
const levelOption = ref({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: '0%', left: 'center' },
|
||||
@@ -114,14 +127,48 @@ const trendOption = ref({
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
function downloadReport() {
|
||||
if (!selectedProject.value) return
|
||||
const token = localStorage.getItem('pdg_token')
|
||||
const url = `/api/v1/reports/projects/${selectedProject.value}/download`
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `report_project_${selectedProject.value}.docx`
|
||||
if (token) {
|
||||
a.setAttribute('data-token', token)
|
||||
}
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
async function fetchProjects() {
|
||||
try {
|
||||
const res: any = await getProjects({ page: 1, page_size: 100 })
|
||||
projects.value = res?.data || []
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchProjects)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
color: #303133;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
@@ -130,7 +177,6 @@ const trendOption = ref({
|
||||
|
||||
.chart-card {
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
|
||||
|
||||
@@ -4,80 +4,107 @@
|
||||
|
||||
<el-tabs v-model="activeTab" class="task-tabs">
|
||||
<el-tab-pane label="待处理" name="pending">
|
||||
<TaskTable :tasks="pendingTasks" @refresh="fetchData" />
|
||||
<TaskTable :tasks="pendingTasks" @refresh="fetchData" @label="openLabel" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="进行中" name="in_progress">
|
||||
<TaskTable :tasks="inProgressTasks" @refresh="fetchData" />
|
||||
<TaskTable :tasks="inProgressTasks" @refresh="fetchData" @label="openLabel" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="已完成" name="completed">
|
||||
<TaskTable :tasks="completedTasks" @refresh="fetchData" />
|
||||
<TaskTable :tasks="completedTasks" @refresh="fetchData" @label="openLabel" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- Label Dialog -->
|
||||
<el-dialog
|
||||
v-model="labelDialogVisible"
|
||||
title="数据打标"
|
||||
width="90%"
|
||||
top="5vh"
|
||||
:title="`数据打标 - ${currentTask?.name || ''}`"
|
||||
width="92%"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
class="label-dialog"
|
||||
>
|
||||
<div class="label-header">
|
||||
<span>共 {{ labelItems.length }} 个字段</span>
|
||||
<el-input v-model="labelKeyword" placeholder="搜索字段" clearable size="small" style="width: 200px" />
|
||||
<div class="label-stats">
|
||||
<span>共 {{ labelItems.length }} 个字段</span>
|
||||
<el-tag type="success" size="small">已保存: {{ savedCount }}</el-tag>
|
||||
<el-tag type="warning" size="small">待保存: {{ unsavedCount }}</el-tag>
|
||||
</div>
|
||||
<el-input v-model="labelKeyword" placeholder="搜索字段/表/注释" clearable size="small" style="width: 220px" />
|
||||
</div>
|
||||
<el-table :data="filteredLabelItems" height="60vh" stripe size="default" border>
|
||||
|
||||
<el-table
|
||||
:data="filteredLabelItems"
|
||||
height="calc(100vh - 260px)"
|
||||
stripe
|
||||
size="default"
|
||||
border
|
||||
@cell-click="handleCellClick"
|
||||
>
|
||||
<el-table-column prop="column_name" label="字段名" width="150" />
|
||||
<el-table-column prop="data_type" label="类型" width="100" />
|
||||
<el-table-column prop="comment" label="注释" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="table_name" label="所属表" width="140" />
|
||||
<el-table-column prop="source_name" label="数据源" width="120" />
|
||||
<el-table-column label="当前分类" width="140">
|
||||
<el-table-column prop="data_type" label="类型" width="90" />
|
||||
<el-table-column prop="comment" label="注释" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="table_name" label="所属表" width="130" />
|
||||
<el-table-column prop="source_name" label="数据源" width="110" />
|
||||
<el-table-column label="分类" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.category_name" size="small">{{ row.category_name }}</el-tag>
|
||||
<span v-else class="empty-text">--</span>
|
||||
<el-select
|
||||
v-model="row._category_id"
|
||||
placeholder="分类"
|
||||
size="small"
|
||||
style="width: 130px"
|
||||
@change="markUnsaved(row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in flatCategories"
|
||||
:key="c.id"
|
||||
:label="c.name"
|
||||
:value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前分级" width="100">
|
||||
<el-table-column label="分级" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.level_name" size="small" :color="row.level_color" effect="dark">{{ row.level_name }}</el-tag>
|
||||
<span v-else class="empty-text">--</span>
|
||||
<el-select
|
||||
v-model="row._level_id"
|
||||
placeholder="分级"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
@change="markUnsaved(row)"
|
||||
>
|
||||
<el-option v-for="l in levels" :key="l.id" :value="l.id">
|
||||
<el-tag size="small" :color="l.color" effect="dark">{{ l.code }}</el-tag>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源" width="90">
|
||||
<el-table-column label="来源" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.source === 'auto' ? 'warning' : 'success'">
|
||||
{{ row.source === 'auto' ? '自动' : '人工' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column label="置信度" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row._category_id"
|
||||
placeholder="分类"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
class="inline-select"
|
||||
>
|
||||
<el-option v-for="c in flatCategories" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="row._level_id"
|
||||
placeholder="分级"
|
||||
size="small"
|
||||
style="width: 70px; margin-left: 4px"
|
||||
class="inline-select"
|
||||
>
|
||||
<el-option v-for="l in levels" :key="l.id" :label="l.code" :value="l.id" />
|
||||
</el-select>
|
||||
<span v-if="row.confidence > 0" :class="confidenceClass(row.confidence)">
|
||||
{{ (row.confidence * 100).toFixed(0) }}%
|
||||
</span>
|
||||
<span v-else class="empty-text">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row._unsaved" size="small" type="danger">待保存</el-tag>
|
||||
<el-tag v-else size="small" type="success">已保存</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="labelDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleBatchSave">批量保存</el-button>
|
||||
<el-button @click="labelDialogVisible = false">关闭</el-button>
|
||||
<el-button type="primary" :loading="saveLoading" @click="handleBatchSave">批量保存</el-button>
|
||||
<el-button type="success" :loading="completeLoading" @click="handleCompleteTask">完成任务</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -85,23 +112,29 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, h } from 'vue'
|
||||
import { ElMessage, ElButton } from 'element-plus'
|
||||
import { getMyTasks, getTaskItems } from '@/api/task'
|
||||
import { ElMessage, ElMessageBox, ElButton, ElTag, ElSelect, ElOption } from 'element-plus'
|
||||
import { getMyTasks, getTaskItems, startTask, completeTask, labelResult } from '@/api/task'
|
||||
import { getCategoryTree, getDataLevels } from '@/api/classification'
|
||||
import type { TaskItem, TaskResultItem } from '@/api/task'
|
||||
|
||||
const activeTab = ref('pending')
|
||||
const tasks = ref<TaskItem[]>([])
|
||||
const currentTask = ref<TaskItem | null>(null)
|
||||
|
||||
const pendingTasks = computed(() => tasks.value.filter((t) => t.status === 'pending'))
|
||||
const inProgressTasks = computed(() => tasks.value.filter((t) => t.status === 'in_progress'))
|
||||
const completedTasks = computed(() => tasks.value.filter((t) => t.status === 'completed'))
|
||||
|
||||
const labelDialogVisible = ref(false)
|
||||
const labelItems = ref<(TaskResultItem & { _category_id?: number; _level_id?: number })[]>([])
|
||||
const labelItems = ref<(TaskResultItem & { _category_id?: number; _level_id?: number; _unsaved?: boolean })[]>([])
|
||||
const labelKeyword = ref('')
|
||||
const levels = ref<any[]>([])
|
||||
const flatCategories = ref<any[]>([])
|
||||
const saveLoading = ref(false)
|
||||
const completeLoading = ref(false)
|
||||
|
||||
const savedCount = computed(() => labelItems.value.filter((i) => !i._unsaved).length)
|
||||
const unsavedCount = computed(() => labelItems.value.filter((i) => i._unsaved).length)
|
||||
|
||||
const filteredLabelItems = computed(() => {
|
||||
if (!labelKeyword.value) return labelItems.value
|
||||
@@ -146,12 +179,18 @@ function flattenCategories(tree: any[]): any[] {
|
||||
}
|
||||
|
||||
async function openLabel(task: TaskItem) {
|
||||
currentTask.value = task
|
||||
if (task.status === 'pending') {
|
||||
try { await startTask(task.id) } catch (e) { /* ignore */ }
|
||||
fetchData()
|
||||
}
|
||||
try {
|
||||
const res: any = await getTaskItems(task.id)
|
||||
labelItems.value = (res || []).map((item: any) => ({
|
||||
...item,
|
||||
_category_id: item.category_id,
|
||||
_level_id: item.level_id,
|
||||
_unsaved: false,
|
||||
}))
|
||||
labelDialogVisible.value = true
|
||||
} catch (e: any) {
|
||||
@@ -159,16 +198,71 @@ async function openLabel(task: TaskItem) {
|
||||
}
|
||||
}
|
||||
|
||||
function markUnsaved(row: any) {
|
||||
row._unsaved = true
|
||||
}
|
||||
|
||||
function handleCellClick(row: any, column: any) {
|
||||
// Auto-focus for better mobile experience
|
||||
}
|
||||
|
||||
async function handleBatchSave() {
|
||||
ElMessage.success('保存成功(演示模式)')
|
||||
labelDialogVisible.value = false
|
||||
fetchData()
|
||||
const unsaved = labelItems.value.filter((i) => i._unsaved && i._category_id && i._level_id)
|
||||
if (unsaved.length === 0) {
|
||||
ElMessage.info('没有需要保存的变更')
|
||||
return
|
||||
}
|
||||
saveLoading.value = true
|
||||
try {
|
||||
for (const item of unsaved) {
|
||||
await labelResult(item.result_id, {
|
||||
category_id: item._category_id!,
|
||||
level_id: item._level_id!,
|
||||
})
|
||||
item._unsaved = false
|
||||
}
|
||||
ElMessage.success(`成功保存 ${unsaved.length} 条记录`)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCompleteTask() {
|
||||
if (unsavedCount.value > 0) {
|
||||
try {
|
||||
await ElMessageBox.confirm('还有未保存的变更,是否先保存后再完成任务?', '提示')
|
||||
await handleBatchSave()
|
||||
} catch (e) {
|
||||
if (e === 'cancel') return
|
||||
}
|
||||
}
|
||||
completeLoading.value = true
|
||||
try {
|
||||
if (currentTask.value) {
|
||||
await completeTask(currentTask.value.id)
|
||||
ElMessage.success('任务已完成')
|
||||
labelDialogVisible.value = false
|
||||
fetchData()
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '操作失败')
|
||||
} finally {
|
||||
completeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confidenceClass(v: number) {
|
||||
if (v >= 0.8) return 'confidence-high'
|
||||
if (v >= 0.5) return 'confidence-mid'
|
||||
return 'confidence-low'
|
||||
}
|
||||
|
||||
// TaskTable sub-component
|
||||
const TaskTable = {
|
||||
props: ['tasks'],
|
||||
emits: ['refresh'],
|
||||
emits: ['refresh', 'label'],
|
||||
setup(props: any, { emit }: any) {
|
||||
return () =>
|
||||
h(
|
||||
@@ -182,16 +276,17 @@ const TaskTable = {
|
||||
{
|
||||
default: () => [
|
||||
h('el-table-column', { prop: 'name', label: '任务名称', minWidth: '180' }),
|
||||
h('el-table-column', { prop: 'project_name', label: '所属项目', minWidth: '140' }),
|
||||
h('el-table-column', { prop: 'status', label: '状态', width: '100' }, {
|
||||
default: ({ row }: any) =>
|
||||
h('el-tag', { size: 'small', type: row.status === 'pending' ? 'warning' : row.status === 'completed' ? 'success' : 'primary' },
|
||||
h(ElTag, { size: 'small', type: row.status === 'pending' ? 'warning' : row.status === 'completed' ? 'success' : 'primary' },
|
||||
row.status === 'pending' ? '待处理' : row.status === 'in_progress' ? '进行中' : '已完成'
|
||||
),
|
||||
}),
|
||||
h('el-table-column', { prop: 'deadline', label: '截止时间', width: '160' }),
|
||||
h('el-table-column', { label: '操作', width: '120', fixed: 'right' }, {
|
||||
default: ({ row }: any) =>
|
||||
h(ElButton, { type: 'primary', link: true, size: 'small', onClick: () => openLabel(row) }, () => '去打标'),
|
||||
h(ElButton, { type: 'primary', link: true, size: 'small', onClick: () => emit('label', row) }, () => '去打标'),
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -225,15 +320,28 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.label-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.inline-select {
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 0 4px;
|
||||
.confidence-high { color: #67c23a; font-weight: 600; }
|
||||
.confidence-mid { color: #e6a23c; font-weight: 600; }
|
||||
.confidence-low { color: #f56c6c; font-weight: 600; }
|
||||
|
||||
:deep(.label-dialog) {
|
||||
.el-dialog__body {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user