Files
YG_FT/frontend/src/views/governance/ResourceAclView.vue
wuyongtao 2adf78a6ed feat: 数据集下载鉴权、MinIO 发布与数据库兼容增强
- 数据集下载接口鉴权:单文件直接返回、多文件打包 ZIP,前端统一走请求客户端携带 Token
- 预检/同步支持 MinIO 对象补建与资源副本记录,兼容接入 MinIO 前的历史数据集
- create_dataset 增加名称重复校验,软删记录释放名称并保留墓碑
- 数据处理发布结果支持写入 MinIO storage_objects 并关联 dataset_file
- 数据存储根目录支持 YG_FT_DATA_ROOT 环境变量
- SQL 迁移兼容旧版 approval_steps / retention_policies / data_process_results
- 训练日志图表按曲线独立过滤缺失采样点

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 14:45:16 +08:00

379 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getDatasetList, type DatasetItem } from '@/api/modules/dataset'
import { getTrainedModels } from '@/api/modules/model'
import type { TrainedModel } from '@/types'
import { getUsers, type SystemUser } from '@/api/modules/system'
import { getResourceAcl, setResourceAcl, type AclEntry } from '@/api/modules/resource-acl'
const loading = ref(false)
const saving = ref(false)
// 资源类型选项
const resourceTypes = [
{ value: 'dataset', label: '数据集' },
{ value: 'trained_model', label: '训练产物(微调模型)' },
]
// 资源列表
const datasets = ref<DatasetItem[]>([])
const models = ref<TrainedModel[]>([])
const users = ref<SystemUser[]>([])
// 当前选择
const form = reactive({
resourceType: 'dataset',
resourceIds: [] as string[],
resourceNames: '' as string,
})
// ACL 编辑
const aclEntries = ref<AclEntry[]>([])
const showAddEntry = ref(false)
const newEntry = reactive({
principal_id: '',
permissions: ['read', 'execute'] as string[],
})
const permissionOptions = [
{ label: '查看 (read)', value: 'read' },
{ label: '编辑 (write)', value: 'write' },
{ label: '使用 (execute)', value: 'execute' },
{ label: '下载 (download)', value: 'download' },
{ label: '删除 (delete)', value: 'delete' },
{ label: '全部权限 (admin)', value: 'admin' },
]
// 加载所有数据
async function loadAll() {
loading.value = true
try {
const [dsRes, trainedRes, userRes] = await Promise.all([
getDatasetList().catch(() => []),
getTrainedModels().catch(() => ({ models: [] })),
getUsers().catch(() => []),
])
datasets.value = dsRes || []
models.value = trainedRes?.models || []
users.value = userRes || []
} finally {
loading.value = false
}
}
// 获取当前资源类型的列表
function currentResourceList(): Array<{ id: string; name: string }> {
if (form.resourceType === 'dataset') {
return datasets.value.map((d: DatasetItem) => ({ id: String(d.id), name: d.name || String(d.id) }))
}
if (form.resourceType === 'trained_model') {
return models.value.map((m: TrainedModel) => ({ id: String(m.id || m.name || ''), name: m.name || String(m.id) || '' }))
}
return []
}
// 获取当前资源的名称
function getResourceName(id: string) {
return currentResourceList().find((r) => r.id === id)?.name || id
}
// 选择资源后加载 ACL
async function onResourceChange() {
if (!form.resourceIds || form.resourceIds.length === 0) {
aclEntries.value = []
form.resourceNames = ''
return
}
// 显示选中的资源名称
const names = form.resourceIds.map((id) => getResourceName(id))
form.resourceNames = names.join('、')
// 加载第一个资源的 ACL 作为初始值(多个资源的 ACL 合并显示)
try {
const res = await getResourceAcl(form.resourceType, form.resourceIds[0])
if (Array.isArray(res)) {
aclEntries.value = res.map((e: any) => ({
...e,
principal_type: e.principal_type || 'user',
permissions: Array.isArray(e.permissions) ? e.permissions : [],
}))
} else if (res && Array.isArray((res as any).entries)) {
aclEntries.value = (res as any).entries.map((e: any) => ({
...e,
principal_type: e.principal_type || 'user',
permissions: Array.isArray(e.permissions) ? e.permissions : [],
}))
} else {
aclEntries.value = []
}
} catch (e) {
console.error('加载 ACL 失败:', e)
aclEntries.value = []
}
}
// 切换资源类型时重置
function onTypeChange() {
form.resourceIds = []
form.resourceNames = ''
aclEntries.value = []
}
// 添加授权条目
function addEntry() {
const trimmedId = (newEntry.principal_id || '').trim()
if (!trimmedId) {
ElMessage.warning('请选择要授权的用户')
return
}
if (!newEntry.permissions || newEntry.permissions.length === 0) {
ElMessage.warning('请至少选择一个权限')
return
}
const exists = aclEntries.value.some((e) => e.principal_id === trimmedId)
if (exists) {
ElMessage.warning('该用户已存在,请先删除再重新添加')
return
}
aclEntries.value.push({
principal_type: 'user',
principal_id: trimmedId,
permissions: [...newEntry.permissions],
})
showAddEntry.value = false
newEntry.principal_id = ''
newEntry.permissions = ['read', 'execute']
}
// 删除授权条目
function removeEntry(index: number) {
aclEntries.value.splice(index, 1)
}
// 保存 ACL
async function saveAcl() {
if (!form.resourceIds || form.resourceIds.length === 0) {
ElMessage.warning('请先选择资源')
return
}
if (aclEntries.value.length === 0) {
ElMessage.warning('请至少添加一个授权用户')
return
}
saving.value = true
try {
// 为每个选中的资源保存相同的 ACL 授权
for (const resourceId of form.resourceIds) {
await setResourceAcl(form.resourceType, resourceId, aclEntries.value)
}
ElMessage.success(`已为 ${form.resourceIds.length} 个资源保存授权配置`)
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
saving.value = false
}
}
// 显示名称
function principalName(entry: unknown) {
const aclEntry = entry as AclEntry
const user = users.value.find((u) => u.id === aclEntry.principal_id)
return user ? `${user.display_name || user.username}` : aclEntry.principal_id
}
onMounted(loadAll)
</script>
<template>
<div class="page" v-loading="loading">
<h2 class="page-title">资源授权管理</h2>
<p class="page-desc">将数据集或微调模型授权给指定用户使用被授权的用户可以在自己的页面看到并使用该资源</p>
<!-- 步骤1选择资源 -->
<el-card class="section-card">
<template #header>
<span> 选择要授权的资源</span>
</template>
<el-form :inline="true" label-width="90px">
<el-form-item label="资源类型">
<el-select v-model="form.resourceType" @change="onTypeChange" style="width: 200px">
<el-option v-for="t in resourceTypes" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<el-form-item label="选择资源">
<el-select
v-model="form.resourceIds"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="搜索并选择多个资源..."
style="width: 400px"
@change="onResourceChange"
>
<el-option
v-for="r in currentResourceList()"
:key="r.id"
:label="`${r.name} (${r.id.slice(0, 8)}...)`"
:value="r.id"
/>
</el-select>
</el-form-item>
</el-form>
</el-card>
<!-- 步骤2当前授权列表 + 添加授权 -->
<el-card class="section-card" v-if="form.resourceIds.length > 0">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span> 管理授权 {{ form.resourceNames }}</span>
<el-button type="primary" size="small" @click="showAddEntry = true">+ 添加授权</el-button>
</div>
</template>
<!-- 已有授权列表 -->
<el-table :data="aclEntries" border empty-text="暂无授权点击上方按钮添加" style="width: 100%">
<el-table-column label="用户">
<template #default="{ row }">{{ principalName(row) }}</template>
</el-table-column>
<el-table-column label="授权资源" min-width="200">
<template #default>
<el-tag v-for="(id, idx) in form.resourceIds" :key="id" size="small" style="margin: 2px">
{{ getResourceName(id) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="权限">
<template #default="{ row }">
<el-space wrap>
<el-tag
v-for="p in row.permissions"
:key="p"
size="small"
:type="p === 'admin' ? 'danger' : p === 'delete' || p === 'write' ? 'warning' : undefined"
>{{ p }}</el-tag>
</el-space>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button type="danger" size="small" link @click="removeEntry($index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加授权表单 -->
<el-divider v-if="showAddEntry" content-position="left">添加新授权</el-divider>
<div v-if="showAddEntry" class="add-entry-form">
<el-form :inline="true" label-width="80px">
<el-form-item label="选择用户">
<el-select
v-model="newEntry.principal_id"
filterable
placeholder="选择要授权的用户"
style="width: 240px"
>
<el-option
v-for="u in users"
:key="u.id"
:label="`${u.display_name || u.username} (${u.username})`"
:value="u.id"
/>
</el-select>
</el-form-item>
<el-form-item label="权限">
<el-checkbox-group v-model="newEntry.permissions">
<el-checkbox v-for="p in permissionOptions" :key="p.value" :value="p.value">{{ p.label }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="addEntry">确认添加</el-button>
<el-button @click="showAddEntry = false">取消</el-button>
</el-form-item>
</el-form>
</div>
<!-- 保存按钮 -->
<div class="save-bar">
<el-button type="primary" :loading="saving" @click="saveAcl">保存授权配置</el-button>
</div>
</el-card>
<!-- 使用说明 -->
<el-card class="section-card help-card">
<template #header><span>使用说明</span></template>
<div class="help-content">
<p><strong>什么是资源授权</strong></p>
<p>默认情况下普通用户只能看到自己创建的资源通过授权管理员可以让指定用户也能看到和使用别人的资源</p>
<br />
<p><strong>典型场景</strong></p>
<ul>
<li>zhangsan 训练出了模型 ft_qwen_001admin 授权给 lisilisi 可以用这个模型做推理</li>
<li>admin 上传了公共数据集授权给指定用户被授权用户可以在训练时使用该数据集</li>
</ul>
<br />
<p><strong>权限说明</strong></p>
<ul>
<li><code>read</code> - 在列表中看到该资源</li>
<li><code>execute</code> - 用该资源做训练/推理/评测</li>
<li><code>write</code> - 修改资源信息</li>
<li><code>download</code> - 下载文件</li>
<li><code>delete</code> - 删除资源</li>
<li><code>admin</code> - 全部权限含转授权</li>
</ul>
</div>
</el-card>
</div>
</template>
<style scoped lang="scss">
.page {
padding: 20px;
max-width: 1100px;
}
.page-title {
font-size: 22px;
font-weight: 600;
margin: 0 0 8px;
}
.page-desc {
color: #666;
margin: 0 0 24px;
font-size: 14px;
}
.section-card {
margin-bottom: 20px;
}
.add-entry-form {
background: #f9fafc;
padding: 16px;
border-radius: 8px;
border: 1px dashed #dcdfe6;
}
.save-bar {
text-align: right;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #ebeef5;
}
.help-card {
.help-content {
font-size: 14px;
line-height: 1.8;
color: #555;
ul {
padding-left: 20px;
margin: 4px 0;
li { margin-bottom: 4px; }
}
code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
}
}
}
</style>