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

962 lines
23 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="generated-assets-page">
<div class="page-header">
<div class="panel-title">作品库</div>
</div>
<div class="view-tabs">
<a-tabs v-model="activeTab" @change="handleTabChange" type="card">
<a-tab-pane key="personal" title="个人" />
<a-tab-pane key="department" title="部门" />
</a-tabs>
</div>
<!-- 查询区域 -->
<div class="query-section">
<div class="form-grid">
<div class="field">
<label>收藏状态</label>
<a-select v-model="filters.isTop" clearable placeholder="全部">
<a-option :value="null">全部</a-option>
<a-option value="Y">已收藏</a-option>
<a-option value="N">未收藏</a-option>
</a-select>
</div>
<div class="field">
<label>开始时间</label>
<a-date-picker v-model="filters.beginTime" placeholder="选择开始时间" style="width: 100%" />
</div>
<div class="field">
<label>结束时间</label>
<a-date-picker v-model="filters.endTime" placeholder="选择结束时间" style="width: 100%" />
</div>
<div class="field actions">
<a-button type="primary" :loading="loading" @click="search(1)">查询</a-button>
<a-button @click="resetFilters">重置</a-button>
</div>
</div>
</div>
<!-- 表格区域 -->
<a-spin :loading="loading">
<div class="total-line">总数{{ pagination.total }}</div>
<div class="table-wrap">
<table class="asset-table">
<thead>
<tr>
<th>ID</th>
<th>订单编号</th>
<th>类型</th>
<th>状态</th>
<th>模型参数</th>
<th>生成结果</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in dataList" :key="item.id">
<td class="td-id">{{ item.id }}</td>
<td>{{ item.orderNum || '-' }}</td>
<td>{{ formatType(item.type) }}</td>
<td>
<a-tag :color="getStatusColor(item.status)">
{{ formatStatus(item.status) }}
</a-tag>
</td>
<td>
<div class="params-cell">
<div v-if="item.model" class="param-tag">模型: {{ item.model }}</div>
<div v-if="item.duration" class="param-tag">时长: {{ item.duration }}s</div>
<div v-if="item.resolution" class="param-tag">分辨率: {{ item.resolution }}</div>
<div v-if="item.ratio" class="param-tag">比例: {{ item.ratio }}</div>
<div v-if="item.mode" class="param-tag">模式: {{ formatMode(item.mode) }}</div>
<div v-if="!item.model && !item.duration && !item.resolution && !item.ratio && !item.mode" class="param-empty">-</div>
</div>
</td>
<td class="result-cell">
<div v-if="isVideoResult(item.result)" class="media-preview">
<video
class="video-thumb"
:src="item.result"
controls
preload="metadata"
@click.stop="openPreview(item.result, 'video')" />
</div>
<div v-else-if="isImageResult(item.result)" class="media-preview">
<img
class="image-thumb"
:src="item.result"
@click.stop="openPreview(item.result, 'image')" />
</div>
<div v-else-if="item.result && item.result.startsWith('cgt-')" class="task-id">
{{ item.result }}
</div>
<div v-else class="result-empty">-</div>
</td>
<td>{{ item.createTime || '-' }}</td>
<td>
<a-button
size="mini"
:type="item.isTop === 'Y' ? 'primary' : 'outline'"
:status="item.isTop === 'Y' ? 'success' : 'default'"
@click="toggleFavorite(item)">
<template v-if="item.isTop === 'Y'">
<a-icon name="star-fill" /> 已收藏
</template>
<template v-else>
<a-icon name="star" /> 收藏
</template>
</a-button>
<a-button
size="mini"
type="outline"
@click="viewDetail(item)">
详情
</a-button>
</td>
</tr>
<tr v-if="!dataList.length">
<td colspan="8" class="empty-tip">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="pager">
<a-pagination
:total="pagination.total"
:current="pagination.pageNum"
:page-size="pagination.pageSize"
show-total
show-jumper
show-page-size
:page-size-options="pagination.pageSizes"
@change="changePage"
@page-size-change="changePageSize" />
</div>
</a-spin>
<!-- 详情弹窗 -->
<a-modal v-model:visible="detailVisible" title="订单详情" :footer="false" width="850px">
<div class="detail-content" v-if="detailData">
<div class="detail-form">
<!-- 基本信息 -->
<div class="detail-group">
<div class="group-title">基本信息</div>
<div class="detail-row">
<div class="label">订单编号</div>
<div class="value">{{ detailData.orderNum || '-' }}</div>
</div>
<div class="detail-row">
<div class="label">功能类型</div>
<div class="value">{{ formatType(detailData.type) }}</div>
</div>
<div class="detail-row">
<div class="label">扣除积分</div>
<div class="value">{{ detailData.amount ? detailData.amount + ' 积分' : '-' }}</div>
</div>
<div class="detail-row">
<div class="label">订单状态</div>
<div class="value">
<a-tag :color="getStatusColor(detailData.status)">
{{ formatStatus(detailData.status) }}
</a-tag>
</div>
</div>
<div class="detail-row">
<div class="label">执行状态</div>
<div class="value">{{ formatExtStatus(detailData.extStatus) }}</div>
</div>
<div class="detail-row">
<div class="label">收藏状态</div>
<div class="value">
<a-tag v-if="detailData.isTop === 'Y'" color="orange">已收藏</a-tag>
<span v-else class="text-muted">未收藏</span>
</div>
</div>
<div class="detail-row">
<div class="label">请求来源</div>
<div class="value">{{ detailData.source || '-' }}</div>
</div>
</div>
<!-- 生成参数 -->
<div class="detail-group">
<div class="group-title">生成参数</div>
<div class="detail-row">
<div class="label">提示词</div>
<div class="value long-text">{{ detailData.text || '-' }}</div>
</div>
<div class="detail-row">
<div class="label">生成模式</div>
<div class="value">{{ formatMode(detailData.mode) }}</div>
</div>
<div class="detail-row">
<div class="label">使用模型</div>
<div class="value">{{ detailData.model || '-' }}</div>
</div>
<div class="detail-row">
<div class="label">视频时长</div>
<div class="value">{{ detailData.duration ? detailData.duration + ' 秒' : '-' }}</div>
</div>
<div class="detail-row">
<div class="label">分辨率</div>
<div class="value">{{ detailData.resolution || '-' }}</div>
</div>
<div class="detail-row">
<div class="label">画面比例</div>
<div class="value">{{ detailData.ratio || '-' }}</div>
</div>
</div>
<!-- 生成结果 -->
<div class="detail-group">
<div class="group-title">生成结果</div>
<div class="detail-row">
<div class="label">生成内容</div>
<div class="value media-preview">
<video
v-if="isVideoResult(detailData.result)"
class="detail-video"
:src="detailData.result"
controls
preload="metadata" />
<img
v-else-if="isImageResult(detailData.result)"
class="detail-image"
:src="detailData.result"
@click="viewImageFull(detailData.result)" />
<div v-else class="result-text">{{ detailData.result || '无结果' }}</div>
</div>
</div>
<div class="detail-row" v-if="detailData.img1">
<div class="label">首帧图片</div>
<div class="value media-preview">
<img class="detail-image small" :src="detailData.img1" @click="viewImageFull(detailData.img1)" />
</div>
</div>
<div class="detail-row" v-if="detailData.img2">
<div class="label">尾帧图片</div>
<div class="value media-preview">
<img class="detail-image small" :src="detailData.img2" @click="viewImageFull(detailData.img2)" />
</div>
</div>
</div>
<!-- 完整参数 -->
<div class="detail-group" v-if="detailData.videoParams">
<div class="group-title">完整参数</div>
<div class="detail-row">
<div class="label">JSON参数</div>
<div class="value">
<pre class="json-block">{{ formatVideoParams(detailData.videoParams) }}</pre>
</div>
</div>
</div>
<!-- 时间信息 -->
<div class="detail-group">
<div class="group-title">时间信息</div>
<div class="detail-row">
<div class="label">创建时间</div>
<div class="value">{{ detailData.createTime || '-' }}</div>
</div>
<div class="detail-row">
<div class="label">更新时间</div>
<div class="value">{{ detailData.updateTime || '-' }}</div>
</div>
</div>
</div>
</div>
</a-modal>
<!-- 预览弹窗 -->
<a-modal
v-model:visible="previewVisible"
:title="previewType === 'video' ? '视频预览' : '图片预览'"
:footer="false"
width="800px"
@cancel="closePreview">
<div class="preview-content">
<video
v-if="previewType === 'video' && previewUrl"
class="preview-video"
:src="previewUrl"
controls
autoplay />
<img
v-else-if="previewType === 'image' && previewUrl"
class="preview-image"
:src="previewUrl" />
</div>
</a-modal>
</div>
</template>
<script>
export default {
name: 'GeneratedAssets',
data() {
return {
loading: false,
dataList: [],
filters: {
isTop: null,
beginTime: '',
endTime: ''
},
activeTab: 'personal', // 'personal'个人, 'department'部门
pagination: {
total: 0,
pageNum: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100]
},
detailVisible: false,
detailData: null,
previewVisible: false,
previewUrl: '',
previewType: 'video'
}
},
mounted() {
this.search()
},
methods: {
// 查询数据
search(page = 1) {
this.pagination.pageNum = page
this.loading = true
const params = {
pageNum: this.pagination.pageNum,
pageSize: this.pagination.pageSize
}
console.log("this.activeTab", this.activeTab)
// 视图模式参数personal = 个人, department = 部门视图(传 dept=true
if (this.activeTab === 'department') {
params.dept = true
}
// 明确传递所有查询条件 - 使用 is_top 作为参数名
if (this.filters.isTop != null && String(this.filters.isTop) !== '') {
// 如果isTop是null或者空字符串或者全部则不传递
if (this.filters.isTop === null || this.filters.isTop === '' || this.filters.isTop === '全部') {
delete params.is_top
} else {
params.is_top = String(this.filters.isTop)
}
}
if (this.filters.beginTime) {
params.beginTime = this.formatDate(this.filters.beginTime)
}
if (this.filters.endTime) {
params.endTime = this.formatDate(this.filters.endTime)
}
this.$axios({
url: 'api/portal/assets/list',
method: 'GET',
data: params
})
.then((res) => {
this.loading = false
if (res.code === 200) {
this.dataList = res.rows || []
this.pagination.total = res.total || 0
} else {
this.$message.error(res.msg || '查询失败')
}
})
.catch((err) => {
this.loading = false
this.$message.error(err?.message || '查询失败')
})
},
// 重置筛选
resetFilters() {
this.filters.isTop = null
this.filters.beginTime = ''
this.filters.endTime = ''
this.search(1)
},
// Tabs 切换处理personal/部门视图)
handleTabChange(tabKey) {
if (tabKey) {
this.activeTab = tabKey
}
this.pagination.pageNum = 1
this.search(1)
},
// 分页变化
changePage(page) {
this.pagination.pageNum = page
this.search(page)
},
// 每页条数变化
changePageSize(pageSize) {
this.pagination.pageSize = pageSize
this.pagination.pageNum = 1
this.search(1)
},
// 格式化日期
formatDate(date) {
if (!date) return ''
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
// 格式化类型
formatType(type) {
const typeMap = {
'11': '图生图',
'12': '图生图2',
'13': '换脸',
'1': '快速生图',
'21': '快速生视频'
}
return typeMap[String(type)] || type || '-'
},
// 格式化状态
formatStatus(status) {
const statusMap = {
0: '进行中',
1: '已完成',
2: '失败'
}
return statusMap[status] || status || '-'
},
// 格式化扩展状态
formatExtStatus(extStatus) {
const statusMap = {
'running': '执行中',
'queued': '队列中',
'succeeded': '已完成',
'failed': '失败',
'cancelled': '已取消',
'expired': '超时'
}
return statusMap[extStatus] || '未知'
},
// 获取状态颜色
getStatusColor(status) {
const colorMap = {
0: 'blue',
1: 'green',
2: 'red'
}
return colorMap[status] || 'default'
},
// 格式化模式
formatMode(mode) {
const modeMap = {
'text-to-video': '文生视频',
'image-first-frame': '图生视频·首帧',
'image-first-last-frame': '图生视频·首尾帧',
'image-reference': '图生视频·参考图'
}
return modeMap[mode] || mode || '-'
},
// 格式化videoParams
formatVideoParams(params) {
if (!params) return ''
try {
const obj = typeof params === 'string' ? JSON.parse(params) : params
return JSON.stringify(obj, null, 2)
} catch (e) {
return params
}
},
// 判断是否为视频结果
isVideoResult(url) {
if (!url) return false
return /\.(mp4|mov|webm|ogg|m4v|avi|mkv)(\?.*)?$/i.test(url)
},
// 判断是否为图片结果
isImageResult(url) {
if (!url) return false
return /\.(jpeg|jpg|png|gif|webp|bmp)(\?.*)?$/i.test(url)
},
// 打开预览
openPreview(url, type) {
this.previewUrl = url
this.previewType = type
this.previewVisible = true
},
// 关闭预览
closePreview() {
this.previewVisible = false
this.previewUrl = ''
},
// 查看图片全屏
viewImageFull(url) {
this.$viewerApi({
options: {
initialViewIndex: 0,
toolbar: true
},
images: [url]
})
},
// 查看详情
viewDetail(item) {
this.detailData = item
this.detailVisible = true
},
// 切换收藏状态
toggleFavorite(item) {
const newIsTop = item.isTop === 'Y' ? 'N' : 'Y'
this.$axios({
url: '/api/portal/assets/favorite',
method: 'POST',
data: {
id: item.id,
isTop: newIsTop
}
})
.then((res) => {
if (res.code === 200) {
item.isTop = newIsTop
this.$message.success(newIsTop === 'Y' ? '收藏成功' : '取消收藏成功')
} else {
this.$message.error(res.msg || '操作失败')
}
})
.catch((err) => {
this.$message.error(err?.message || '操作失败')
})
}
}
}
</script>
<style scoped lang="less">
.generated-assets-page {
padding: 16px;
min-height: 100%;
background: #0a0b0d;
color: rgba(255, 255, 255, 0.9);
}
.page-header {
margin-bottom: 16px;
}
.panel-title {
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.query-section {
background: rgba(22, 24, 30, 0.92);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(4, minmax(180px, 1fr));
gap: 12px;
@media (max-width: 1200px) {
grid-template-columns: repeat(3, minmax(180px, 1fr));
}
@media (max-width: 900px) {
grid-template-columns: repeat(2, minmax(180px, 1fr));
}
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
label {
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
}
&.actions {
flex-direction: row;
align-items: flex-end;
gap: 8px;
}
}
.total-line {
margin: 10px 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
}
.table-wrap {
overflow: auto;
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
}
.asset-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 12px;
th, td {
padding: 12px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
text-align: left;
vertical-align: top;
}
th {
color: rgba(255, 255, 255, 0.72);
background: rgba(255, 255, 255, 0.02);
font-weight: 500;
}
// 列宽定义
th:nth-child(1), td:nth-child(1) { width: 60px; }
th:nth-child(2), td:nth-child(2) { width: 120px; }
th:nth-child(3), td:nth-child(3) { width: 80px; }
th:nth-child(4), td:nth-child(4) { width: 80px; }
th:nth-child(5), td:nth-child(5) { width: 180px; }
th:nth-child(6), td:nth-child(6) { width: 200px; }
th:nth-child(7), td:nth-child(7) { width: 140px; }
th:nth-child(8), td:nth-child(8) { width: 140px; }
}
.td-id {
word-break: break-all;
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
}
.params-cell {
display: flex;
flex-direction: column;
gap: 4px;
.param-tag {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.05);
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.param-empty {
color: rgba(255, 255, 255, 0.35);
}
}
.result-cell {
.media-preview {
.video-thumb {
width: 160px;
height: 90px;
object-fit: cover;
border-radius: 6px;
background: #000;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
.image-thumb {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 6px;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
}
.task-id {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
word-break: break-all;
}
.result-empty {
color: rgba(255, 255, 255, 0.35);
}
}
.pager {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.empty-tip {
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
text-align: center;
padding: 40px 0;
}
/* 详情弹窗 - label:value 形式深色背景白字 */
.detail-content {
padding: 20px;
font-size: 14px;
background: #1a1f2e;
color: rgba(255, 255, 255, 0.95);
min-height: 100%;
.detail-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.detail-group {
background: rgba(22, 24, 30, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
}
.group-title {
font-size: 15px;
font-weight: 600;
color: #00cae0;
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(0, 202, 224, 0.2);
}
.detail-row {
display: flex;
align-items: flex-start;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
&:last-child {
border-bottom: none;
}
}
.label {
width: 100px;
flex-shrink: 0;
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
padding-top: 2px;
}
.value {
flex: 1;
color: rgba(255, 255, 255, 0.95);
word-break: break-all;
line-height: 1.5;
&.long-text {
max-height: 110px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.3);
padding: 10px;
border-radius: 6px;
font-size: 13px;
}
.text-muted {
color: rgba(255, 255, 255, 0.45);
}
}
.media-preview {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-video {
width: 100%;
max-width: 420px;
border-radius: 8px;
background: #000;
}
.detail-image {
max-width: 100%;
max-height: 260px;
border-radius: 8px;
cursor: pointer;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s ease;
&:hover {
transform: scale(1.03);
box-shadow: 0 0 0 3px rgba(0, 202, 224, 0.3);
}
&.small {
max-height: 160px;
}
}
.result-text {
color: rgba(255, 255, 255, 0.5);
font-style: italic;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
}
.json-block {
background: #1a1f2e;
color: #a5d6ff;
padding: 14px;
border-radius: 8px;
font-size: 12px;
max-height: 220px;
overflow: auto;
white-space: pre;
font-family: 'Consolas', monospace;
line-height: 1.4;
border: 1px solid rgba(165, 214, 255, 0.1);
}
}
// 预览弹窗样式
// Tabs 样式 - 深色主题卡片风格适配 Arco Design Vue + 项目深色UI
.view-tabs {
margin-bottom: 20px;
padding: 0 4px;
::v-deep(.arco-tabs) {
.arco-tabs-nav {
background: rgba(22, 24, 30, 0.98) !important;
border: 1px solid rgba(255, 255, 255, 0.12) !important;
border-radius: 12px !important;
padding: 6px !important;
margin-bottom: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.arco-tabs-tab {
color: rgba(255, 255, 255, 0.85) !important;
margin: 0 4px !important;
padding: 10px 28px !important;
border-radius: 8px !important;
transition: all 0.2s ease !important;
font-size: 14px;
font-weight: 500;
border: 1px solid transparent !important;
}
.arco-tabs-tab:hover {
color: white !important;
background: rgba(0, 102, 204, 0.25) !important;
border-color: rgba(0, 102, 204, 0.3) !important;
}
.arco-tabs-tab-active {
color: white !important;
background: #0066cc !important;
border-color: #0066cc !important;
font-weight: 600 !important;
box-shadow: 0 4px 15px rgba(0, 102, 204, 0.5) !important;
}
.arco-tabs-ink-bar {
display: none !important;
}
// 当前 tabs 仅用于视图切换不需要内容区域避免出现白色空框
.arco-tabs-content {
display: none !important;
}
}
}
.tabs-field {
grid-column: span 2;
}
.preview-content {
display: flex;
justify-content: center;
align-items: center;
.preview-video {
width: 100%;
max-height: 500px;
border-radius: 8px;
}
.preview-image {
max-width: 100%;
max-height: 500px;
border-radius: 8px;
}
}
/* 作品详情弹窗深色底样式 - 严格符合 GeneratedAssets.vue 深色主题适配 Arco Design */
::v-deep(.arco-modal) {
.arco-modal-content {
background-color: #1a1f2e !important;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.arco-modal-header {
background: transparent !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: 20px 24px 16px;
}
.arco-modal-title {
color: rgba(255, 255, 255, 0.95) !important;
font-size: 17px;
font-weight: 600;
}
.arco-modal-close {
color: rgba(255, 255, 255, 0.65) !important;
top: 18px;
right: 20px;
&:hover {
color: #fff !important;
background: rgba(255, 255, 255, 0.1);
}
}
.arco-modal-body {
padding: 0 !important;
background: transparent;
color: rgba(255, 255, 255, 0.9);
}
// 确保 detail-content 与页面卡片风格一致深色背景白字
.detail-content {
background: #1a1f2e !important;
color: rgba(255, 255, 255, 0.95) !important;
padding: 20px !important;
min-height: 200px;
}
}
</style>