fix: 提交代码

This commit is contained in:
liuzhiyuan 2025-10-17 18:02:55 +08:00
parent fda6156df5
commit 87ad6f9428
20 changed files with 5487 additions and 149 deletions

View File

@ -3,44 +3,44 @@ import request from '@/config/axios'
// 获取规则类目列表
export const getAttCleanCatList = (params) => {
return request.get({
url: '/v2/att/attCleanCat/list',
url: '/att/attCleanCat/list',
params
})
}
// 添加规则类目
export const saveAttCleanCat = (data) => {
return request.post({ url: '/v2/att/attCleanCat', data })
return request.post({ url: '/att/attCleanCat', data })
}
// 更新规则类目
export const updateAttCleanCat = (data) => {
return request.put({ url: '/v2/att/attCleanCat', data })
return request.put({ url: '/att/attCleanCat', data })
}
// 删除规则类目
export const deleteAttCleanCat = (id) => {
return request.delete({ url: `/v2/att/attCleanCat/${id}`})
return request.delete({ url: `/att/attCleanCat/${id}`})
}
// 获取规则列表
export const getAttCleanRuleList = (params) => {
return request.get({
url: '/v2/att/attCleanRule/list',
url: '/att/attCleanRule/list',
params
})
}
// 添加规则
export const saveAttCleanRule = (data) => {
return request.post({ url: '/v2/att/attCleanRule', data })
return request.post({ url: '/att/attCleanRule', data })
}
// 更新规则
export const updateAttCleanRule = (data) => {
return request.put({ url: '/v2/att/attCleanRule', data })
return request.put({ url: '/att/attCleanRule', data })
}
// 删除规则
export const deleteAttCleanRule = (id) => {
return request.delete({ url: `/v2/att/attCleanRule/${id}`})
return request.delete({ url: `/att/attCleanRule/${id}`})
}

View File

@ -3,22 +3,22 @@ import request from '@/config/axios'
// 获取规则列表
export const getAttAuditRuleList = (params) => {
return request.get({
url: '/v2/att/attAuditRule/list',
url: '/att/attAuditRule/list',
params
})
}
// 添加规则
export const addAttAuditRule = (data) => {
return request.post({ url: '/v2/att/attAuditRule', data })
return request.post({ url: '/att/attAuditRule', data })
}
// 更新规则
export const updateAttAuditRule = (data) => {
return request.put({ url: '/v2/att/attAuditRule', data })
return request.put({ url: '/att/attAuditRule', data })
}
// 删除规则
export const deleteAttAuditRule = (id) => {
return request.delete({ url: `/v2/att/attAuditRule/${id}`})
return request.delete({ url: `/att/attAuditRule/${id}`})
}

View File

@ -78,3 +78,7 @@ export const importUserTemplates = (params: any) => {
return request.download({ url: '/llm/dataset/download-example', params })
}
export const getAllList = async () => {
return await request.get({url: '/data/data-set-middle/getAllList'})
}

View File

@ -0,0 +1,80 @@
import request from '@/config/axios'
export interface DataSetVO {
id?: string //数据集ID
datasetName: string //数据集名称
datasetCategory: string //数据集类型
status: string //状态
datasetIntro: string //数据集描述
datasetFile: string //数据文件
datasetType: string //数据集类型
datasetFileUrl: string //文件URL地址
tenantId: number //租户编号
}
// 新增
export const createDataSet = (data: DataSetVO) => {
return request.post({url: '/data/data-set-middle/create', data})
}
// 新增 - 用于datasetParentType为2的情况
export const createDataSetV2 = (data: DataSetVO) => {
return request.post({url: '/llm/dataset/createDatasetMoreModal', data})
}
// 修改
export const updateDataSet = (data: DataSetVO) => {
return request.put({url: '/data/data-set-middle/update', data})
}
// 修改 - 用于datasetParentType为2的情况
export const updateDataSetV2 = (data: DataSetVO) => {
return request.put({url: '/llm/dataset/update-v2', data})
}
// 删除
export const deleteDataSet = (id: number) => {
return request.delete({url: '/llm/dataset/delete?id=' + id})
}
// 查询详情
export const getDeta = (id: number) => {
return request.get({url: '/data/data-set-middle/getOneInfo?id=' + id})
}
// 查询列表
export const getPage = async (params: PageParam) => {
return await request.get({url: '/data/data-set-middle/page', params})
}
// 查询全部
export const getAll = async () => {
return await request.get({url: '/llm/dataset/all'})
}
// 获得数据集数据问题分页
export const getQuestionPageList = async (params: any) => {
return await request.get({url: '/platform/dataset-question/page', params})
}
// 保存标注接口
export const updateQuestion = (data: any) => {
return request.put({url: '/platform/dataset-question/data-anno', data})
}
// 导出数据集
export const exportData = (params: any) => {
return request.download({ url: '/llm/dataset-question/export-excel', params })
}
// 下载数据集模板
export const importUserTemplate = () => {
return request.download({ url: '/infra/file/29/get/8e05fde5b769415cc7a6323aa3d7b86fdd2daa067018d16d03eb3b7cf9707801.json' })
}
// 下载数据集模板
export const importUserTemplates = (params: any) => {
return request.download({ url: '/llm/dataset/download-example', params })
}

View File

@ -0,0 +1,84 @@
<template>
<div class="asset-map-container">
<el-card shadow="never" class="mb-20">
<template #header>
<div class="card-header">
<span>资产地图</span>
</div>
</template>
<!-- 页面内容区域 -->
<div class="content-area">
<div class="empty-state" v-if="!dataLoaded">
<el-empty description="暂无数据" />
</div>
<div class="map-content" v-else>
<!-- 地图内容将在这里渲染 -->
</div>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from '@/hooks/web/useI18n'
//
const dataLoaded = ref(false)
//
const message = useMessage()
const { t } = useI18n()
//
const initData = async () => {
try {
//
// await fetchMapData()
dataLoaded.value = true
} catch (error) {
console.error('初始化资产地图数据失败:', error)
message.error('加载数据失败')
}
}
//
onMounted(() => {
initData()
})
</script>
<style lang="scss" scoped>
.asset-map-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.content-area {
min-height: 400px;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.map-content {
width: 100%;
height: 600px;
border-radius: 8px;
overflow: hidden;
background-color: #f5f7fa;
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="asset-map-container">
<el-card shadow="never" class="mb-20">
<template #header>
<div class="card-header">
<span>数据链接</span>
</div>
</template>
<!-- 页面内容区域 -->
<div class="content-area">
<div class="empty-state" v-if="!dataLoaded">
<el-empty description="暂无数据" />
</div>
<div class="map-content" v-else>
<!-- 地图内容将在这里渲染 -->
</div>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from '@/hooks/web/useI18n'
//
const dataLoaded = ref(false)
//
const message = useMessage()
const { t } = useI18n()
//
const initData = async () => {
try {
//
// await fetchMapData()
dataLoaded.value = true
} catch (error) {
console.error('初始化资产地图数据失败:', error)
message.error('加载数据失败')
}
}
//
onMounted(() => {
initData()
})
</script>
<style lang="scss" scoped>
.asset-map-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.content-area {
min-height: 400px;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.map-content {
width: 100%;
height: 600px;
border-radius: 8px;
overflow: hidden;
background-color: #f5f7fa;
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="asset-map-container">
<el-card shadow="never" class="mb-20">
<template #header>
<div class="card-header">
<span>数据查询</span>
</div>
</template>
<!-- 页面内容区域 -->
<div class="content-area">
<div class="empty-state" v-if="!dataLoaded">
<el-empty description="暂无数据" />
</div>
<div class="map-content" v-else>
<!-- 地图内容将在这里渲染 -->
</div>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from '@/hooks/web/useI18n'
//
const dataLoaded = ref(false)
//
const message = useMessage()
const { t } = useI18n()
//
const initData = async () => {
try {
//
// await fetchMapData()
dataLoaded.value = true
} catch (error) {
console.error('初始化资产地图数据失败:', error)
message.error('加载数据失败')
}
}
//
onMounted(() => {
initData()
})
</script>
<style lang="scss" scoped>
.asset-map-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.content-area {
min-height: 400px;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.map-content {
width: 100%;
height: 600px;
border-radius: 8px;
overflow: hidden;
background-color: #f5f7fa;
}
</style>

View File

@ -121,6 +121,7 @@ const submitForm = async () => {
}
const fileItem = fileList.value[0];
console.log('========>',fileItem.raw)
if (!fileItem || !fileItem.raw) {
message.error('文件上传异常,请重新选择文件');
return;

View File

@ -132,8 +132,10 @@ const submitForm = async () => {
console.log(importFormRef.value.fileList)
if (importFormRef.value.fileList.length == 0 && route?.query.id || (Array.isArray(form.value.datasetFiles) && form.value.datasetFiles.length > 0)) {
console.log(111)
await importSuccess()
} else {
console.log(222)
await importFormRef.value.submitForm()
}
}

View File

@ -0,0 +1,519 @@
<template>
<el-dialog
v-model="dialogVisible"
title="新增清洗规则"
width="900px"
:before-close="handleCancel"
>
<!-- 左侧导航菜单 -->
<div class="dialog-content">
<el-aside width="240px" class="sidebar">
<!-- 导航头部搜索框 -->
<div class="sidebar-header">
<el-input
v-model="searchKeyword"
placeholder="请输入规则类型"
:prefix-icon="Search"
class="sidebar-search"
/>
</div>
<!-- 动态导航菜单 -->
<el-menu
:default-active="activeMenu"
class="quality-menu"
background-color="#ffffff"
text-color="#1D2129"
active-text-color="#165DFF"
:unique-opened="true"
>
<!-- 动态生成菜单 -->
<template v-for="menu in filteredMenus" :key="menu.index">
<!-- 有子菜单的情况 -->
<el-sub-menu v-if="menu.children && menu.children.length" :index="menu.index">
<template #title>
<span><el-icon><List /></el-icon>{{ menu.title }}</span>
</template>
<!-- 二级菜单 -->
<el-menu-item
v-for="subMenu in menu.children"
:key="subMenu.index"
:index="subMenu.index"
@click="handleMenuClick(subMenu)"
>
<span><el-icon><List /></el-icon>{{ subMenu.title }}</span>
</el-menu-item>
</el-sub-menu>
<!-- 没有子菜单的情况 -->
<el-menu-item v-else :index="menu.index" @click="handleMenuClick(menu)">
<span><el-icon><List /></el-icon>{{ menu.title }}</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<!-- 主内容区域 -->
<el-main class="main">
<!-- 规则列表 -->
<div class="rule-grid">
<div
v-for="rule in filteredRules"
:key="rule.id"
class="rule-card"
:class="{ 'selected': selectedRules.includes(rule.id) }"
@click="toggleRuleSelection(rule)"
>
<div class="rule-card-header">
<div class="rule-title">{{ rule.name }}</div>
<el-checkbox v-model="rule.selected" @change="toggleRuleSelection(rule)">
<span v-if="rule.status" class="status-badge">上线</span>
</el-checkbox>
</div>
<div class="rule-code">{{ rule.rule }}</div>
<div class="rule-description">{{ rule.description }}</div>
</div>
</div>
</el-main>
</div>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { Search, List } from '@element-plus/icons-vue'
// API使index.vueAPI
import { getAttCleanCatList, getAttCleanRuleList } from '@/api/base/cleaningRules'
//
interface MenuNode {
index: string
title: string
id: number
parentId: number
sortOrder?: number
children: MenuNode[]
}
interface CleaningRule {
id: string
name: string
rule: string
description: string
category: string
status: boolean
selected: boolean
}
// Props
interface Props {
visible: boolean
}
const props = withDefaults(defineProps<Props>(), {
visible: false
})
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean]
'confirm': [selectedRules: any[]]
'cancel': []
}>()
//
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
//
const searchKeyword = ref('')
//
const activeMenu = ref('')
// ID
const selectedRules = ref<string[]>([])
//
const menuData = ref<MenuNode[]>([])
//
const menuLoading = ref(false)
//
const loadMenuData = async () => {
try {
menuLoading.value = true
//
const response = await getAttCleanCatList({})
//
menuData.value = convertToTree(response.data || response)
//
if (menuData.value.length > 0) {
const firstMenu = menuData.value[0]
if (firstMenu.children && firstMenu.children.length > 0) {
activeMenu.value = firstMenu.children[0].index
} else {
activeMenu.value = firstMenu.index
}
}
} catch (error) {
console.error('获取清洗规则类型数据失败:', error)
// 使
const mockData = [
{ id: 1, code: 'accuracy', name: '准确性修正', parentId: 0, sortOrder: 1 },
{ id: 2, code: 'accuracy-1', name: '数值边界调整', parentId: 1, sortOrder: 1 },
{ id: 3, code: 'accuracy-2', name: '字段前后缀统一', parentId: 1, sortOrder: 2 },
{ id: 4, code: 'completeness', name: '完整性修复', parentId: 0, sortOrder: 2 },
{ id: 5, code: 'completeness-1', name: '组合字段为空删除', parentId: 4, sortOrder: 1 },
{ id: 6, code: 'completeness-2', name: '字段补零', parentId: 4, sortOrder: 2 },
{ id: 7, code: 'consistency', name: '一致性修正', parentId: 0, sortOrder: 3 },
{ id: 8, code: 'consistency-1', name: '枚举值映射标准化', parentId: 7, sortOrder: 1 },
{ id: 9, code: 'consistency-2', name: '字段值替换', parentId: 7, sortOrder: 2 },
{ id: 10, code: 'uniqueness', name: '唯一性维护', parentId: 0, sortOrder: 4 },
{ id: 11, code: 'uniqueness-1', name: '手机号格式统一', parentId: 10, sortOrder: 1 },
{ id: 12, code: 'uniqueness-2', name: '日期格式统一', parentId: 10, sortOrder: 2 },
{ id: 13, code: 'validity', name: '有效性处理', parentId: 0, sortOrder: 5 },
{ id: 14, code: 'validity-1', name: '字符串转数值', parentId: 13, sortOrder: 1 },
{ id: 15, code: 'validity-2', name: '字符串转日期', parentId: 13, sortOrder: 2 },
{ id: 16, code: 'validity-3', name: '任意类型转布尔值', parentId: 13, sortOrder: 3 },
{ id: 17, code: 'timeliness', name: '及时性调整', parentId: 0, sortOrder: 6 },
{ id: 18, code: 'timeliness-1', name: '小数位统一', parentId: 17, sortOrder: 1 },
{ id: 19, code: 'timeliness-2', name: '去除字段空格', parentId: 17, sortOrder: 2 },
{ id: 20, code: 'timeliness-3', name: '正则表达式替换', parentId: 17, sortOrder: 3 },
{ id: 21, code: 'timeliness-4', name: '超长字段截断', parentId: 17, sortOrder: 4 },
{ id: 22, code: 'timeliness-5', name: '数值空值填充', parentId: 17, sortOrder: 5 }
]
menuData.value = convertToTree(mockData)
//
if (menuData.value.length > 0) {
const firstMenu = menuData.value[0]
if (firstMenu.children && firstMenu.children.length > 0) {
activeMenu.value = firstMenu.children[0].index
} else {
activeMenu.value = firstMenu.index
}
}
} finally {
menuLoading.value = false
}
}
//
const convertToTree = (items: any[]): MenuNode[] => {
if (!items || !Array.isArray(items)) return []
// id便
const map: Record<number, MenuNode> = {}
const tree: MenuNode[] = []
//
items.forEach(item => {
map[item.id] = {
index: item.code, // 使code
title: item.name, // 使name
id: item.id,
parentId: item.parentId,
sortOrder: item.sortOrder,
children: []
}
})
//
items.forEach(item => {
const node = map[item.id]
if (item.parentId === 0) {
//
tree.push(node)
} else {
//
const parent = map[item.parentId]
if (parent) {
parent.children.push(node)
} else {
//
tree.push(node)
}
}
})
// sortOrder
const sortByOrder = (nodes: MenuNode[]) => {
if (!nodes || !nodes.length) return
nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
nodes.forEach(node => {
if (node.children && node.children.length) {
sortByOrder(node.children)
}
})
}
sortByOrder(tree)
return tree
}
//
const rulesData = ref<CleaningRule[]>([])
//
const rulesLoading = ref(false)
//
const loadRulesData = async () => {
try {
rulesLoading.value = true
//
const response = await getAttCleanRuleList({})
// selected
const rules = response.data || response || []
rulesData.value = rules.map(rule => ({
...rule,
selected: false // selected
}))
} catch (error) {
console.error('获取清洗规则数据失败:', error)
// 使
rulesData.value = [
{ id: '1', name: '数值边界调整', rule: '数值边界调整', description: '将超过预设范围的数值调整为合理的边界值。', category: 'accuracy-1', status: true, selected: false },
{ id: '2', name: '字段前后缀统一', rule: '字段前后缀统一', description: '对宇段值统一加前后缀,或去除非业务所需的标记。', category: 'accuracy-2', status: true, selected: false },
{ id: '3', name: '组合字段为空删除', rule: '组合字段为空删除', description: '配置关键宇段(如主键)为空时,删除整行记录。', category: 'completeness-1', status: true, selected: false },
{ id: '4', name: '枚举值映射标准化', rule: '枚举值映射标准化', description: '将宇段值根据字典进行映射转换,保留值统一。', category: 'consistency-1', status: true, selected: false },
{ id: '5', name: '字段值替换', rule: '字段值替换', description: '将不符合规则设置的数值替换为指定的默认或空值。', category: 'consistency-2', status: true, selected: false },
{ id: '6', name: '字符串转数值', rule: '字符串转数值', description: '将规范宇符串转换为数值类型。', category: 'validity-1', status: true, selected: false },
{ id: '7', name: '字符串转日期', rule: '字符串转日期', description: '将符合格式的宇符串转换为日期类型。', category: 'validity-2', status: true, selected: false },
{ id: '8', name: '任意类型转布尔值', rule: '任意类型转布尔值', description: '将是/否、"1"/"0"、"true"/"false"等转换为布尔值。', category: 'validity-3', status: true, selected: false },
{ id: '9', name: '手机号格式统一', rule: '手机号格式统一', description: '统一手机号为国内11位数宇格式去掉国家位、空格、短横线等。', category: 'uniqueness-1', status: true, selected: false },
{ id: '10', name: '日期格式统一', rule: '日期格式统一', description: '统一各种格式日期为指定标准格式如yyyy-MM-dd支持配置模板。', category: 'uniqueness-2', status: true, selected: false },
{ id: '11', name: '小数位统一', rule: '小数位统一', description: '将数值字段统一保留指定的小数位数(保留两位小数)。', category: 'timeliness-1', status: true, selected: false },
{ id: '12', name: '去除字段空格', rule: '去除字段空格', description: '去除宇段中的前导空格或中间多余空格,提升匹配准确性。', category: 'timeliness-2', status: true, selected: false },
{ id: '13', name: '正则表达式替换', rule: '正则表达式替换', description: '使用正则表达式匹配并替换字段值。', category: 'timeliness-3', status: true, selected: false },
{ id: '14', name: '超长字段截断', rule: '超长字段截断', description: '当字段长度超过指定长度时进行截断处理。', category: 'timeliness-4', status: true, selected: false },
{ id: '15', name: '数值空值填充', rule: '数值空值填充', description: '将空的数值字段填充为指定默认值如0、1、平均值等。', category: 'timeliness-5', status: true, selected: false },
{ id: '16', name: '字段补零', rule: '字段补零', description: '在字段前后补零,保持指定长度。', category: 'completeness-2', status: true, selected: false }
]
} finally {
rulesLoading.value = false
}
}
//
const filteredMenus = computed(() => {
if (!searchKeyword.value) return menuData.value
const filterMenu = (menus: MenuNode[]): MenuNode[] => {
return menus.filter(menu => {
//
if (menu.title.includes(searchKeyword.value)) {
return true
}
//
if (menu.children && menu.children.length) {
const filteredChildren = filterMenu(menu.children)
if (filteredChildren.length > 0) {
menu.children = filteredChildren
return true
}
}
return false
})
}
return filterMenu(JSON.parse(JSON.stringify(menuData.value)))
})
//
const filteredRules = computed(() => {
if (!activeMenu.value) {
return rulesData.value
}
return rulesData.value.filter(rule => rule.category === activeMenu.value)
})
//
const handleMenuClick = (menu: MenuNode) => {
activeMenu.value = menu.index
}
//
const toggleRuleSelection = (rule: CleaningRule) => {
if (rule.selected) {
//
const index = selectedRules.value.indexOf(rule.id)
if (index > -1) {
selectedRules.value.splice(index, 1)
}
rule.selected = false
} else {
//
selectedRules.value.push(rule.id)
rule.selected = true
}
}
//
onMounted(async () => {
//
await Promise.all([loadMenuData(), loadRulesData()])
})
//
const handleConfirm = () => {
const selected = rulesData.value.filter(rule => selectedRules.value.includes(rule.id))
emit('confirm', selected)
resetState()
dialogVisible.value = false
}
//
const handleCancel = () => {
emit('cancel')
resetState()
dialogVisible.value = false
}
//
const resetState = () => {
selectedRules.value = []
rulesData.value.forEach(rule => {
rule.selected = false
})
}
</script>
<style scoped>
.dialog-content {
display: flex;
height: 450px;
}
.sidebar {
background-color: #ffffff;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
}
/* 侧边栏头部搜索样式 */
.sidebar-header {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
.sidebar-search {
width: 100%;
--el-input-height: 32px;
background-color: #f5f7fa;
border-color: #e5e7eb;
}
.sidebar-search .el-input__wrapper {
box-shadow: none;
}
/* 菜单样式调整 */
.quality-menu {
border-right: none;
flex: 1;
overflow-y: auto;
}
.quality-menu :deep(.el-sub-menu__title),
.quality-menu :deep(.el-menu-item) {
height: 40px;
line-height: 40px;
font-size: 14px;
color: #1D2129;
}
.quality-menu :deep(.el-sub-menu__title:hover),
.quality-menu :deep(.el-menu-item:hover) {
background-color: #f7f8fa;
}
.quality-menu :deep(.el-menu-item.is-active) {
background-color: #e8f3ff !important;
color: #165DFF !important;
font-weight: 500;
}
/* 主内容区域样式 */
.main {
flex: 1;
padding: 15px;
overflow-y: auto;
background-color: #f7f8fa;
}
/* 规则卡片网格 */
.rule-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
}
/* 规则卡片样式 */
.rule-card {
background-color: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.rule-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border-color: #165DFF;
}
.rule-card.selected {
border-color: #165DFF;
background-color: #f0f7ff;
}
.rule-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.rule-title {
font-size: 16px;
font-weight: 500;
color: #1D2129;
flex: 1;
}
.rule-code {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.rule-description {
font-size: 13px;
color: #86909C;
line-height: 1.5;
}
.status-badge {
background-color: #f0f9eb;
color: #67c23a;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,389 @@
<template>
<el-card shadow="never" class="b-r-top-15" style="margin-bottom: 10px">
<div class="card-header">
<el-icon @click="goBack"><ArrowLeft /></el-icon>
<span>{{ title }}</span>
</div>
</el-card>
<el-card shadow="never" class="b-r-bottom-15">
<div style="padding: 0 20px;">
<!-- 流域代码基本信息 -->
<div class="basin-info-container">
<h3 class="info-title">流域代码</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">英文名称</span>
<span class="info-value">{{ form.basCode }}</span>
</div>
<div class="info-item">
<span class="info-label">类目编码</span>
<span class="info-value">{{ form.categoryCode || '水土保持' }}</span>
</div>
<div class="info-item">
<span class="info-label">类型</span>
<span class="info-value">{{ form.type || '数据元' }}</span>
</div>
<div class="info-item">
<span class="info-label">责任人</span>
<span class="info-value">{{ form.responsiblePerson || '管理员' }}</span>
</div>
<div class="info-item">
<span class="info-label">联系电话</span>
<span class="info-value">{{ form.contactPhone || '13800000001' }}</span>
</div>
<div class="info-item">
<span class="info-label">字段类型</span>
<span class="info-value">{{ form.fieldType || 'VARCHAR2' }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value">{{ form.status || '启用' }}</span>
</div>
<div class="info-item">
<span class="info-label">创建时间</span>
<span class="info-value">{{ form.createTime || '2025-06-12' }}</span>
</div>
<div class="info-item">
<span class="info-label">创建人</span>
<span class="info-value">{{ form.creator || 'admin' }}</span>
</div>
<div class="info-item full-width">
<span class="info-label">描述</span>
<span class="info-value">{{ form.description || '主键,唯一标识流域' }}</span>
</div>
</div>
</div>
<!-- 关联清洗规则 -->
<div class="cleaning-rules-container">
<div class="rules-header">
<h3 class="rules-title">关联清洗规则</h3>
<el-button type="primary" size="small" class="add-rule-btn" @click="showRuleSelector = true">
关联 +
</el-button>
</div>
<el-table
:data="cleaningRulesList"
show-header="true"
:header-cell-style="{ background: '#f2f3f5', color: '#6b7785', fontWeight: 'bold' }"
border
>
<el-table-column label="序号" width="80">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="清洗名称" prop="name" min-width="150" />
<el-table-column label="清洗规则" prop="rule" min-width="150" />
<el-table-column label="规则描述" prop="description" min-width="200">
<template #default="scope">
{{ scope.row.description || '-' }}
</template>
</el-table-column>
<el-table-column label="维度" prop="dimension" min-width="150" />
<el-table-column label="状态" prop="status" min-width="100">
<template #default="scope">
<span class="status-online">{{ scope.row.status || '上线' }}</span>
</template>
</el-table-column>
<el-table-column label="关联时间" prop="关联时间" min-width="180" />
<el-table-column label="操作" width="80">
<!-- <template #default="scope">
<a href="#" class="view-link">查看</a>
</template> -->
</el-table-column>
</el-table>
<div style="margin-top: 15px; text-align: right;">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getCleaningRulesList"
/>
</div>
</div>
</div>
</el-card>
<!-- 清洗规则选择弹窗 -->
<CleaningRuleSelector
v-model:visible="showRuleSelector"
@confirm="handleRuleConfirm"
@cancel="handleRuleCancel"
/>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
// import { getBasinCodeDetail, getCleaningRulesPageList } from '@/api/basinCode'
import { ArrowLeft } from '@element-plus/icons-vue'
import { useRouter, useRoute } from 'vue-router'
import CleaningRuleSelector from './components/CleaningRuleSelector.vue'
//
const router = useRouter()
const route = useRoute()
const title = ref('数据元详情')
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
const total = ref(0) //
const form = ref<any>({
basCode: 'BAS_CODE',
categoryCode: '水土保持',
type: '数据元',
responsiblePerson: '管理员',
contactPhone: '13800000001',
fieldType: 'VARCHAR2',
status: '启用',
createTime: '2025-06-12',
creator: 'admin',
description: '主键,唯一标识流域'
})
//
const showRuleSelector = ref(false)
//
const cleaningRulesList = ref([
{
name: '数值边界调整',
rule: '数值边界调整',
dimension: '异常值修正类',
status: '上线',
'关联时间': '2025-08-28 11:54:04'
},
{
name: '字段前后缀统一',
rule: '字段前后缀统一',
dimension: '格式标准化类',
status: '上线',
'关联时间': '2025-08-28 13:47:29'
},
{
name: '枚举值映射标准化',
rule: '枚举值映射标准化',
dimension: '字段映射替换类',
status: '上线',
'关联时间': '2025-08-28 16:27:23'
},
{
name: '23',
rule: '数值边界调整',
dimension: '异常值修正类',
status: '上线',
'关联时间': '2025-09-24 14:38:23'
}
])
//
const goBack = () => {
router.push({
path: '/basinCodeManagement',
query: {
//
}
})
}
const getDetail = async () => {
const id: number = Number(route.query?.id) || 0
let res = await getBasinCodeDetail(id)
if (res) {
form.value = res
}
await getCleaningRulesList()
}
const getCleaningRulesList = async () => {
let res: any = await getCleaningRulesPageList({
basinCodeId: Number(route.query?.id) || 0,
...queryParams
})
if (res) {
cleaningRulesList.value = res.list
total.value = res.total
}
}
//
const handleRuleConfirm = (selectedRules) => {
//
selectedRules.forEach(rule => {
//
let dimension = ''
if (rule.category.includes('accuracy')) {
dimension = '异常值修正类'
} else if (rule.category.includes('completeness')) {
dimension = '完整性修复类'
} else if (rule.category.includes('consistency')) {
dimension = '字段映射替换类'
} else if (rule.category.includes('uniqueness')) {
dimension = '唯一性维护类'
} else if (rule.category.includes('validity')) {
dimension = '有效性处理类'
} else if (rule.category.includes('timeliness')) {
dimension = '及时性调整类'
}
//
const exists = cleaningRulesList.value.some(item => item.name === rule.name)
if (!exists) {
cleaningRulesList.value.push({
name: rule.name,
rule: rule.rule,
dimension: dimension,
status: rule.status ? '上线' : '下线',
'关联时间': new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-')
})
}
})
//
total.value = cleaningRulesList.value.length
}
//
const handleRuleCancel = () => {
//
console.log('取消选择规则')
}
getDetail()
onMounted(() => {})
</script>
<style lang="scss" scoped>
.card-header {
display: flex;
align-items: center;
height: 30px;
margin-bottom: 10px;
.el-icon {
cursor: pointer;
margin-right: 8px;
color: #666;
}
span {
font-size: 16px;
font-weight: bold;
color: #333;
}
}
.basin-info-container {
margin-bottom: 30px;
padding: 15px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.info-title {
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
font-size: 14px;
color: #333;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px 20px;
}
.info-item {
display: flex;
align-items: flex-start;
}
.full-width {
grid-column: 1 / -1;
}
.info-label {
flex: 0 0 100px;
color: #666;
font-weight: 500;
font-size: 14px;
}
.info-value {
flex: 1;
color: #333;
word-break: break-all;
font-size: 14px;
}
.status-active {
color: #409eff;
padding: 2px 6px;
background-color: #ecf5ff;
border-radius: 4px;
font-size: 12px;
}
.cleaning-rules-container {
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
padding: 15px;
}
.rules-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.rules-title {
margin: 0;
font-size: 16px;
color: #333;
}
.add-rule-btn {
background-color: #409eff;
border-color: #409eff;
}
.status-online {
color: #67c23a;
padding: 2px 6px;
background-color: #f0f9eb;
border-radius: 4px;
font-size: 12px;
}
.view-link {
color: #409eff;
text-decoration: none;
}
.view-link:hover {
text-decoration: underline;
}
::v-deep .el-table {
margin-bottom: 15px;
}
</style>

View File

@ -51,108 +51,142 @@
</template>
</el-sub-menu>
<!-- 没有子菜单的情况 -->
<el-menu-item v-else :index="menu.index" @click="handleMenuClick(menu)">
<span><el-icon><list /></el-icon>{{ menu.title }}</span>
</el-menu-item>
<el-sub-menu v-else :index="menu.index" @click="handleMenuClick(menu)">
<template #title>
<span><el-icon><list /></el-icon>{{ menu.title }}</span>
</template>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
<!-- 主内容区域 -->
<el-main class="main">
<!-- 搜索区域 -->
<!-- 搜索区域 - 调整为三输入框布局 -->
<div class="search-container">
<el-row :gutter="20">
<el-col :span="5">
<el-row :gutter="15">
<el-col :span="6">
<el-input
v-model="searchForm.ruleType"
placeholder="请输入规则类型"
v-model="searchForm.chineseName"
placeholder="请输入中文名称"
prefix-icon="Search"
class="search-input"
/>
</el-col>
<el-col :span="6">
<el-input
v-model="searchForm.englishName"
placeholder="请输入英文名称"
prefix-icon="Search"
class="search-input"
/>
</el-col>
<el-col :span="5">
<el-input
v-model="searchForm.ruleName"
placeholder="请输入规则名称"
prefix-icon="Search"
class="search-input"
/>
<el-select
v-model="searchForm.type"
placeholder="请选择类型"
class="search-select"
>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="5">
<el-input
v-model="searchForm.ruleCode"
placeholder="请输入规则编码"
prefix-icon="Search"
class="search-input"
/>
<el-col :span="3">
<el-button type="primary" @click="handleSearch" class="search-btn">查询</el-button>
</el-col>
<el-col :span="4">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-col :span="3">
<el-button @click="handleReset" class="reset-btn">重置</el-button>
</el-col>
</el-row>
</div>
<!-- 表格区域 -->
<div class="table-container">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增
</el-button>
<el-table
:data="tableData"
v-loading="loading"
height="500"
show-header=true
:header-cell-style="{ background: '#f2f3f5', color: '#6b7785', fontWeight: 'bold' }"
border
>
<el-table-column
prop="ruleCode"
label="规则编码"
type="index"
label="编号"
width="60"
align="center"
/>
<el-table-column
prop="chineseName"
label="中文名称"
width="160"
/>
<el-table-column
prop="englishName"
label="英文名称"
width="160"
/>
<el-table-column
prop="type"
label="类型"
width="100"
/>
>
<template #default="scope">
<el-tag type="success" size="small">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="ruleName"
label="规则名称"
width="180"
/>
<el-table-column
prop="qualityDimension"
label="质量维度"
prop="dataElement"
label="数据元素类目"
width="120"
/>
<el-table-column
prop="fieldType"
label="字段类型"
width="120"
/>
<el-table-column
prop="status"
label="状态"
width="100"
align="center"
>
<template #default="scope">
<el-tag
:type="getDimensionTagType(scope.row.qualityDimension)"
size="small"
>
{{ scope.row.qualityDimension }}
</el-tag>
<el-switch
v-model="scope.row.status"
active-color="#13ce66"
inactive-color="#ff4949"
:disabled="true"
/>
</template>
</el-table-column>
<el-table-column
prop="ruleDescription"
label="规则描述"
min-width="200"
prop="description"
label="描述"
min-width="180"
/>
<el-table-column
prop="useScenario"
label="使用场景"
min-width="200"
/>
<el-table-column
prop="example"
label="示例"
min-width="200"
>
<template #default="scope">
<div v-html="scope.row.example"></div>
</template>
</el-table-column>
<el-table-column
label="操作"
width="120"
width="180"
align="center"
>
<template #default>
<el-button size="small" type="link" class="operation-btn">编辑</el-button>
<el-button size="small" type="link" class="operation-btn" style="color: #ff4d4f">删除</el-button>
<template #default="scope">
<el-button size="small" type="text" class="operation-btn edit-btn" @click="handleEdit(scope.row)">
<el-icon><Edit /></el-icon>修改
</el-button>
<el-button size="small" type="text" class="operation-btn delete-btn" @click="handleDelete(scope.row)">
<el-icon><Delete /></el-icon>删除
</el-button>
<el-button size="small" type="text" class="operation-btn detail-btn" @click="handleDetail(scope.row)">
<el-icon><View /></el-icon>详情
</el-button>
</template>
</el-table-column>
</el-table>
@ -167,16 +201,142 @@
</el-main>
</div>
</el-card>
<!-- 表单弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="700px"
:before-close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
class="data-form"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="中文名称" prop="chineseName">
<el-input v-model="formData.chineseName" placeholder="请输入中文名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="英文名称" prop="englishName">
<el-input v-model="formData.englishName" placeholder="请输入英文名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio label="数据元">数据元</el-radio>
<el-radio label="代码集">代码集</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数据元素类目" prop="dataElement">
<el-select
v-model="formData.dataElement"
placeholder="请选择所属类目"
>
<el-option
v-for="item in dataElementOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="字段类型" prop="fieldType">
<el-select
v-model="formData.fieldType"
placeholder="请选择字段类型"
>
<el-option
v-for="item in fieldTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="负责人" prop="responsiblePerson">
<el-select
v-model="formData.responsiblePerson"
placeholder="请选择"
>
<el-option
v-for="person in responsiblePersons"
:key="person.value"
:label="person.label"
:value="person.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="元描述" prop="description">
<el-input
v-model="formData.description"
placeholder="请输入元描述"
type="textarea"
rows="4"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { Search, List } from "@element-plus/icons-vue";
import { getAttCleanCatList } from '@/api/base/cleaningRules'
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue';
import { Search, List, Plus, Edit, Delete, View, Refresh } from "@element-plus/icons-vue";
import { ElMessageBox } from 'element-plus'
// 使API
import { getAttCleanCatList, getAttCleanRuleList, saveAttCleanRule, updateAttCleanRule, deleteAttCleanRule } from '@/api/base/cleaningRules'
import { useRouter, useRoute } from 'vue-router'
//
const { proxy } = getCurrentInstance();
const router = useRouter() //
//
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
serviceName: ''
chineseName: '',
englishName: '',
type: ''
})
//
@ -184,19 +344,62 @@ const activeMenu = ref('');
//
const menuData = ref([]);
//
//
const menuLoading = ref(false);
//
const typeOptions = ref([
{ label: '数据元', value: '数据元' },
{ label: '代码集', value: '代码集' },
]);
//
const dataElementOptions = ref([
{ label: '用户信息', value: 'user_info' },
{ label: '订单信息', value: 'order_info' },
{ label: '产品信息', value: 'product_info' },
{ label: '交易信息', value: 'transaction_info' },
{ label: '系统配置', value: 'system_config' },
]);
//
const fieldTypeOptions = ref([
{ label: '字符串', value: 'string' },
{ label: '整数', value: 'integer' },
{ label: '浮点数', value: 'float' },
{ label: '日期', value: 'date' },
{ label: '布尔值', value: 'boolean' },
{ label: '数组', value: 'array' },
{ label: '对象', value: 'object' },
]);
//
const responsiblePersons = ref([
{ label: '张三', value: 'zhangsan' },
{ label: '李四', value: 'lisi' },
{ label: '王五', value: 'wangwu' },
{ label: '赵六', value: 'zhaoliu' },
{ label: '钱七', value: 'qianqi' },
]);
//
const loadMenuData = async () => {
try {
menuLoading.value = true;
const response = await getAttCleanCatList();
// { data: [...] }
const flatData = response || [];
//
const response = [
{ id: 1, code: 'category1', name: '基础数据', parentId: 0, sortOrder: 1 },
{ id: 2, code: 'category1-1', name: '用户数据', parentId: 1, sortOrder: 1 },
{ id: 3, code: 'category1-2', name: '产品数据', parentId: 1, sortOrder: 2 },
{ id: 4, code: 'category2', name: '业务数据', parentId: 0, sortOrder: 2 },
{ id: 5, code: 'category2-1', name: '订单数据', parentId: 4, sortOrder: 1 },
{ id: 6, code: 'category2-2', name: '交易数据', parentId: 4, sortOrder: 2 },
{ id: 7, code: 'category2-2-1', name: '线上交易', parentId: 6, sortOrder: 1 },
{ id: 8, code: 'category2-2-2', name: '线下交易', parentId: 6, sortOrder: 2 },
];
//
menuData.value = convertToTree(flatData);
menuData.value = convertToTree(response);
//
if (menuData.value.length > 0) {
@ -261,11 +464,6 @@ const convertToTree = (items) => {
return tree;
};
//
onMounted(() => {
loadMenuData();
});
//
const searchKeyword = ref('');
@ -298,80 +496,317 @@ const filteredMenus = computed(() => {
//
const searchForm = ref({
ruleType: '',
ruleName: '',
ruleCode: ''
chineseName : '',
englishName : '',
type : ''
});
//
//
const tableSearch = ref('');
// -
const tableData = ref([
{
ruleCode: '311',
ruleName: '多字段组合唯一性校验',
qualityDimension: '一致性',
ruleDescription: '检查字段组合在全表中是否唯一',
useScenario: '检查字段组合在全表中是否唯一',
example: '<ul><li>同一用户在某一天只能有一条记录</li></ul>'
id: '1',
chineseName: '用户ID',
englishName: 'userId',
type: '数据元',
dataElement: 'user_info',
fieldType: 'string',
status: true,
responsiblePerson: 'zhangsan',
contactPhone: '13800138000',
description: '用户唯一标识符,系统自动生成'
},
{
ruleCode: '110',
ruleName: '数值字段精度校验',
qualityDimension: '完整性',
ruleDescription: '检查小数位数是否符合业务定义如保留2位',
useScenario: '检查小数位数是否符合业务定义如保留2位',
example: '<ul><li>金额字段小数点后最多保留2位</li></ul>'
id: '2',
chineseName: '用户姓名',
englishName: 'userName',
type: '数据元',
dataElement: 'user_info',
fieldType: 'string',
status: true,
responsiblePerson: 'zhangsan',
contactPhone: '13800138000',
description: '用户的真实姓名'
},
{
ruleCode: '107',
ruleName: '字段字符类型校验',
qualityDimension: '完整性',
ruleDescription: '检查字段值是否为期望的字符组成(字母、数字等)',
useScenario: '检查字段值是否为期望的字符组成(字母、数字等)',
example: '<ul><li>手机号码段应为纯数字,出现字母则为异常</li></ul>'
id: '3',
chineseName: '用户状态',
englishName: 'userStatus',
type: '代码集',
dataElement: 'user_info',
fieldType: 'integer',
status: true,
responsiblePerson: 'lisi',
contactPhone: '13900139000',
description: '0-禁用1-正常2-锁定'
},
{
id: '4',
chineseName: '订单编号',
englishName: 'orderNo',
type: '数据元',
dataElement: 'order_info',
fieldType: 'string',
status: false,
responsiblePerson: 'wangwu',
contactPhone: '13700137000',
description: '订单唯一标识符,系统自动生成'
},
{
id: '5',
chineseName: '订单金额',
englishName: 'orderAmount',
type: '数据元',
dataElement: 'order_info',
fieldType: 'float',
status: true,
responsiblePerson: 'wangwu',
contactPhone: '13700137000',
description: '订单总金额,保留两位小数'
},
{
id: '6',
chineseName: '订单状态',
englishName: 'orderStatus',
type: '代码集',
dataElement: 'order_info',
fieldType: 'integer',
status: true,
responsiblePerson: 'zhaoliu',
contactPhone: '13600136000',
description: '0-待支付1-已支付2-已发货3-已完成4-已取消'
},
{
id: '7',
chineseName: '产品编码',
englishName: 'productCode',
type: '数据元',
dataElement: 'product_info',
fieldType: 'string',
status: true,
responsiblePerson: 'qianqi',
contactPhone: '13500135000',
description: '产品唯一编码'
}
]);
//
const total = ref(3);
//
const total = ref(7);
//
const loading = ref(false);
//
const dialogVisible = ref(false);
const dialogTitle = ref('新增数据元');
const currentId = ref('');
const formRef = ref(null);
//
const formData = reactive({
chineseName: '',
englishName: '',
type: '数据元',
dataElement: '',
fieldType: '',
status: false, //
responsiblePerson: '',
contactPhone: '',
description: ''
});
//
const formRules = ref({
chineseName: [
{ required: true, message: '请输入中文名称', trigger: 'blur' }
],
englishName: [
{ required: true, message: '请输入英文名称', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择类型', trigger: 'change' }
],
dataElement: [
{ required: true, message: '请选择数据元素类目', trigger: 'change' }
],
fieldType: [
{ required: true, message: '请选择字段类型', trigger: 'change' }
],
contactPhone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
});
//
const getList = async () => {
try {
loading.value = true;
//
await new Promise(resolve => setTimeout(resolve, 500));
// 使
// const response = await getAttCleanRuleList(params);
// tableData.value = response.data.list || [];
// total.value = response.data.total || 0;
} catch (error) {
console.error('获取列表数据失败:', error);
} finally {
loading.value = false;
}
};
//
const handleMenuClick = (menu) => {
activeMenu.value = menu.index;
console.log('选中菜单:', menu);
//
//
queryParams.pageNo = 1;
getList();
};
//
const handleSearch = () => {
console.log('搜索条件:', searchForm.value);
//
queryParams.pageNo = 1;
getList();
};
//
const handleTableSearch = () => {
//
console.log('表格搜索:', tableSearch.value);
//
};
//
const refreshTable = () => {
getList();
};
//
const handleReset = () => {
searchForm.value = {
ruleType: '',
ruleName: '',
ruleCode: ''
chineseName: '',
englishName: '',
type: ''
};
//
queryParams.pageNo = 1;
getList();
};
//
const getDimensionTagType = (dimension) => {
const tagTypes = {
'准确性': 'primary',
'一致性': 'success',
'规范性': 'warning',
'时效性': 'info',
'完整性': 'info'
};
return tagTypes[dimension] || 'default';
//
const handleAdd = () => {
dialogTitle.value = '新增数据元';
currentId.value = '';
//
Object.keys(formData).forEach(key => {
formData[key] = key === 'status' ? false : '';
if (key === 'type') formData[key] = '数据元';
});
dialogVisible.value = true;
};
//
const getList = () => {
console.log('分页查询:', queryParams);
//
const handleEdit = (row) => {
dialogTitle.value = '修改数据元';
currentId.value = row.id;
//
Object.keys(formData).forEach(key => {
formData[key] = row[key] !== undefined ? row[key] : '';
});
dialogVisible.value = true;
};
//
const handleDetail = (row) => {
router.push({
path: '/dataplan/standardDE/detail',
query: {
id: row.id
}
})
};
//
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(
'确定要删除这条数据吗?',
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
//
tableData.value = tableData.value.filter(item => item.id !== row.id);
total.value = tableData.value.length;
ElMessage.success('删除成功');
//
getList();
} catch (error) {
if (error !== 'cancel') {
console.error('删除数据失败:', error);
ElMessage.error('删除失败,请重试');
}
}
};
//
const handleSave = async () => {
try {
//
await formRef.value.validate();
if (currentId.value) {
//
const index = tableData.value.findIndex(item => item.id === currentId.value);
if (index !== -1) {
tableData.value[index] = { ...tableData.value[index], ...formData, id: currentId.value };
}
proxy.$message.success('更新成功');
} else {
//
const newId = Math.max(...tableData.value.map(item => parseInt(item.id)), 0) + 1;
tableData.value.unshift({
id: newId.toString(),
...formData
});
total.value = tableData.value.length;
proxy.$message.success('新增成功');
}
dialogVisible.value = false;
//
getList();
} catch (error) {
if (error.name !== 'Error') {
//
return;
}
console.error('保存数据失败:', error);
proxy.$message.error('保存失败,请重试');
}
};
//
const handleDialogClose = () => {
//
formRef.value.resetFields();
dialogVisible.value = false;
};
//
onMounted(() => {
loadMenuData().then(() => {
getList();
});
});
</script>
<style scoped>
@ -379,6 +814,7 @@ const getList = () => {
display: flex;
flex: 1;
overflow: hidden;
height: calc(100vh - 60px);
}
.sidebar {
@ -386,7 +822,6 @@ const getList = () => {
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
height: 100%;
}
/* 侧边栏头部搜索样式 */
@ -395,19 +830,20 @@ const getList = () => {
border-bottom: 1px solid #f0f0f0;
}
.sidebar-header {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
.sidebar-search {
width: 100%;
--el-input-height: 32px;
background-color: #f5f7fa;
border-color: #e5e7eb;
}
.sidebar-search :deep(.el-input__wrapper) {
background-color: #f7f8fa;
.sidebar-search .el-input__wrapper {
box-shadow: none;
border-radius: 4px;
}
.sidebar-search :deep(.el-input__inner) {
color: #1D2129;
font-size: 14px;
}
/* 菜单样式调整 */
@ -438,12 +874,12 @@ const getList = () => {
/* 一级菜单 */
.quality-menu :deep(.el-sub-menu:first-child .el-sub-menu__title) {
font-weight: 500;
padding-left: 16px !important;
padding-left: 22px !important;
}
/* 二级菜单 - 有子菜单的 */
.quality-menu :deep(.el-sub-menu .el-sub-menu__title) {
padding-left: 42px !important;
padding-left: 22px !important;
font-size: 14px;
color: #4E5969;
}
@ -455,7 +891,7 @@ const getList = () => {
/* 三级菜单项 */
.quality-menu :deep(.el-menu-item) {
padding-left: 70px !important;
padding-left: 47px !important;
font-size: 14px;
color: #4E5969;
}
@ -473,11 +909,12 @@ const getList = () => {
background-color: #f7f8fa;
}
/* 搜索区域样式 */
.search-container {
margin-bottom: 20px;
padding: 15px;
background-color: #ffffff;
border-radius: 4px;
margin-bottom: 15px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
@ -485,14 +922,73 @@ const getList = () => {
width: 100%;
}
.table-container {
background-color: #fff;
border-radius: 4px;
padding: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
.search-select {
width: 100%;
}
.search-btn {
width: 100%;
}
.reset-btn {
width: 100%;
}
/* 表格区域样式 */
.table-container {
border-radius: 4px;
background-color: #ffffff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
overflow: hidden;
padding: 15px;
}
/* 表格样式 */
:deep(.el-table) {
border-top: none !important;
margin-top: 15px;
}
:deep(.el-table th) {
background-color: #f2f3f5 !important;
color: #6b7785 !important;
font-weight: 500 !important;
}
:deep(.el-table tr:hover > td) {
background-color: #f7f8fa !important;
}
/* 操作按钮样式 */
.operation-btn {
padding: 0 5px;
margin: 0 2px;
}
.edit-btn {
color: #165DFF;
}
.delete-btn {
color: #ff4d4f;
}
.detail-btn {
color: #00b42a;
}
/* 表单样式 */
.data-form {
margin-top: 10px;
}
.data-form .el-form-item {
margin-bottom: 15px;
}
:deep(.el-radio-group) {
display: flex;
gap: 15px;
margin-top: 5px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,276 @@
<template>
<el-card style="min-height: 100px">
<template #header>
<div class="card-header" @click="goReturn">
<el-icon>
<ArrowLeft/>
</el-icon>
<span>返回</span>
</div>
</template>
<el-row :gutter="20" class="toolbar">
<el-col :span="4">数据集名称{{ form.datasetName }}</el-col>
<el-col :span="4">数据集类型
{{
getDictLabel(`llm_dataset_category_${form.datasetType}`, form.datasetCategory) || '无'
}}
</el-col>
<el-col :span="2">标注进度</el-col>
<el-col :span="4">
<el-progress :percentage="form.annotateProgress"/>
</el-col>
<el-col :span="5">
<Pagination
:total="total"
size="small"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
layout="prev, pager, next"
style="float: none"
@pagination="getQuestionList"
/>
</el-col>
<el-col :span="3">
<el-button type="primary" @click="save" :loading="loading">保存标注</el-button>
</el-col>
</el-row>
<el-table
:data="questionList"
style="width: 100%; margin-top: 10px; margin-bottom: 10px"
:header-cell-style="{
backgroundColor: '#F7F7F9',
color: '#84868C',
fontSize: '14px',
textAlign: 'center'
}"
>
<el-table-column align="center" label="序号" type="index" width="80"/>
<el-table-column prop="question" label="问题" width="300" align="center"/>
<el-table-column
prop="datasetAnswerRespVO" label="回答" align="center"
v-if="route.query.datasetType == '1'">
<template #default="scope">
<div>
<div style="padding: 10px">
<el-row
v-for="(item, index) in scope.row.datasetAnswerRespVO"
:key="index"
style="margin-bottom: 10px"
>
<el-col :span="4">回答{{ index + 1 }}</el-col>
<el-col :span="18">
<el-input placeholder="请输入回答内容" v-model="item.answer" type="textarea"/>
</el-col>
</el-row>
</div>
<div>
<el-button
text
bg
size="small"
@click="addAnswer(scope.row, scope.$index)"
>
<Icon icon="ep:plus"/>
</el-button>
<el-button
text
bg
size="small"
@click="deleteAnswer(scope.row, scope.$index)"
>
<Icon icon="ep:semi-select"/>
</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column
prop="answer" label="回答" align="center"
v-if="route.query.datasetType == '2'">
<template #default="scope">
<el-input v-model="scope.row.answer" placeholder="请输入"/>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import {ArrowLeft} from '@element-plus/icons-vue'
import {getDeta, getQuestionPageList, updateQuestion} from '@/api/dataService/dataset'
import {getDictLabel} from "@/utils/dict";
const router = useRouter() //
const route = useRoute() //
const message = useMessage() //
const title = ref('返回')
const questionList = ref<any>([])
const loading = ref(false) // Add loading state
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
const total = ref(0) //
const form = ref<any>({
datasetName: '',
datasetCategory: ''
})
//
const addAnswer = (row: any, index: number) => {
// datasetAnswerRespVO null []
if (!questionList.value[index].datasetAnswerRespVO) {
questionList.value[index].datasetAnswerRespVO = [];
}
questionList.value[index].datasetAnswerRespVO.push({
answer: '',
datasetId: row.datasetId,
datasetFilesId: row.datasetFilesId,
questionId: row.id
})
}
const deleteAnswer = (row: any, index: any)=> {
if (row.datasetAnswerRespVO.length == 1) {
ElMessage.error('至少保留一个选项')
return
}
let returnData = [...row.datasetAnswerRespVO]
returnData.pop()
questionList.value[index].datasetAnswerRespVO = returnData
}
const getList = async () => {
const id: number = route.query?.id
let res = await getDeta(id)
form.value = res
title.value = res.datasetType == 1 ? '训练数据集详情' : '评估数据集详情'
await getQuestionList()
}
const getQuestionList = async () => {
let res: any = await getQuestionPageList({
datasetId: route.query?.id,
...queryParams
})
questionList.value = res.list.map((e: any) => {
return {
...e,
answer: e.datasetAnswerRespVO?.[0]?.answer ?? '' // datasetAnswerRespVO
}
})
console.log(questionList.value, 'questionList.value')
total.value = res.total
}
//
const goReturn = () => {
// 1
const datasetType = route.query?.datasetType || '1'
// 1
const datasetParentType = route.query?.datasetParentType || '1'
router.push({
path: `/dataService/dataset/manage`,
query: {
activeTab: datasetType, // tab
parentType: datasetParentType //
}
})
}
const save = async () => {
if (loading.value) return
loading.value = true
try {
let hasAnswer = false;
for (const question of questionList.value) {
if (route.query.datasetType == '1') {
if (question.datasetAnswerRespVO && question.datasetAnswerRespVO.length > 0 && question.datasetAnswerRespVO.some(answer => answer.answer)) {
hasAnswer = true;
break;
}
} else if (route.query.datasetType == '2') {
if (question.answer) {
hasAnswer = true;
break;
}
}
}
if (!hasAnswer) {
message.error('至少有一个问题必须有回答!');
return;
}
for (const question of questionList.value) {
if (route.query.datasetType == '1') {
if (question.datasetAnswerRespVO && question.datasetAnswerRespVO.length > 0 && question.datasetAnswerRespVO.some(answer => !answer.answer)) {
message.error('请全部回答完毕!');
return;
}
} else if (route.query.datasetType == '2') {
if (!question.answer) {
message.error('请全部回答完毕!');
return;
}
}
}
const data = [...questionList.value]
if (route.query.datasetType == '2') {
data.forEach((e: any) => {
if (e.datasetAnswerRespVO[0] && e.datasetAnswerRespVO[0].id) {
e.datasetAnswerRespVO[0].answer = e.answer
} else {
e.datasetAnswerRespVO = [
{
answer: e.answer,
datasetId: e.datasetId,
datasetFilesId: e.datasetFilesId,
questionId: e.id
}
]
}
})
}
await updateQuestion(route.query.datasetType == '1' ? questionList.value : data)
await getList()
message.success('标注保存成功!')
} catch (e) {
message.error('标注保存失败!')
}finally {
loading.value = false
}
}
getList()
</script>
<style lang="scss" scoped>
.divLine {
margin-bottom: 10px;
}
.card-header {
display: flex;
align-items: center;
height: 30px;
.el-icon {
cursor: pointer;
}
}
.toolbar {
height: 40px;
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,581 @@
<template>
<div class="data-import-container">
<!-- 标签页 -->
<el-tabs v-model="activeTab" class="import-tabs">
<!-- 上传文件标签页 -->
<el-tab-pane label="上传文件" name="file">
<div class="upload-content">
<el-upload
ref="uploadRef"
:accept="accept"
v-model:file-list="fileList"
:action="importUrl + '?updateSupport=' + updateSupport"
:auto-upload="false"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
:on-remove="handleRemove"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center" style="width: 100%; max-width: 510px; margin: 0 auto;">
<span>{{ accept.includes('.zip') && accept === '.zip' ? '仅支持zip格式压缩包文件上传' : '支持json、csv、xlsx、txt格式文件及包含上述文件类型的tar.gz/zip压缩包文件上传' }}</span>
<template v-if="accept !== '.zip'">
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline;margin-right: 10px"
type="primary"
@click="importTemplate(4)"
>
下载json示例数据集
</el-link>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline;margin-right: 10px"
type="primary"
@click="importTemplate(2)"
>
下载xlsx示例数据集
</el-link>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline;margin-right: 10px"
type="primary"
@click="importTemplate(3)"
>
下载csv示例数据集
</el-link>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate(1)"
>
下载txt示例数据集
</el-link>
</template>
<template v-else>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline;margin-right: 10px"
type="primary"
@click="importTemplate(5)"
>
下载zip示例数据集
</el-link>
</template>
</div>
</template>
</el-upload>
</div>
</el-tab-pane>
<!-- 文件夹标签页 -->
<el-tab-pane label="文件夹" name="folder">
<div class="upload-content folder-content">
<div class="folder-upload-area">
<Icon icon="ep:folder" class="folder-icon" />
<p>拖拽文件夹到此处或点击选择文件夹按钮</p>
<el-button type="primary" @click="selectFolder">选择文件夹</el-button>
<input
ref="folderInputRef"
type="file"
webkitdirectory
directory
style="display: none"
@change="handleFolderSelect"
/>
<p class="support-tip">支持上传整个文件夹及其子文件</p>
</div>
</div>
</el-tab-pane>
<!-- URL爬取数据标签页 -->
<el-tab-pane label="URL爬取数据" name="url">
<div class="upload-content form-content">
<el-form label-width="80px">
<el-form-item label="URL地址" required>
<el-input v-model="urlForm.url" placeholder="请输入合法地址" />
</el-form-item>
<el-form-item label="Prompt">
<div class="prompt-input-wrapper">
<el-input v-model="urlForm.prompt" placeholder="请输入Prompt内容" />
<el-button type="primary" @click="addPrompt">添加Prompt</el-button>
</div>
</el-form-item>
<p class="hint-text">提示请输入想要爬取的内容名称</p>
</el-form>
</div>
</el-tab-pane>
<!-- API获取标签页 -->
<el-tab-pane label="API获取" name="api">
<div class="upload-content form-content">
<el-form label-width="80px">
<el-form-item label="API地址" required>
<el-input v-model="apiForm.url" placeholder="请输入合法地址" />
</el-form-item>
<el-form-item label="请求方式">
<el-select v-model="apiForm.method">
<el-option label="GET" value="GET" />
<el-option label="POST" value="POST" />
<el-option label="PUT" value="PUT" />
<el-option label="DELETE" value="DELETE" />
</el-select>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import * as dataSetApi from '@/api/dataControl'
import { getAccessToken, getTenantId } from '@/utils/auth'
import download from '@/utils/download'
import { ref, nextTick } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
defineOptions({ name: 'SystemUserImportForm' })
const props = defineProps({
accept: {
type: String,
default: '.json,.csv,.xlsx,.txt,.zip,.gz'
},
query: {
type: Object,
default: () => ({})
}
})
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const uploadRef = ref()
const importUrl =
import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/llmUpload'
const uploadHeaders = ref() // Header
const fileList = ref<any[]>([]) //
const updateSupport = ref(0) //
const activeTab = ref('file') //
//
const folderInputRef = ref()
const selectedFolder = ref<FileList | null>(null)
// URL
const urlForm = ref({
url: '',
prompt: ''
})
// API
const apiForm = ref({
url: '',
method: 'GET'
})
/** 重置表单 */
const resetForm = async (): Promise<void> => {
//
formLoading.value = false
fileList.value = []
selectedFolder.value = null
urlForm.value = {
url: '',
prompt: ''
}
apiForm.value = {
url: '',
method: 'GET'
}
await nextTick()
uploadRef.value?.clearFiles()
}
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
updateSupport.value = 0
fileList.value = []
resetForm()
// 使 props.query
console.log('导入参数:', props.query)
}
/** 提交表单 */
const submitForm = async () => {
try {
formLoading.value = true
switch (activeTab.value) {
case 'file':
await submitFileUpload()
break
case 'folder':
await submitFolderUpload()
break
case 'url':
await submitUrlCrawl()
break
case 'api':
await submitApiFetch()
break
}
} catch (error) {
message.error('操作失败,请重试')
formLoading.value = false
}
}
/** 提交文件上传 */
const submitFileUpload = async () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
formLoading.value = false
return
}
const fileItem = fileList.value[0];
console.log('========>', fileItem)
if (!fileItem || !fileItem.raw) {
message.error('文件上传异常,请重新选择文件');
formLoading.value = false
return;
}
const file = fileItem.raw;
const maxSize = 256 * 1024 * 1024 // 256MB
if (file.size > maxSize) {
message.error('文件大小不能超过256MB')
formLoading.value = false
return
}
//
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
uploadRef.value!.submit()
}
/** 选择文件夹 */
const selectFolder = () => {
folderInputRef.value?.click()
}
/** 处理文件夹选择 */
const handleFolderSelect = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files && target.files.length > 0) {
selectedFolder.value = target.files
message.success(`已选择文件夹,包含 ${target.files.length} 个文件`)
}
}
/** 提交文件夹上传 */
const submitFolderUpload = async () => {
if (!selectedFolder.value) {
message.error('请选择文件夹')
formLoading.value = false
return
}
//
message.success('文件夹上传成功')
emits('success', { folder: selectedFolder.value })
formLoading.value = false
resetForm()
}
/** 提交URL爬取 */
const submitUrlCrawl = async () => {
if (!urlForm.value.url) {
message.error('请输入URL地址')
formLoading.value = false
return
}
// URL
message.success('URL爬取数据成功')
emits('success', { url: urlForm.value })
formLoading.value = false
resetForm()
}
/** 添加Prompt */
const addPrompt = () => {
if (!urlForm.value.prompt) {
message.warning('请输入Prompt内容')
return
}
message.success('Prompt添加成功')
}
/** 提交API获取 */
const submitApiFetch = async () => {
if (!apiForm.value.url) {
message.error('请输入API地址')
formLoading.value = false
return
}
// API
message.success('API数据获取成功')
emits('success', { api: apiForm.value })
formLoading.value = false
resetForm()
}
defineExpose({ open, submitForm, fileList, resetForm }) // open resetForm
/** 文件上传成功 */
const emits = defineEmits(['success', 'removeFile'])
const submitFormSuccess = (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
//
const data = response.data
console.log(data,'data')
formLoading.value = false
dialogVisible.value = false
//
emits('success', data)
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
const handleRemove = () =>{
resetForm()
emits('removeFile')
}
/** 下载模板操作 */
// const base64DecodeUnicode = (str) => {
// // Base64
// const binaryStr = atob(str);
// const len = binaryStr.length;
// const bytes = new Uint8Array(len);
// for (let i = 0; i < len; i++) {
// bytes[i] = binaryStr.charCodeAt(i);
// }
// // 使 TextDecoder UTF-8
// const decoder = new TextDecoder('utf-8');
// return decoder.decode(bytes);
// }
const importTemplate = async (type) => {
const data = await dataSetApi.importUserTemplates({type:type})
if(type === 1){
download.txt(data, '示例数据集.txt')
}else if(type === 2){
download.excel(data, '示例数据集.xlsx')
}else if(type === 3){
download.csv(data, '示例数据集.csv')
}else if(type === 4){
download.json(data, '示例数据集.json')
}else if(type === 5){
download.zip(data, '示例数据集.zip')
}
}
open()
</script>
<style scoped>
/* 容器样式 */
.data-import-container {
padding: 0;
width: 100%;
}
/* 标签页样式 */
.import-tabs {
margin: 0;
}
/* 统一所有上传内容区域的容器样式 */
.upload-content {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
}
/* 核心样式 - 统一所有内容区域的虚线框样式 */
.upload-content :deep(.el-upload-dragger),
.folder-upload-area,
.form-content :deep(.el-form) {
/* 基础样式 */
width: 100%;
padding: 50px 20px;
box-sizing: border-box;
border: 1px dashed #dcdfe6;
border-radius: 4px;
background-color: #ffffff;
transition: border-color 0.3s;
min-height: 240px;
}
/* 鼠标悬停效果 - 统一虚线框颜色变化 */
.upload-content :deep(.el-upload-dragger:hover),
.folder-upload-area:hover,
.form-content :deep(.el-form:hover) {
border-color: #409eff;
}
/* 文件上传区域特殊样式 */
.upload-content :deep(.el-upload-dragger) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* 文件上传提示文字样式 */
.upload-content :deep(.el-upload__text) {
color: #606266;
margin-top: 10px;
}
.upload-content :deep(.el-upload__text em) {
color: #409eff;
}
/* 文件上传提示信息容器 */
.upload-content :deep(.el-upload__tip) {
width: 100%;
text-align: center;
margin-top: 15px;
}
/* 文件上传和文件夹上传区域容器样式 */
.upload-content,
.folder-content {
width: 500px;
margin: 0 auto;
padding: 0;
}
/* 文件夹上传区域样式 */
.folder-upload-area {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
margin: 0 auto;
}
/* 文件夹图标样式 */
.folder-icon {
font-size: 60px;
color: #409eff;
margin-bottom: 16px;
}
/* 文件夹文字样式 */
.folder-upload-area p {
margin: 8px 0;
color: #606266;
}
/* 文件夹选择按钮样式 */
.folder-upload-area .el-button {
margin: 10px 0;
}
/* 表单内容区域样式 */
.form-content {
padding: 0;
width: 500px;
margin: 0 auto;
}
/* 表单容器样式 */
.form-content :deep(.el-form) {
display: flex;
flex-direction: column;
justify-content: center;
}
/* 表单项样式 - 统一宽度和间距 */
.form-content :deep(.el-form-item) {
width: 100%;
margin-bottom: 20px;
}
/* 表单标签样式 */
.form-content :deep(.el-form-item__label) {
width: 80px;
padding-right: 10px;
}
/* 表单内容区域样式 */
.form-content :deep(.el-form-item__content) {
flex: 1;
}
/* 输入框和选择器统一样式 */
.form-content :deep(.el-input),
.form-content :deep(.el-select) {
width: 100%;
}
/* Prompt输入框和按钮组合样式 */
.form-content :deep(.el-form-item) .prompt-input-wrapper {
display: flex;
align-items: center;
width: 100%;
}
.form-content :deep(.el-form-item) .prompt-input-wrapper .el-input {
flex: 1;
margin-right: 10px;
}
/* 提示文本样式 - 统一所有标签页的提示文字 */
.support-tip,
.hint-text {
margin-top: 15px;
color: #909399;
font-size: 12px;
text-align: center;
}
/* 必填项标记样式 */
:deep(.el-form-item__label.is-required:not(.is-no-asterisk):before) {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
/* 确保所有标签页内容居中且宽度一致 */
.el-tab-pane {
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -0,0 +1,213 @@
<template>
<el-card shadow="never" class="b-r-top-15" style="margin-bottom: 10px">
<div class="card-header">
<el-icon @click="goBack"><ArrowLeft /></el-icon>
<span>{{ title }}</span>
</div>
</el-card>
<el-card shadow="never" class="b-r-bottom-15">
<div style="height: 70vh">
<div style="margin: 20px">
<el-row>
<el-col :span="6"> 数据集名称{{ form.datasetName }}</el-col>
<el-col :span="6"> 数据集类型{{ getDictLabel(`llm_dataset_category_${form.datasetType}`, form.datasetCategory) || '无' }}</el-col>
<el-col :span="6"> 清洗规则</el-col>
</el-row>
</div>
<!-- 当datasetParentType为2时显示新的表格结构 -->
<el-table v-if="datasetParentType === '2'" :data="questionList" show-header=true :header-cell-style="{ background: '#f2f3f5', color: '#6b7785', fontWeight: 'bold' }">
<el-table-column label="序号">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="图片">
<template #default="scope">
<!-- 显示imagesList中的第一张图片 -->
<img
v-if="Array.isArray(scope.row.imagesList) && scope.row.imagesList[0]"
:src="scope.row.imagesList[0]"
alt="图片"
style="max-width: 100px; max-height: 100px; border-radius: 4px;"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="问题">
<template #default="scope">
<el-popover
:content="scope.row.question || ''"
placement="top-start"
width="300"
trigger="hover"
>
<template #reference>
{{ (scope.row.question?.length || 0) > 49 ? (scope.row.question || '').substring(0,49)+'...' : (scope.row.question || '-') }}
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="回答">
<template #default="scope">
<!-- 遍历datasetAnswerRespVO数组显示所有回答 -->
<div
:class="`${index === (scope.row.datasetAnswerRespVO?.length || 1) - 1 ? 'el-table-column-children' : 'el-table-column-children el-table-column-children-border'}`"
v-for="(item, index) in (Array.isArray(scope.row.datasetAnswerRespVO) ? scope.row.datasetAnswerRespVO : [])"
:key="index">
<el-popover
:content="item.answer || ''"
placement="top-start"
:width="300"
trigger="hover"
>
<template #reference>
{{ (item.answer?.length || 0) > 18 ? (item.answer || '').substring(0, 18) + '...' : (item.answer || '-') }}
</template>
</el-popover>
</div>
</template>
</el-table-column>
</el-table>
<!-- 默认显示原有的表格结构 -->
<el-table v-else :data="questionList" show-header=true :header-cell-style="{ background: '#f2f3f5', color: '#6b7785', fontWeight: 'bold' }">
<el-table-column label="序号" prop="id">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="system" label="System"/>
<el-table-column prop="question" label="Prompt">
<template #default="scope">
<el-popover
:content="scope.row.question"
placement="top-start"
width="300"
trigger="hover"
>
<template #reference>
{{ scope.row.question.length>49 ? scope.row.question.substring(0,49)+'...' : scope.row.question }}
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column prop="response" label="Response">
<template #default="scope">
<div
:class="`${index === scope.row.datasetAnswerRespVO.length - 1 ? 'el-table-column-children' : 'el-table-column-children el-table-column-children-border'}`"
v-for="(item, index) in scope.row.datasetAnswerRespVO"
:key="index">
<el-popover
:content="item.answer"
placement="top-start"
:width="300"
trigger="hover"
>
<template #reference>
{{ item.answer.length>18 ? item.answer.substring(0,18)+'...' : item.answer }}
</template>
</el-popover>
</div>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 15px">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getQuestionList"
/>
</div>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { getDeta, getQuestionPageList } from '@/api/dataService/dataset'
import { ArrowLeft } from '@element-plus/icons-vue'
import { getDictLabel } from '@/utils/dict'
//
const router = useRouter() //
const route = useRoute() //
const title = ref('训练数据集详情')
// datasetParentType
const datasetParentType = computed(() => {
return route.query?.datasetParentType || '1'; // 1
})
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
const total = ref(0) //
const form = ref<any>({
datasetName: '',
datasetCategory: ''
})
const questionList = ref([])
//
const goBack = () => {
// 1
const datasetType = route.query?.datasetType || '1'
const datasetParentType = route.query?.datasetParentType || '1'
router.push({
path: `/dataService/dataset/manage`,
query: {
activeTab: datasetType, // tab
parentType: datasetParentType // /
}
})
}
const getList = async () => {
const id: number = Number(route.query?.id) || 0
let res = await getDeta(id)
form.value = res
title.value = res.datasetType == 1 ? '训练数据集详情' : '评估数据集详情'
await getQuestionList()
}
const getQuestionList = async () => {
let res: any = await getQuestionPageList({
datasetId: Number(route.query?.id) || 0,
datasetParentType: datasetParentType.value,
...queryParams
})
questionList.value = res.list
total.value = res.total
}
getList()
onMounted(() => {
})
</script>
<style lang="scss" scoped>
.divLine {
margin-bottom: 10px;
}
.card-header {
display: flex;
align-items: center;
height: 30px;
.el-icon {
cursor: pointer;
}
}
.el-table-column-children {
padding: 10px 0;
}
.el-table-column-children-border {
border-bottom: var(--el-table-border);
}
</style>

View File

@ -0,0 +1,408 @@
<template>
<el-card shadow="never" style="margin-bottom: 10px">
<el-form
v-loading="formLoading"
ref="formRef"
:inline="true"
:model="form"
class="-mb-15px"
label-width="auto"
:rules="formRules"
style="max-width: 1000px"
>
<el-form-item label="数据集名称" prop="datasetName" required>
<el-input
v-model="form.datasetName"
maxlength="20"
show-word-limit
class="!w-560px"
placeholder="请输入"
/>
</el-form-item>
<!-- 数据集类型 -->
<div v-if="!route?.query.id">
<el-form-item label="数据集类型" prop="datasetCategory">
<el-select v-model="form.datasetCategory" placeholder="请选择" style="width: 180px">
<el-option
v-for="dict in getIntDictOptions((form.datasetParentType === '2' ? 'vl_llm_dataset_category_' : 'llm_dataset_category_') + datasetType)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</div>
<!-- 标注状态 -->
<div>
<el-form-item label="标注状态" prop="markStatus" required v-if="datasetType!='2' && form.datasetParentType!='2'">
<el-select v-model="form.markStatus" placeholder="请选择" style="width: 180px">
<el-option
v-for="dict in getIntDictOptions('llm_dataset_mark_status')"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</div>
<!-- 清洗规则 -->
<div>
<el-form-item label="清洗规则" prop="cleanRule" required v-if="datasetType!='2' && form.datasetParentType!='2'">
<el-select v-model="form.cleanRule" placeholder="请选择" style="width: 180px">
<!-- 模拟两条清洗规则 -->
<el-option
v-for="rule in [
{value: '1', label: '基础数据清洗(去除空值、重复数据)'},
{value: '2', label: '高级数据标准化(格式化日期、统一单位)'}
]"
:key="rule.value"
:label="rule.label"
:value="rule.value"
/>
</el-select>
</el-form-item>
</div>
<el-form-item label="数据集描述" prop="datasetIntro" style="width: 100%; max-width: 600px;">
<el-input v-model="form.datasetIntro" placeholder="请输入" type="textarea" :rows="5" maxlength="200" show-word-limit/>
</el-form-item>
<el-form-item label="" required>
<div class="upload-container">
<DatasetImportForm @success="importSuccess" @remove-file="handleRemoveFile" ref="importFormRef" :accept="form.datasetParentType === '2' ? '.zip' : ''" />
</div>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" class="b-r-bottom-15" style="text-align: right; border: none;">
<el-button @click="handleCancel" class="no-border-btn">取消</el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm" class="no-border-btn"> 确认</el-button>
</el-card>
</template>
<script lang="ts" setup>
import {getIntDictOptions} from "@/utils/dict";
import {FormRules} from "element-plus";
import * as dataSetApi from '@/api/dataService/dataset'
import DatasetImportForm from "@/views/dataService/datasetManage/datasetImportForm.vue";
import { watch, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const props = defineProps({
query: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits(['create-success', 'close'])
const route = useRoute()
const router = useRouter()
//
const form = ref<any>({
datasetName: '', //
datasetCategory: '', //
markStatus: '', //
cleanRule: '', //
datasetIntro: '', //
datasetType: '', //
datasetParentType: '', //
type: '', // 0 1
filesList: []
})
const formRef = ref() //
const formLoading = ref(false) //
const importFormRef = ref() //
const isPopupMode = ref(false) //
const formRules = reactive<FormRules>({
datasetName: [{required: true, message: '数据集名称不能为空', trigger: 'blur'}],
markStatus: [{required: true, message: '状态不能为空', trigger: 'blur'}],
cleanRule: [{required: true, message: '请选择清洗规则', trigger: 'change'}],
datasetCategory: [{required: true, message: '请选择数据集类型', trigger: 'change'}],
})
//
const datasetType = ref('')
//
const getDetails = async (id) => {
let da = await dataSetApi.getDeta(id)
importFormRef.value.fileList = []
// datasetFiles forEach
const datasetFiles = da.datasetFiles;
if (datasetFiles && Array.isArray(datasetFiles)) {
for (const x of datasetFiles) {
importFormRef.value.fileList.push({
url: x.datasetFileUrl,
name: x.datasetFileName
});
}
}
console.log(importFormRef.value.fileList)
form.value = da
}
const submitForm = async () => {
await formRef.value.validate()
console.log('11111',importFormRef.value.fileList.length)
console.log(form.value)
if (importFormRef.value.fileList.length == 0 || (Array.isArray(form.value.filesList) && form.value.filesList.length > 0)) {
console.log(111)
await importSuccess()
} else {
console.log(2222)
await importFormRef.value.submitForm()
}
}
//
const handleRemoveFile = () => {
importFormRef.value.fileList = []
form.value.filesList = []
}
const importSuccess = async (data?: any) => {
if (data) {
form.value.filesList = [
{
id: data.id,
url: data.url,
filename: data.fileName
}
]
}
//
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
form.value.datasetType = datasetType.value || form.value.datasetType
form.value.datasetParentType = route.query.datasetParentType as string || form.value.datasetParentType
form.value.type = route.query.type || form.value.type
try {
const data = form.value as unknown as dataSetApi.DataSetVO
let result
if (!route?.query.id && !isPopupMode.value) {
// datasetParentType
if (form.value.datasetParentType === '2') {
result = await dataSetApi.createDataSetV2(data)
} else {
result = await dataSetApi.createDataSet(data)
}
// message.success(t('common.createSuccess'))
} else if (isPopupMode.value && !props.query.id) {
//
if (form.value.datasetParentType === '2') {
result = await dataSetApi.createDataSetV2(data)
} else {
result = await dataSetApi.createDataSet(data)
}
// message.success(t('common.createSuccess'))
} else {
//
if (form.value.datasetParentType === '2') {
result = await dataSetApi.updateDataSetV2(data)
} else {
result = await dataSetApi.updateDataSet(data)
}
// message.success(t('common.updateSuccess'))
}
if (isPopupMode.value) {
//
emits('create-success', result)
} else {
router.back()
}
} catch (error) {
// /
if (importFormRef.value) {
//
importFormRef.value.fileList = []
//
await importFormRef.value.resetForm()
}
//
form.value.filesList = []
//
throw error
} finally {
formLoading.value = false
}
}
//
const handleCancel = () => {
console.log('取消按钮点击,当前是否为弹窗模式:', isPopupMode.value);
if (isPopupMode.value) {
//
console.log('触发close事件');
emits('close');
} else {
//
router.back();
}
}
// -
const resetForm = () => {
//
const currentDatasetType = datasetType.value;
const currentParentType = form.value.datasetParentType;
const currentType = form.value.type;
//
//
form.value = {
datasetName: '',
datasetCategory: '',
markStatus: '',
cleanRule: '',
datasetIntro: '',
datasetType: currentDatasetType,
datasetParentType: currentParentType,
type: currentType,
filesList: []
}
//
if (formRef.value) {
formRef.value.resetFields();
}
//
if (importFormRef.value) {
importFormRef.value.fileList = [];
if (importFormRef.value.resetForm) {
importFormRef.value.resetForm();
}
}
}
defineExpose({ submitForm, resetForm })
// props
watch(() => props.query, (newQuery) => {
if (newQuery && Object.keys(newQuery).length > 0) {
isPopupMode.value = true
//
datasetType.value = newQuery?.datasetType as string || ''
form.value.datasetParentType = newQuery?.datasetParentType as string || ''
form.value.type = newQuery?.type || 0
//
resetForm();
// ID
if (newQuery?.id) {
getDetails(newQuery.id)
}
}
}, { immediate: true, deep: true })
onMounted(() => {
//
if (!isPopupMode.value) {
const id = route?.query.id
datasetType.value = route.query.datasetType as string
form.value.datasetParentType = route.query.datasetParentType as string
if (id) {
getDetails(id)
}
}
})
//
const initFormFromQuery = (query: any) => {
datasetType.value = query?.datasetType as string || ''
form.value = {
datasetName: '',
datasetCategory: '',
markStatus: '',
cleanRule: '',
datasetIntro: '',
datasetType: query?.datasetType || '',
datasetParentType: query?.datasetParentType || '',
type: query?.type || 0,
filesList: []
}
// ID
if (query?.id) {
getDetails(query.id)
}
}
watch(() => form.value.filesList, (newVal) => {
if (newVal?.length > 0) {
nextTick(() => {
formRef.value?.validateField('fildData')
})
}
}, { deep: true })
</script>
<style lang="scss" scoped>
.divLine {
margin-bottom: 10px;
}
/* 行内字段容器样式 */
.inline-fields-row {
display: flex;
flex-wrap: nowrap;
margin-bottom: 16px;
width: 100%;
}
/* 上传区域容器样式 */
.upload-container {
width: 100%;
max-width: 600px;
margin-left: 90px; /* 与数据集描述输入框对齐 */
}
/* 减小el-tabs__content的高度 */
.upload-container :deep(.el-tabs__content) {
overflow: hidden;
}
/* 确保上传区域高度减小 */
.upload-container :deep(.el-upload-dragger) {
height: 40px !important;
line-height: 40px !important;
}
/* 无边框按钮样式 */
.no-border-btn {
border: none !important;
}
/* 确保表单项样式 */
:deep(.el-form-item) {
margin-bottom: 16px;
margin-right: 0;
}
/* 调整表单项内容区域 */
:deep(.el-form-item__content) {
width: auto;
}
/* 响应式设计 */
@media (max-width: 968px) {
:deep(.el-select) {
width: 240px !important;
}
}
</style>

View File

@ -0,0 +1,637 @@
<template>
<!-- 列表 -->
<el-card shadow="never" class="b-r-15">
<div style="position: relative">
<!-- 数据集类型选择区域 -->
<!-- <div class="dataset-selection-area">
<div class="selection-row">
<span class="selection-label">数据集类型</span>
<el-radio-group v-model="datasetParentType" class="radio-group">
<el-radio-button :label="1">文本数据集</el-radio-button>
<el-radio-button :label="2">多模态数据集</el-radio-button>
</el-radio-group>
</div>
</div> -->
<!-- 数据集用途选项卡 -->
<el-tabs v-model="datasetType" class="demo-tabs">
<el-tab-pane label="训练数据集" :name="1">
<template v-if="datasetType === 1">
<dataSetTable :datasetType="1" :dataset-parent-type="datasetParentType" />
</template>
</el-tab-pane>
<el-tab-pane label="评估数据集" :name="2">
<template v-if="datasetType === 2">
<dataSetTable :datasetType="2" :dataset-parent-type="datasetParentType" />
</template>
</el-tab-pane>
</el-tabs>
</div>
<el-dialog v-model="dialogVisible" title="添加文档" style="width: 60%; height: 80%">
<el-form ref="queryFormRef" :inline="true" :model="dialogForm" class="-mb-15px" label-width="auto">
<div class="diaBox">
<el-steps :active="diaSteps" align-center>
<el-step title="选择文件" />
<el-step title="规则配置" />
</el-steps>
<div v-if="diaSteps == 1">
<div style="padding: 10px">
<el-upload
ref="uploadRef" v-model:file-list="fileList" :auto-upload="false" :disabled="formLoading"
:limit="1" :on-exceed="handleExceed" accept=".pdf,.txt,.doc,.ppt,.docx,.pptx,.epub,.md" action="none"
drag>
<h5 class="el-upload__text">点击或将文件拖拽到这里上传</h5>
<div>
文档格式支持[".pdf",".txt”,".doc",".ppt",".docx",".pptx",".epub,".md]请确保内容可复制文档格式30MB以内单次最多上传1个请确保文档内容可复制文档中的表格和图片暂时无法学习音频上传后会自动解析成文字存储并且学习内容可修改
</div>
</el-upload>
</div>
<div style="position: absolute; bottom: 10px; left: 45%">
<el-button type="primary" @click="next">下一步</el-button>
</div>
</div>
<div v-if="diaSteps == 2">
<div class="secondBox">
<el-row :gutter="20">
<el-col :span="5">
<div>
{{ diaForm.title }}
</div>
</el-col>
<el-col :span="11">
<div class="textareaClass">
<el-input v-model="diaForm.text" type="textarea" :rows="20" />
</div>
</el-col>
<el-col :span="8" style="border-left: 1px solid #eeeff3">
<div>
<el-radio-group v-model="diaForm.radio1">
<el-radio value="1" size="large" style="width: 100%">自动分段</el-radio>
会将上传的文档自动进行分段和去除敏感数据如个人信息url及敏感词
<el-radio value="2" size="large">自定义</el-radio>
自定义数据处理规则
</el-radio-group>
<div v-if="diaForm.radio1 == 2">
<el-form-item label="分段标识" prop="name" required>
<el-input v-model="diaForm.name" placeholder="请输入分段标识符" />
</el-form-item>
<el-form-item label="长度" prop="length" required>
<el-input v-model="diaForm.length" placeholder="请输入分段长度" />
</el-form-item>
<el-checkbox-group v-model="diaForm.checkList">
<el-checkbox label="对连续的空格、换行符和制表符进行去重" value="1" />
<el-checkbox label="删除所有URL和电子邮件地址" value="2" />
<el-checkbox label="删除敏感词" value="3" />
</el-checkbox-group>
</div>
</div>
</el-col>
</el-row>
</div>
<div style="position: absolute; bottom: 10px; left: 45%">
<el-row :gutter="20">
<el-col :span="12">
<el-button @click="goBack">上一步</el-button>
</el-col>
<el-col :span="12">
<el-button type="primary" @click="trim">完成</el-button>
</el-col>
</el-row>
</div>
</div>
</div>
</el-form>
</el-dialog>
<el-dialog v-model="knowDialogVisible" title="数据预览" style="width: 60%">
<el-form ref="configurationFormRef" :inline="true" :model="configurationForm" class="-mb-15px" label-width="auto">
<el-row>
<el-col>
<el-form-item label="文档内容" prop="text">
<el-input v-model="configurationForm.text" placeholder="请输入文档内容" />
</el-form-item>
<el-form-item label="使用状态" prop="stutas">
<el-select v-model="configurationForm.stutas" clearable placeholder="请选择使用状态" class="!w-240px">
<el-option v-for="item in statusList" :key="item.id" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-button type="primary">查询</el-button>
<el-button>重置</el-button>
</el-col>
</el-row>
<el-row>
<el-col>
<div class="box">
<div class="smallBox" v-for="(item, index) in boxList" :key="index">
<el-row>
<el-col :span="12">{{ item.title }}</el-col>
<el-col :span="12" style="text-align: right">
<Icon icon="ep:delete" />
</el-col>
</el-row>
<div>
{{ item.content }}
</div>
<el-row>
<el-col :span="12">
<el-switch v-model="item.switch" />
</el-col>
<el-col :span="12" style="text-align: right">
{{ item.num }}
</el-col>
</el-row>
</div>
</div>
</el-col>
</el-row>
<!-- 分页 -->
<div style="display: flex; justify-content: flex-end; margin: 10px">
<el-pagination
v-model:current-page="currentPage2" v-model:page-size="pageSize2"
:page-sizes="[100, 200, 300, 400]" :size="size" :disabled="disabled" :background="background"
layout="sizes, prev, pager, next" :total="1000" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</el-form>
</el-dialog>
<el-dialog v-model="testDialogVisible" title="命中测试" style="width: 60%">
<div class="textBox">
<div>
<el-input placeholder="输入文档中的内容进行命中测试" v-model="testForm.text" type="textarea" :rows="5" />
</div>
<el-row :gutter="20" style="margin-top: 10px">
<el-col :span="12">
<div style="margin: 10px 0">
<span style="font-weight: bold"> 测试记录 </span>
<span style="margin-left: 10px; color: #b1b7c0">仅展示最近15条</span>
</div>
<div>
<el-table
:header-cell-style="{ background: '#E7EBFF', color: '#606266' }" :data="testLefeData"
style="height: 200px; border: 1px solid #e5e6ec; border-radius: 10px">
<el-table-column prop="time" label="时间" />
<el-table-column prop="content" label="测试内容" />
<el-table-column prop="duan" label="命中段落" />
</el-table>
</div>
</el-col>
<el-col :span="12" style="border-left: 1px solid #eee">
<div style="width: 100%">
<img style="width: 100%" src="@/assets/imgs/1733561223884.jpg" alt="" />
</div>
</el-col>
</el-row>
<div style="display: flex; justify-content: center; margin-top: 10px">
<el-row :gutter="20">
<el-col :span="12">
<el-button @click="testDialogVisible = false">
<Icon icon="ep:circle-close" />
取消
</el-button>
</el-col>
<el-col :span="12">
<el-button @click="testDialogVisible = false" type="primary">
<Icon icon="ep:circle-check" />
测试
</el-button>
</el-col>
</el-row>
</div>
</div>
</el-dialog>
<el-dialog v-model="oaDialogVisible" :title="oaTitle" style="width: 60%">
<el-form ref="configurationFormRef" :inline="true" :model="oaForm" class="-mb-15px" label-width="auto">
<el-form-item label="问题" prop="problem" required style="width: 100%">
<el-input
v-model="oaForm.problem" style="width: 80%; margin-right: 10px" maxlength="100" show-word-limit
type="text" />
<el-button>
<Icon icon="ep:plus" />
</el-button>
<el-button>
<Icon icon="ep:close" />
</el-button>
</el-form-item>
<el-form-item label="答案" prop="answer" required style="width: 90%">
<el-input
v-model="oaForm.answer" style="width: 100%" maxlength="3000" show-word-limit :rows="10"
type="textarea" />
</el-form-item>
</el-form>
<div style="display: flex; justify-content: center; margin-top: 10px">
<el-row :gutter="20">
<el-col :span="12">
<el-button @click="oaDialogVisible = false">
<Icon icon="ep:circle-close" />
取消
</el-button>
</el-col>
<el-col :span="12">
<el-button @click="oaDialogVisible = false" type="primary">
<Icon icon="ep:circle-check" />
确认
</el-button>
</el-col>
</el-row>
</div>
</el-dialog>
</el-card>
</template>
<script lang="ts" setup>
import dataSetTable from './table.vue'
import { getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { ContentWrap } from '@/components/ContentWrap'
import { Icon } from '@/components/Icon'
import { ElMessageBox } from 'element-plus'
import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from '@/hooks/web/useI18n'
import { useRouter, useRoute } from 'vue-router'
//
const dataCategory = ref('text') //
const datasetParentType = ref(1) // 1: , 2:
//
const oaForm = ref({})
const selectable = (row) => ![1, 2].includes(row.id)
const wenSelectable = (row) => ![1, 2].includes(row.id)
const oaSelectable = (row) => ![1, 2].includes(row.id)
const message = useMessage() //
const { t } = useI18n() //
const router = useRouter() //
const route = useRoute() //
const knowDialogVisible = ref(false)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
datasetType: 1,
datasetName: undefined,
datasetCategory: undefined,
})
const datasetType = ref(1)
const oaTitle = ref('')
const oaDialogVisible = ref(false)
const testDialogVisible = ref(false)
const configurationForm = ref({
topK: 4,
Score: 0.6,
knowLength: 100
})
const tableData = ref([
{
name: '数据集名称1',
type: 'DPO',
size: '这是描述内容~这是描述内容~这是描述内容~',
length: '1000',
status: '进行中',
jindu: '6%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称2',
type: 'SFT',
size: '这是描述内容~这是描述内容~',
length: '2000',
status: '进行中',
jindu: '80%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称3',
type: 'DPO',
size: '这是描述内容~',
length: '5000',
status: '已完成',
jindu: '100%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称4',
type: 'SFT',
size: '这是描述内容~这是描述内容~',
length: '8000',
status: '已完成',
jindu: '100%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称5',
type: 'SFT',
size: '这是描述内容~',
length: '2000',
status: '已完成',
jindu: '100%',
time: '2024-03-29 12:30:00'
}
])
const tableData1 = ref([
{
name: '数据集名称1',
type: 'Only Query',
size: '这是描述内容~这是描述内容~这是描述内容~',
length: '1000',
status: '进行中',
jindu: '6%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称2',
type: 'QA对',
size: '这是描述内容~这是描述内容~',
length: '2000',
status: '进行中',
jindu: '80%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称3',
type: 'Only Query',
size: '这是描述内容~',
length: '5000',
status: '已完成',
jindu: '100%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称4',
type: 'QA对',
size: '这是描述内容~这是描述内容~',
length: '8000',
status: '已完成',
jindu: '100%',
time: '2024-03-29 12:30:00'
},
{
name: '数据集名称5',
type: 'Only Query',
size: '这是描述内容~',
length: '2000',
status: '已完成',
jindu: '100%',
time: '2024-03-29 12:30:00'
}
])
const queryFormRef = ref() //
const exportLoading = ref(false) //
const cardList = ref([
{
name: 'MES知识数据库',
title: 'MES知识数据库',
num: 2,
content: '存储mes业务功能框架以及影响的业务场景说明'
},
{
name: 'MES知识数据库',
title: 'MES知识数据库',
num: 2,
content: '存储mes业务功能框架以及影响的业务场景说明'
},
{
name: 'MES知识数据库',
title: 'MES知识数据库',
num: 2,
content: '存储mes业务功能框架以及影响的业务场景说明'
},
{
name: 'MES知识数据库',
title: 'MES知识数据库',
num: 2,
content: '存储mes业务功能框架以及影响的业务场景说明'
},
{
name: 'MES知识数据库',
title: 'MES知识数据库',
num: 2,
content: '存储mes业务功能框架以及影响的业务场景说明'
}
])
const OAtableData = ref([
{
id: '185663182054991362',
problem: 'ceshi',
answer: '11',
status: '未处理',
time: '2024-11-13 09:34:55'
},
{
id: '185634325432591362',
problem: '234',
answer: '234',
status: '未处理',
time: '2024-11-13 09:34:55'
},
{
id: '183245235234324324',
problem: '234',
answer: '234',
status: '未处理',
time: '2024-11-13 09:34:55'
}
])
const diaForm = ref({
title: '新建 文本文档(9)....',
radio1: '2',
length: 200,
checkList: ['3'],
text: '大模型训练平台 图生文 支持 9.28-9.30号 文生图支持 具体排期等产品出图 后端和算法已经开始对接预计10.16号完成 文生图 支持 具体排期等产品出图后端和算法已经开始对接 预计10.20号完成 大模型训练平台--键配置Ul需要等功能开发完毕 占用前后端 3-5天时间 当前进度:整体平台开发基础功能完成,管理端待测试 和上正式环境。多模态模型已经部署,差原型图,前后端算法联调。大模型平台迭代 热词处理 9.28-9.30 测试环境部署完 等测试有空再提到正式环境 手机号邮箱特殊识别 9.28-9.30 测试环境部署完 等测试有空再提到正式环境 RAG-迭代 更新向量模型以及rerank模块 10.12号完成RAG 知识检索问答 支持附带图片和视频功能 9.28刚对完方案和功能 具体排期等产品出图 预计10.18号完成 RAG 多轮知识问答 支持离线式对知识库进行处理 来实现机器人多轮知识问答 具体排期等产品出图 预计10.25号完成 RAG web支持爬取政府网站文件 需求未对 排期待定 大模型平台 对话 支持图片对话功能 原型出了 算法开发基本完毕 优先级在后面 预计10.31号完成 大模型平台 对话 支持文档/excel 对话功能 算法开发基本完毕 优先级在后面 排期'
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增知识库')
const dialogForm = ref({
radio1: 1,
length: 200,
checkList: '3'
})
const diaSteps = ref(1)
const testForm = ref({
text: ''
})
const testLefeData = ref([
{
time: '2014-11-14',
content: '555',
duan: '6'
},
{
time: '2024-11-14',
content: '555',
duan: '6'
}
])
const boxList = ref([
{
title: '1',
switch: true,
num: '202/300',
content:
'AI数字人培训方案北京捷通华声科技股份有限公司修订历史记录表日期版本说明作者2024-10-30V1.0第一版帅博目录1 培训目的 12 培训方式 13 培训课程说明 1..'
},
{
title: '2',
switch: true,
num: '202/300',
content:
'通过培训,最终招标方的技术人员能独立掌握设备的配置,故障诊断,维护管理等技术,使之能适应系统正常运行的需求。培训方式我司免费为招标方提供培训。…'
},
{
title: '3',
switch: true,
num: '202/300',
content:
'由我司对招标方技术人员进行基础培训,包括功能原理、平台使用、使用维护以及运行管理等内容。培训课程说明培训主要内容应包括功能原理、软件安装、配置.'
},
{
title: '4',
switch: true,
num: '202/300',
content:
'培训时间:产品运行期间,将根据用户时间安排,组织系统培训。培训地点:工程现场。主持人:项目经理,原厂培训讲师。培训对象:领导、最终用户、平台系.'
},
{
title: '5',
switch: true,
num: '202/300',
content:
'0.5天平台高级功能配置1天异常情况处理1天建立培训评价体系为确保培训的质量和效果可以通过培训的评价机制对培训效果进行评价。1.监督指导。培训组织'
}
])
const statusList = ref([
{
label: '开启',
value: true
},
{
label: '关闭',
value: false
}
])
const handleClick = (e) => {
datasetType.value = e
}
const next = () => {
diaSteps.value = 2
}
const goBack = () => {
diaSteps.value = 1
}
//
const trim = () => {
dialogVisible.value = false
}
//
const dataPreview = (e) => {
console.log(e, '数据预览')
knowDialogVisible.value = true
}
//
const addOA = () => {
oaTitle.value = '添加OA'
oaDialogVisible.value = true
}
// oa
const oaEdit = (e) => {
oaTitle.value = '编辑OA'
oaDialogVisible.value = true
}
// oa
const oaDelete = (e) => {
ElMessageBox.confirm('确定要删除当前OA吗', '', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
})
}
// tab
watchEffect(() => {
//
if (route.query?.activeTab) {
datasetType.value = parseInt(route.query.activeTab as string) || 1
}
// /
if (route.query?.parentType) {
datasetParentType.value = parseInt(route.query.parentType as string) || 1
}
})
// onMounted
onMounted(() => {
// URL activeTab 使 tab
if (route.query?.activeTab) {
datasetType.value = parseInt(route.query.activeTab as string) || 1
}
})
</script>
<style scoped>
/* 新增样式 - 匹配图片中的设计 */
.dataset-selection-area {
background: white;
padding: 15px 20px;
border-radius: 4px;
margin-bottom: 15px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.selection-row {
display: flex;
align-items: center;
}
.selection-label {
width: 100px;
font-weight: 500;
color: #606266;
flex-shrink: 0;
font-size: 14px;
}
.radio-group {
display: flex;
gap: 10px;
}
/* 原有样式保持不变 */
.content {
display: flex;
width: 100%;
height: 100%;
flex-wrap: wrap;
}
.content-box {
width: 400px;
}
.diaBox {
padding: 10px;
}
.secondBox {
height: 100%;
margin: 20px 10px;
}
.secondBox textarea::-webkit-scrollbar {
display: none;
}
.smallBox {
float: left;
width: 48%;
padding: 10px;
margin: 5px;
background-color: #f2f3f5;
border-radius: 10px;
}
</style>

View File

@ -0,0 +1,340 @@
<template>
<div class="dataset-table-container">
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="auto"
@submit.prevent
>
<el-form-item label="数据集名称" prop="datasetName">
<el-input
v-model="queryParams.datasetName"
class="!w-240px"
clearable
placeholder="请输入"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="数据集类型" prop="datasetCategory">
<el-select
v-model="queryParams.datasetCategory" class="!w-240px" clearable
placeholder="请选择">
<el-option
v-for="dict in getIntDictOptions('llm_dataset_category_'+props.datasetType)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item style="float: right">
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-divider/>
<div class="buttonBox">
<el-button type="primary" @click="goDataCreate">创建数据集</el-button>
<el-button type="primary" @click="setDataLabel" v-if="props.datasetType===1 && props.datasetParentType !== 2">数据标注</el-button>
</div>
<el-table
:data="list" v-loading="loading" highlight-current-row @current-change="handleCurrentChange" style="width: 100%"
show-header=true stripe :header-cell-style="{ background: '#f2f3f5', color: '#6b7785', fontWeight: 'bold' }">
<el-table-column width="50" align="center">
<template #default="scope">
<el-radio-group v-model="currentRow.id">
<el-radio :value="scope.row?.id" />
</el-radio-group>
</template>
</el-table-column>
<el-table-column prop="datasetName" label="数据集名称"/>
<el-table-column prop="datasetCategory" label="数据集类型">
<template #default="scope">
{{ getDictLabel('llm_dataset_category_' + props.datasetType, scope.row.datasetCategory) }}
</template>
</el-table-column>
<el-table-column prop="datasetIntro" label="数据集描述">
<template #default="scope">
<el-popover
placement="top"
trigger="hover"
width="300"
:content="scope.row.datasetIntro"
>
<template #reference>
{{ scope.row.datasetIntro && scope.row.datasetIntro.length>18 ? scope.row.datasetIntro.substring(0,18)+'...' : scope.row.datasetIntro }}
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column prop="dataLength" label="数据集长度"/>
<el-table-column prop="status" label="标注状态" v-if="datasetType == 1">
<template #default="scope">
<!-- <el-tag >{{ getDictLabel('llm_dataset_mark_status', scope.row.status) }}</el-tag>-->
<el-tag v-if="scope.row.markStatus == 0" type="primary">{{
getDictLabel('llm_dataset_mark_status', scope.row.markStatus)
}}</el-tag>
<el-tag v-if="scope.row.markStatus == 1" type="warning">{{
getDictLabel('llm_dataset_mark_status', scope.row.markStatus)
}}</el-tag>
<el-tag v-if="scope.row.markStatus == 2" type="success">{{
getDictLabel('llm_dataset_mark_status', scope.row.markStatus)
}}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="datasetType == 1" prop="annotateProgress" label="标注进度">
<template #default="scope">
{{ scope.row.annotateProgress }}%
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
/>
<el-table-column :width="300" align="center" label="操作">
<template #default="scope">
<el-button link type="primary" @click="ruleConfiguration(scope.row)"> 详情</el-button>
<el-button link type="primary" @click="goDataCreate(scope.row)" v-if="props.datasetParentType !== 2"> 修改</el-button>
<el-button link type="primary" @click="handleExport(scope.row)" v-if="props.datasetParentType !== 2">导出</el-button>
<el-button link type="danger" @click="detale(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 10px">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 数据集创建表单弹窗 -->
<el-dialog
v-model="datasetFormVisible"
title="创建数据集"
width="800px"
:before-close="handleCloseDialog"
:close-on-click-modal="false"
>
<dataset-form
ref="datasetFormRef"
:query="datasetFormQuery"
@create-success="handleDatasetCreateSuccess"
@close="handleCloseDialog"
/>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted, nextTick } from 'vue'
import {getDictLabel, getIntDictOptions} from "@/utils/dict"
import DatasetForm from './form.vue';
import {dateFormatter} from "@/utils/formatTime";
import * as dataSetApi from '@/api/dataService/dataset'
import download from "@/utils/download";
import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from '@/hooks/web/useI18n'
import { useRouter, useRoute } from 'vue-router'
const props = defineProps({
datasetType: Number,
datasetParentType: Number
});
const message = useMessage() //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
datasetType: props.datasetType,
datasetParentType: props.datasetParentType,
datasetName: undefined,
datasetCategory: undefined,
type: 0,
})
const {t} = useI18n() //
const router = useRouter() //
const {path} = useRoute()
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryFormRef = ref() //
const currentRow = ref<any>({
id: ''
})
const exportLoading = ref(false)
const datasetFormRef = ref() //
const datasetFormVisible = ref(false) //
const datasetFormQuery = ref({}) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await dataSetApi.getPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
// //
// //
// const ruleConfiguration = (e) => {
//
// }
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
const goDataCreate = (row?: any) => {
//
datasetFormQuery.value = {
id: row?.id,
datasetType: props.datasetType,
datasetParentType: props.datasetParentType,
type: queryParams.type
}
//
datasetFormVisible.value = true
}
const setDataLabel = () => {
if (currentRow.value.id) {
router.push({
path: '/dataService/dataset/mark',
query: {
id: currentRow.value.id,
type: queryParams.type,
datasetType: currentRow.value.datasetType,
datasetParentType: props.datasetParentType,
}
})
} else {
message.warning('请选择数据集!')
}
}
//
//
const ruleConfiguration = (row: any) => {
router.push({
path: '/dataService/dataset/detail',
query: {
id: row.id,
type: queryParams.type,
datasetType: props.datasetType,
datasetParentType: props.datasetParentType
}
})
}
const handleExport = async (row: any) => {
try {
// 100%
if (row.annotateProgress !== 100) {
message.warning('标注进度未达到100%,无法导出数据')
return
}
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await dataSetApi.exportData({
datasetId: row.id
})
download.excel(data, '数据集数据.xls')
} catch {
} finally {
exportLoading.value = false
}
}
//
const detale = async (e) => {
try {
//
await message.delConfirm()
//
await dataSetApi.deleteDataSet(e.id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {
}
}
/** 数据集创建成功回调 */
const handleDatasetCreateSuccess = () => {
message.success('数据集创建成功')
datasetFormVisible.value = false
//
getList()
}
/** 关闭弹窗前的回调 */
const handleCloseDialog = () => {
datasetFormVisible.value = false
}
const handleCurrentChange = (val: any) => {
if (val) {
currentRow.value = val
} else {
currentRow.value = {
id: ""
}
}
}
/** 监听datasetParentType变化 */
watch(() => props.datasetParentType, (newVal) => {
if (newVal !== undefined) {
queryParams.datasetParentType = newVal;
queryParams.pageNo = 1;
getList();
}
}, { immediate: false });
/** 初始化 */
onMounted(() => {
let pathList = path.split('-')
queryParams.type = Number(pathList[pathList.length - 1]) || 0
queryParams.datasetParentType = props.datasetParentType;
getList()
})
</script>
<style scoped lang="scss">
form{
margin-top: 20px
}
.buttonBox{
margin-bottom: 20px
}
</style>

View File

@ -144,7 +144,7 @@ const form = reactive<any>({
//
const getDataSet = async () => {
let list = await dataSetApi.getAll()
let list = await dataSetApi.getAllList()
list[0]['datasetName'] = '数据集'
list[1]['datasetName'] = '官方数据'
dataSetList.value = list