86c487ae40
- Fix Classification.vue: levels use res.data, auto-fetch on mount - Fix Task.vue: levels and categories use res.data - Fix Project.vue: templates use res.data
319 lines
8.5 KiB
Vue
319 lines
8.5 KiB
Vue
<template>
|
|
<div class="page-container">
|
|
<div class="page-header">
|
|
<h2 class="page-title">项目管理</h2>
|
|
<el-button type="primary" @click="handleAdd">
|
|
<el-icon><Plus /></el-icon>新建项目
|
|
</el-button>
|
|
</div>
|
|
|
|
<div class="search-bar card-shadow">
|
|
<el-input
|
|
v-model="keyword"
|
|
placeholder="搜索项目名称"
|
|
clearable
|
|
style="width: 260px"
|
|
@keyup.enter="handleSearch"
|
|
>
|
|
<template #append>
|
|
<el-button @click="handleSearch">
|
|
<el-icon><Search /></el-icon>
|
|
</el-button>
|
|
</template>
|
|
</el-input>
|
|
</div>
|
|
|
|
<div class="project-list">
|
|
<el-row :gutter="16">
|
|
<el-col v-for="p in projectList" :key="p.id" :xs="24" :sm="12" :lg="8">
|
|
<div class="project-card card-shadow">
|
|
<div class="project-header">
|
|
<div class="project-name">{{ p.name }}</div>
|
|
<el-tag :type="statusType(p.status)" size="small">{{ statusText(p.status) }}</el-tag>
|
|
</div>
|
|
<div class="project-desc">{{ p.description || '暂无描述' }}</div>
|
|
<div class="project-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-num">{{ p.stats?.total || 0 }}</div>
|
|
<div class="stat-label">总字段</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-num">{{ p.stats?.auto || 0 }}</div>
|
|
<div class="stat-label">自动</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-num">{{ p.stats?.manual || 0 }}</div>
|
|
<div class="stat-label">人工</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-num">{{ p.stats?.reviewed || 0 }}</div>
|
|
<div class="stat-label">已审</div>
|
|
</div>
|
|
</div>
|
|
<div class="project-actions">
|
|
<el-button type="primary" size="small" @click="handleAutoClassify(p)">自动分类</el-button>
|
|
<el-button type="danger" link size="small" @click="handleDelete(p)">删除</el-button>
|
|
</div>
|
|
</div>
|
|
</el-col>
|
|
</el-row>
|
|
</div>
|
|
|
|
<!-- Add Dialog -->
|
|
<el-dialog v-model="dialogVisible" title="新建项目" width="520px" destroy-on-close>
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
<el-form-item label="项目名称" prop="name">
|
|
<el-input v-model="form.name" placeholder="请输入项目名称" />
|
|
</el-form-item>
|
|
<el-form-item label="模板" prop="template_id">
|
|
<el-select v-model="form.template_id" placeholder="选择分类分级模板" style="width: 100%">
|
|
<el-option v-for="t in templates" :key="t.id" :label="t.name" :value="t.id" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="数据源范围">
|
|
<el-select v-model="form.target_source_ids" multiple placeholder="选择数据源(不选则为全部)" style="width: 100%">
|
|
<el-option v-for="s in dataSources" :key="s.id" :label="s.name" :value="String(s.id)" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="描述">
|
|
<el-input v-model="form.description" type="textarea" :rows="2" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { Plus, Search } from '@element-plus/icons-vue'
|
|
import { getProjects, createProject, deleteProject, autoClassifyProject } from '@/api/project'
|
|
import { getTemplates } from '@/api/classification'
|
|
import { getDataSources } from '@/api/datasource'
|
|
import type { ProjectItem } from '@/api/project'
|
|
|
|
const loading = ref(false)
|
|
const projectList = ref<ProjectItem[]>([])
|
|
const page = ref(1)
|
|
const pageSize = ref(20)
|
|
const total = ref(0)
|
|
const keyword = ref('')
|
|
|
|
const dialogVisible = ref(false)
|
|
const submitLoading = ref(false)
|
|
const formRef = ref()
|
|
const form = reactive({
|
|
name: '',
|
|
template_id: undefined as number | undefined,
|
|
target_source_ids: [] as string[],
|
|
description: '',
|
|
})
|
|
const rules = {
|
|
name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
|
|
template_id: [{ required: true, message: '请选择模板', trigger: 'change' }],
|
|
}
|
|
|
|
const templates = ref<any[]>([])
|
|
const dataSources = ref<any[]>([])
|
|
|
|
function statusType(status: string) {
|
|
const map: Record<string, any> = {
|
|
created: 'info',
|
|
scanning: 'warning',
|
|
assigning: '',
|
|
labeling: 'primary',
|
|
reviewing: 'success',
|
|
accepting: 'success',
|
|
published: 'success',
|
|
}
|
|
return map[status] || 'info'
|
|
}
|
|
|
|
function statusText(status: string) {
|
|
const map: Record<string, string> = {
|
|
created: '已创建',
|
|
scanning: '扫描中',
|
|
assigning: '分配中',
|
|
labeling: '打标中',
|
|
reviewing: '审核中',
|
|
accepting: '验收中',
|
|
published: '已发布',
|
|
}
|
|
return map[status] || status
|
|
}
|
|
|
|
async function fetchData() {
|
|
loading.value = true
|
|
try {
|
|
const res: any = await getProjects({ page: page.value, page_size: pageSize.value, keyword: keyword.value || undefined })
|
|
projectList.value = res.data || []
|
|
total.value = res.total || 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
page.value = 1
|
|
fetchData()
|
|
}
|
|
|
|
function handleAdd() {
|
|
form.name = ''
|
|
form.template_id = templates.value[0]?.id
|
|
form.target_source_ids = []
|
|
form.description = ''
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
submitLoading.value = true
|
|
try {
|
|
await createProject({
|
|
name: form.name,
|
|
template_id: form.template_id!,
|
|
target_source_ids: form.target_source_ids.join(','),
|
|
description: form.description,
|
|
})
|
|
ElMessage.success('创建成功')
|
|
dialogVisible.value = false
|
|
fetchData()
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.message || '创建失败')
|
|
} finally {
|
|
submitLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleAutoClassify(p: ProjectItem) {
|
|
try {
|
|
const res: any = await autoClassifyProject(p.id)
|
|
ElMessage.success(res.message || '自动分类完成')
|
|
fetchData()
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.message || '自动分类失败')
|
|
}
|
|
}
|
|
|
|
async function handleDelete(p: ProjectItem) {
|
|
try {
|
|
await ElMessageBox.confirm('确认删除该项目?', '提示', { type: 'warning' })
|
|
await deleteProject(p.id)
|
|
ElMessage.success('删除成功')
|
|
fetchData()
|
|
} catch (e: any) {
|
|
if (e !== 'cancel') ElMessage.error(e?.message || '删除失败')
|
|
}
|
|
}
|
|
|
|
async function fetchMeta() {
|
|
try {
|
|
const [tRes, sRes] = await Promise.all([getTemplates(), getDataSources()])
|
|
templates.value = (tRes as any)?.data || []
|
|
dataSources.value = (sRes as any)?.data || []
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchData()
|
|
fetchMeta()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
|
|
.page-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
}
|
|
}
|
|
|
|
.search-bar {
|
|
padding: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.project-list {
|
|
.el-col {
|
|
margin-bottom: 16px;
|
|
}
|
|
}
|
|
|
|
.project-card {
|
|
padding: 20px;
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
transition: box-shadow 0.2s;
|
|
|
|
&:hover {
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.project-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
|
|
.project-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
}
|
|
}
|
|
|
|
.project-desc {
|
|
font-size: 13px;
|
|
color: #909399;
|
|
margin-bottom: 16px;
|
|
min-height: 20px;
|
|
}
|
|
|
|
.project-stats {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
padding: 12px 0;
|
|
border-top: 1px solid #f0f0f0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
flex: 1;
|
|
|
|
.stat-num {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #303133;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
margin-top: 2px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.project-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
}
|
|
</style>
|