ai_images/portal-ui/src/views/AssetManage.vue

542 lines
14 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.

<template>
<div class="asset-manage-page">
<section class="asset-left">
<div class="asset-left-head">
<div class="panel-title">素材组树</div>
<div class="field actions">
<a-button size="mini" type="outline" :loading="groupLoading" @click="loadGroups">刷新分组</a-button>
</div>
</div>
<div class="group-tree">
<div
v-for="g in groups"
:key="g.Id || g.id"
:class="['group-node', { active: selectedGroupId === (g.Id || g.id) }]"
@click="selectGroup(g)">
<span class="folder-icon">📁</span>
<div class="group-meta">
<div class="group-name">{{ g.Name || g.name || (g.Id || g.id) }}</div>
<div class="group-id">{{ g.Id || g.id }}</div>
</div>
</div>
<div v-if="!groups.length" class="empty-tip">暂无素材组,请先在“资源组管理”创建</div>
</div>
</section>
<section class="asset-right">
<div class="asset-left-head">
<div class="panel-title">素材管理</div>
<div class="field actions">
<a-button type="primary" size="mini" @click="openCreateAssetDialog">新增素材</a-button>
</div>
</div>
<div class="panel-title mtop">查询素材</div>
<div class="form-grid">
<div class="field">
<label>GroupId</label>
<a-input v-model="filters.groupId" placeholder="按组过滤(默认选中左侧)" />
</div>
<div class="field">
<label>Name</label>
<a-input v-model="filters.name" placeholder="按名称过滤" />
</div>
<div class="field">
<label>Status</label>
<a-select v-model="filters.status">
<a-option value="">全部</a-option>
<a-option value="Active">Active</a-option>
<a-option value="Processing">Processing</a-option>
<a-option value="Failed">Failed</a-option>
</a-select>
</div>
<div class="field">
<label>SortBy</label>
<a-select v-model="filters.sortBy">
<a-option value="CreateTime">CreateTime</a-option>
<a-option value="UpdateTime">UpdateTime</a-option>
<a-option value="GroupId">GroupId</a-option>
</a-select>
</div>
<div class="field">
<label>SortOrder</label>
<a-select v-model="filters.sortOrder">
<a-option value="Desc">Desc</a-option>
<a-option value="Asc">Asc</a-option>
</a-select>
</div>
<div class="field actions">
<a-button type="primary" :loading="listLoading" @click="searchAssets(1)">查询</a-button>
<a-button @click="resetFilters">重置</a-button>
</div>
</div>
<a-spin :loading="listLoading">
<div class="total-line">总数{{ totalCount }}</div>
<div class="table-wrap">
<table class="asset-table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>URL</th>
<th>GroupId</th>
<th>AssetType</th>
<th>Status</th>
<th>CreateTime</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="it in items" :key="it.Id || it.id">
<td>{{ it.Id || it.id }}</td>
<td>{{ it.Name || it.name || '-' }}</td>
<td class="url-cell">{{ it.URL || it.url || '-' }}</td>
<td>{{ it.GroupId || it.groupId || '-' }}</td>
<td>{{ it.AssetType || it.assetType || '-' }}</td>
<td>{{ it.Status || it.status || '-' }}</td>
<td>{{ it.CreateTime || it.createTime || '-' }}</td>
<td>
<a-button size="mini" type="outline" @click="getAsset(it)">详情</a-button>
<a-button size="mini" status="danger" @click="deleteAsset(it)">删除</a-button>
</td>
</tr>
<tr v-if="!items.length">
<td colspan="8" class="empty-tip">暂无素材</td>
</tr>
</tbody>
</table>
</div>
<div class="pager">
<a-pagination
:total="totalCount"
:current="filters.pageNumber"
:page-size="filters.pageSize"
show-total
show-jumper
@change="searchAssets"
@page-size-change="onPageSizeChange" />
</div>
</a-spin>
</section>
<a-modal v-model:visible="detailVisible" title="素材详情" :footer="false" width="700px">
<pre class="detail-box">{{ detailText }}</pre>
</a-modal>
<a-modal
v-model:visible="createAssetVisible"
title="新增素材"
:confirm-loading="createLoading"
@ok="createAsset">
<div class="create-form-vertical">
<div class="field">
<label>素材组</label>
<a-select v-model="createForm.groupId" placeholder="请选择素材组">
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
{{ g.Name || g.name || (g.Id || g.id) }}
</a-option>
</a-select>
</div>
<div class="field">
<label>上传素材</label>
<input type="file" @change="onFileChange" />
<div class="group-id">{{ createForm.fileName || '未选择文件' }}</div>
</div>
<div class="field">
<label>素材名称</label>
<a-input v-model="createForm.name" placeholder="素材名称(可选)" />
</div>
<div class="field">
<label>类型</label>
<a-select v-model="createForm.assetType">
<a-option value="Image">图片</a-option>
<a-option value="Video">视频</a-option>
<a-option value="Audio">音频</a-option>
</a-select>
</div>
</div>
</a-modal>
</div>
</template>
<script>
const GROUP_LIST_API = 'api/byteAssetGroup/listAssetGroups'
const ASSET_CREATE_API = 'api/byteAsset/createAsset'
const ASSET_LIST_API = 'api/byteAsset/listAssets'
const ASSET_GET_API = 'api/byteAsset/getAsset'
const ASSET_DELETE_API = 'api/byteAsset/deleteAsset'
export default {
name: 'AssetManage',
data() {
return {
groupLoading: false,
createLoading: false,
listLoading: false,
createAssetVisible: false,
groups: [],
selectedGroupId: '',
createForm: {
groupId: '',
file: null,
fileName: '',
name: '',
assetType: 'Image'
},
filters: {
groupId: '',
name: '',
status: '',
pageNumber: 1,
pageSize: 10,
sortBy: 'CreateTime',
sortOrder: 'Desc'
},
totalCount: 0,
items: [],
detailVisible: false,
detailData: null
}
},
computed: {
detailText() {
return this.detailData ? JSON.stringify(this.detailData, null, 2) : '{}'
}
},
mounted() {
this.loadGroups()
},
methods: {
async openCreateAssetDialog() {
await this.loadGroups()
this.createForm.groupId = this.selectedGroupId || this.createForm.groupId || ''
this.createForm.file = null
this.createForm.fileName = ''
this.createForm.name = ''
this.createForm.assetType = 'Image'
this.createAssetVisible = true
},
async loadGroups() {
this.groupLoading = true
try {
const res = await this.$axios({
url: GROUP_LIST_API,
method: 'POST',
data: {
Filter: { GroupType: 'AIGC' },
PageNumber: 1,
PageSize: 100,
SortBy: 'CreateTime',
SortOrder: 'Desc'
}
})
this.groups = Array.isArray(res?.data?.Items) ? res.data.Items : []
if (!this.selectedGroupId && this.groups.length) {
const gid = this.groups[0].Id || this.groups[0].id
this.selectedGroupId = gid
this.createForm.groupId = gid
this.filters.groupId = gid
this.searchAssets(1)
}
} catch (e) {
this.$message.error(e?.message || '加载分组失败')
} finally {
this.groupLoading = false
}
},
selectGroup(g) {
const gid = g?.Id || g?.id
this.selectedGroupId = gid
this.createForm.groupId = gid
this.filters.groupId = gid
this.searchAssets(1)
},
onFileChange(e) {
const file = e?.target?.files?.[0]
this.createForm.file = file || null
this.createForm.fileName = file?.name || ''
},
async createAsset() {
const groupId = String(this.createForm.groupId || '').trim()
if (!groupId) return this.$message.error('请填写 GroupId')
if (!this.createForm.file) return this.$message.error('请选择上传文件')
this.createLoading = true
try {
const fd = new FormData()
fd.append('file', this.createForm.file)
fd.append('groupId', groupId)
fd.append('assetType', this.createForm.assetType)
fd.append('name', String(this.createForm.name || '').trim())
const res = await this.$axios({
url: ASSET_CREATE_API,
method: 'POST',
data: fd
})
if (res.code === 200) {
this.$message.success('新增素材成功')
this.createForm.file = null
this.createForm.fileName = ''
this.createForm.name = ''
this.createAssetVisible = false
this.searchAssets(1)
} else {
this.$message.error(res.msg || '新增素材失败')
}
} catch (e) {
this.$message.error(e?.message || '新增素材失败')
} finally {
this.createLoading = false
}
},
buildListPayload() {
const filter = {
GroupType: 'AIGC'
}
const gid = String(this.filters.groupId || '').trim()
if (gid) filter.GroupIds = [gid]
const name = String(this.filters.name || '').trim()
if (name) filter.Name = name
if (this.filters.status) filter.Statuses = [this.filters.status]
return {
Filter: filter,
PageNumber: this.filters.pageNumber,
PageSize: this.filters.pageSize,
SortBy: this.filters.sortBy,
SortOrder: this.filters.sortOrder
}
},
async searchAssets(page = this.filters.pageNumber) {
this.filters.pageNumber = Number(page) || 1
this.listLoading = true
try {
const res = await this.$axios({
url: ASSET_LIST_API,
method: 'POST',
data: this.buildListPayload()
})
this.totalCount = Number(res?.data?.TotalCount || 0)
this.items = Array.isArray(res?.data?.Items) ? res.data.Items : []
if (res.code !== 200) {
this.$message.error(res.msg || '查询素材失败')
}
} catch (e) {
this.$message.error(e?.message || '查询素材失败')
} finally {
this.listLoading = false
}
},
onPageSizeChange(size) {
this.filters.pageSize = Number(size) || 10
this.searchAssets(1)
},
resetFilters() {
this.filters = {
groupId: this.selectedGroupId || '',
name: '',
status: '',
pageNumber: 1,
pageSize: 10,
sortBy: 'CreateTime',
sortOrder: 'Desc'
}
this.searchAssets(1)
},
async getAsset(it) {
const id = it?.Id || it?.id
if (!id) return
try {
const res = await this.$axios({
url: ASSET_GET_API,
method: 'POST',
data: { Id: id }
})
if (res.code === 200) {
this.detailData = res.data || {}
this.detailVisible = true
} else {
this.$message.error(res.msg || '查询详情失败')
}
} catch (e) {
this.$message.error(e?.message || '查询详情失败')
}
},
deleteAsset(it) {
const id = it?.Id || it?.id
if (!id) return
this.$confirm({
title: '删除素材',
content: `确认删除素材 ${id} 吗?`,
onOk: async () => {
try {
const res = await this.$axios({
url: ASSET_DELETE_API,
method: 'POST',
data: { Id: id }
})
if (res.code === 200) {
this.$message.success('删除成功')
this.searchAssets(this.filters.pageNumber)
} else {
this.$message.error(res.msg || '删除失败')
}
} catch (e) {
this.$message.error(e?.message || '删除失败')
}
}
})
}
}
}
</script>
<style scoped lang="less">
.asset-manage-page {
display: grid;
grid-template-columns: 300px 1fr;
gap: 14px;
padding: 16px;
min-height: 100%;
background: #0a0b0d;
color: rgba(255, 255, 255, 0.9);
}
.asset-left,
.asset-right {
background: rgba(22, 24, 30, 0.92);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 12px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
}
.asset-left-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.group-tree {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 72vh;
overflow: auto;
}
.group-node {
display: flex;
gap: 8px;
align-items: flex-start;
padding: 8px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
cursor: pointer;
}
.group-node:hover {
background: rgba(255, 255, 255, 0.04);
}
.group-node.active {
border-color: rgba(0, 202, 224, 0.45);
background: rgba(0, 202, 224, 0.12);
}
.folder-icon {
font-size: 14px;
line-height: 1.6;
}
.group-name {
font-size: 13px;
}
.group-id {
font-size: 12px;
color: rgba(255, 255, 255, 0.55);
word-break: break-all;
}
.form-grid {
display: grid;
grid-template-columns: repeat(3, minmax(180px, 1fr));
gap: 10px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field label {
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
}
.field.actions {
align-self: end;
flex-direction: row;
align-items: center;
}
.create-form-vertical {
display: flex;
flex-direction: column;
gap: 12px;
}
.mtop {
margin-top: 14px;
}
.total-line {
margin: 10px 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
}
.table-wrap {
overflow: auto;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
}
.asset-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.asset-table th,
.asset-table td {
padding: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
text-align: left;
vertical-align: top;
}
.asset-table th {
color: rgba(255, 255, 255, 0.72);
background: rgba(255, 255, 255, 0.02);
}
.url-cell {
max-width: 280px;
word-break: break-all;
}
.pager {
margin-top: 10px;
display: flex;
justify-content: flex-end;
}
.empty-tip {
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
text-align: center;
padding: 10px 0;
}
.detail-box {
margin: 0;
padding: 10px;
background: rgba(0, 0, 0, 0.35);
border-radius: 8px;
max-height: 420px;
overflow: auto;
color: #d8f4f7;
}
@media (max-width: 980px) {
.asset-manage-page {
grid-template-columns: 1fr;
}
.form-grid {
grid-template-columns: 1fr;
}
}
</style>