Files
msh-system/msh_single_uniapp/pages/ai-generate/assets.vue

1059 lines
24 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
<view class="container">
<!-- AI生成内容标识 -->
<view class="ai-notice">
<text class="ai-notice-icon">🤖</text>
<text class="ai-notice-text">作品由AI生成结果仅供参考</text>
</view>
<!-- 页面头部 -->
<view class="header">
<text class="subtitle">管理你的AI数字资产</text>
</view>
<!-- 资产统计 -->
<view class="stats-section">
<view class="stats-card">
<text class="stats-number">{{totalAssets}}</text>
<text class="stats-label">总资产数</text>
</view>
<view class="stats-card">
<text class="stats-number">{{activeAssets}}</text>
<text class="stats-label">活跃资产</text>
</view>
<view class="stats-card">
<text class="stats-number">{{totalViews}}</text>
<text class="stats-label">总播放量</text>
</view>
</view>
<!-- 资产分类 -->
<view class="category-section">
<view class="category-tabs">
<view class="tab" :class="{active: currentTab === 'all'}" @tap="switchTab('all')">
全部
</view>
<view class="tab" :class="{active: currentTab === 'video'}" @tap="switchTab('video')">
装修实拍
</view>
<view class="tab" :class="{active: currentTab === 'image'}" @tap="switchTab('image')">
翻新前后
</view>
<view class="tab" :class="{active: currentTab === 'text'}" @tap="switchTab('text')">
设计案例
</view>
</view>
</view>
<!-- 资产列表 -->
<view class="assets-list">
<!-- 加载状态 -->
<transition name="fade" mode="out-in">
<view class="loading-state" v-if="loading && assetList.length === 0" key="loading">
<!-- 主加载动画 -->
<view class="loading-container">
<view class="loading-spinner">
<view class="spinner-ring"></view>
<view class="spinner-ring"></view>
<view class="spinner-ring"></view>
</view>
<text class="loading-text">{{ loadingText }}</text>
<text class="loading-subtitle">{{ loadingSubtitle }}</text>
</view>
<!-- 骨架屏 -->
<view class="skeleton-container" v-if="showSkeleton">
<view class="skeleton-item" v-for="n in 3" :key="n">
<view class="skeleton-cover"></view>
<view class="skeleton-content">
<view class="skeleton-title"></view>
<view class="skeleton-desc"></view>
<view class="skeleton-meta">
<view class="skeleton-meta-item"></view>
<view class="skeleton-meta-item"></view>
</view>
</view>
</view>
</view>
</view>
<!-- 超时提示 -->
<view class="timeout-state" v-else-if="showTimeout" key="timeout">
<view class="timeout-icon"></view>
<text class="timeout-text">加载时间较长</text>
<text class="timeout-desc">请检查网络连接或稍后重试</text>
<button class="retry-btn" @tap="retryLoad">重新加载</button>
</view>
</transition>
<view class="asset-item" v-for="item in assetList" :key="item.id" @tap="onAssetTap(item)">
<view class="asset-cover">
<image :src="item.coverUrl" mode="aspectFill" class="cover-image" lazy-load="true"></image>
<view class="asset-type">{{item.type}}</view>
<view class="asset-status" :class="'status-' + item.status">{{item.statusText}}</view>
</view>
<view class="asset-info">
<text class="asset-title">{{item.title}}</text>
<text class="asset-desc">{{item.description}}</text>
<view class="asset-meta">
<text class="meta-item">{{item.createTime}}</text>
<text class="meta-item">{{item.viewCount}}次播放</text>
</view>
</view>
</view>
<!-- 加载更多状态 -->
<view class="load-more" v-if="assetList.length > 0">
<text class="load-more-text" v-if="loading">加载中...</text>
<text class="load-more-text" v-else-if="!hasMore">没有更多了</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="assetList.length === 0 && !loading">
<image src="/static/images/empty-assets.png" class="empty-image"></image>
<text class="empty-text">暂无数字资产</text>
<text class="empty-desc">开始创建你的第一个AI数字分身吧</text>
<button class="create-btn" @tap="onCreate">立即创建</button>
</view>
<!-- 浮动创建按钮 -->
<view class="fab" @tap="onCreate">
<image src="/static/images/add.png" class="fab-icon"></image>
</view>
</view>
</template>
<script>
import api from '@/api/models-api.js'
export default {
data() {
return {
// 统计数据
totalAssets: 0,
activeAssets: 0,
totalViews: '0',
// 当前选中的标签
currentTab: 'all',
// 资产列表
assetList: [],
// 原始完整列表(用于筛选)
fullAssetList: [],
// 分页参数
currentPage: 1,
pageSize: 10,
hasMore: true,
loading: false,
// 加载状态优化
loadingText: '加载中...',
loadingSubtitle: '正在获取您的数字资产',
showSkeleton: false,
showTimeout: false,
loadingTimer: null,
timeoutTimer: null
}
},
onLoad() {
console.log('数字资产页面加载')
this.loadArticleList()
},
onShow() {
// 页面显示时刷新数据
this.refreshData()
},
methods: {
// 加载文章列表
async loadArticleList(page = 1, isRefresh = false) {
if (this.loading) return
this.loading = true
this.showTimeout = false
// 设置加载文本动画
this.startLoadingAnimation()
// 延迟显示骨架屏
setTimeout(() => {
if (this.loading) {
this.showSkeleton = true
}
}, 800)
// 设置超时检测
this.timeoutTimer = setTimeout(() => {
if (this.loading) {
this.showTimeout = true
this.showSkeleton = false
}
}, 10000) // 10秒超时
try {
const response = await api.getArticleList({
page: page,
size: this.pageSize
})
if (response.code === 200 && response.data) {
const articles = response.data.records || []
// 转换文章数据为资产数据格式
const assetList = articles.map(article => ({
id: article.id,
title: article.title || '未命名作品',
description: article.prompt || article.synopsis || article.content || '暂无描述',
type: this.getAssetType(article),
coverUrl: article.imageInput || '/static/images/placeholder.png',
createTime: this.formatDate(article.createTime),
viewCount: parseInt(article.visit) || 0,
status: this.getAssetStatus(article.statusTask),
statusText: this.getStatusText(article.statusTask),
videoUrl: article.videoUrl,
taskId: article.taskId,
prompt: article.prompt,
aspectRatio: article.aspectRatio
}))
if (isRefresh || page === 1) {
// 刷新或首次加载
this.assetList = assetList
this.fullAssetList = assetList
this.currentPage = 1
this.hasMore = assetList.length >= this.pageSize
} else {
// 加载更多
this.assetList = [...this.assetList, ...assetList]
this.fullAssetList = [...this.fullAssetList, ...assetList]
this.hasMore = assetList.length >= this.pageSize
}
// 更新统计数据
this.updateStats()
}
} catch (error) {
console.error('加载文章列表失败:', error)
// 网络错误处理
if (!navigator.onLine) {
this.loadingText = '网络连接失败'
this.loadingSubtitle = '请检查网络设置后重试'
} else {
this.loadingText = '加载失败'
this.loadingSubtitle = '服务器暂时无法访问'
}
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
this.stopLoadingAnimation()
this.loading = false
this.showSkeleton = false
this.showTimeout = false
// 清除定时器
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer)
this.timeoutTimer = null
}
}
},
// 开始加载动画
startLoadingAnimation() {
const texts = [
{ main: '加载中...', sub: '正在获取您的数字资产' },
{ main: '努力加载中...', sub: '马上就好了' },
{ main: '即将完成...', sub: '感谢您的耐心等待' }
]
let index = 0
this.loadingTimer = setInterval(() => {
if (this.loading && !this.showTimeout) {
const text = texts[index % texts.length]
this.loadingText = text.main
this.loadingSubtitle = text.sub
index++
}
}, 2000)
},
// 停止加载动画
stopLoadingAnimation() {
if (this.loadingTimer) {
clearInterval(this.loadingTimer)
this.loadingTimer = null
}
// 重置加载文本
this.loadingText = '加载中...'
this.loadingSubtitle = '正在获取您的数字资产'
},
// 重试加载
retryLoad() {
this.showTimeout = false
this.loadArticleList(1, true)
},
// 获取资产类型
getAssetType(article) {
if (article.videoUrl) {
return '视频'
} else if (article.imageInput) {
return '图片'
} else {
return '文本'
}
},
// 获取资产状态
getAssetStatus(statusTask) {
switch (statusTask) {
case 0:
return 'pending'
case 1:
return 'processing'
case 2:
return 'active'
case 3:
return 'failed'
default:
return 'unknown'
}
},
// 获取状态文本
getStatusText(statusTask) {
switch (statusTask) {
case 0:
return '等待中'
case 1:
return '已完成'
case 2:
return '已完成'
case 3:
return '失败'
default:
return '未知'
}
},
// 格式化日期
formatDate(dateString) {
if (!dateString) return ''
const date = new Date(dateString)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
},
// 更新统计数据
updateStats() {
const totalAssets = this.fullAssetList.length
const activeAssets = this.fullAssetList.filter(item => item.status === 'active').length
const totalViews = this.fullAssetList.reduce((sum, item) => sum + item.viewCount, 0)
this.totalAssets = totalAssets
this.activeAssets = activeAssets
this.totalViews = totalViews > 1000 ? `${(totalViews / 1000).toFixed(1)}k` : totalViews.toString()
},
// 刷新数据
refreshData() {
this.loadArticleList(1, true)
},
// 切换标签
switchTab(tab) {
this.currentTab = tab
this.filterAssets(tab)
},
// 筛选资产
filterAssets(tab) {
let filteredList = this.fullAssetList
if (tab !== 'all') {
const typeMap = {
'video': '视频',
'image': '图片',
'text': '文本'
}
filteredList = this.fullAssetList.filter(item =>
item.type === typeMap[tab]
)
}
this.assetList = filteredList
},
// 点击资产项
onAssetTap(asset) {
console.log('查看资产详情:', asset)
// 根据资产类型跳转到不同页面
if (asset.type === '视频') {
uni.navigateTo({
url: `/pages/ai-generate/video?id=${asset.id}`
})
} else {
uni.showToast({
title: `查看${asset.title}`,
icon: 'none'
})
}
},
// 创建新资产
onCreate() {
uni.showActionSheet({
itemList: ['一键创作', '上传模板'],
success: (res) => {
const actions = ['一键创作', '上传模板']
uni.showToast({
title: `已选择:${actions[res.tapIndex]}`,
icon: 'none'
})
// 根据选择跳转到不同的创建页面
if (res.tapIndex === 0) {
// 跳转到一键创作页面
uni.navigateTo({
url: '/pages/ai-generate/oneclick'
})
} else if (res.tapIndex === 1) {
// 保留原逻辑:跳转到视频创作页面
uni.switchTab({
url: '/pages/create/create'
})
}
}
})
}
},
// 下拉刷新
onPullDownRefresh() {
this.refreshData()
setTimeout(() => {
uni.stopPullDownRefresh()
}, 1000)
},
// 上拉加载更多
onReachBottom() {
if (!this.hasMore || this.loading) {
return
}
console.log('加载更多资产')
const nextPage = this.currentPage + 1
this.currentPage = nextPage
this.loadArticleList(nextPage, false)
}
}
</script>
<style>
.container {
padding: 20rpx;
background: linear-gradient(135deg, #149e20 0%, #06430e 100%);
min-height: 100vh;
}
/* AI生成内容标识 */
.ai-notice {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 16rpx 32rpx;
margin: 20rpx 0;
background: rgba(255, 255, 255, 0.1);
border: 2rpx solid rgba(255, 255, 255, 0.2);
border-radius: 24rpx;
backdrop-filter: blur(10rpx);
}
.ai-notice-icon {
font-size: 28rpx;
}
.ai-notice-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
line-height: 1.4;
}
/* 操作按钮与浮动按钮统一主色 */
.create-btn {
background: linear-gradient(45deg, #42ca4d, #2aa53a);
color: #ffffff;
border: none;
border-radius: 50rpx;
padding: 24rpx 48rpx;
font-size: 28rpx;
font-weight: bold;
}
.fab {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(45deg, #42ca4d, #2aa53a);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(66, 202, 77, 0.4);
z-index: 999;
}
.action-btn {
width: 60rpx;
height: 60rpx;
background: rgba(66, 202, 77, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.action-btn:active {
background: rgba(66, 202, 77, 0.2);
transform: scale(0.9);
}
/* 页面头部 */
.header {
text-align: center;
margin-bottom: 20rpx; margin-top: 34px;
padding: 20rpx 0;
}
.title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 10rpx;
}
.subtitle {
display: block;
font-size: 34rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 资产统计 */
.stats-section {
display: flex;
justify-content: space-between;
margin-bottom: 40rpx;
gap: 20rpx;
}
.stats-card {
flex: 1;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20rpx;
padding: 30rpx 20rpx;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stats-number {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 10rpx;
}
.stats-label {
display: block;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 分类标签 */
.category-section {
margin-bottom: 30rpx;
}
.category-tabs {
display: flex;
background: rgba(255, 255, 255, 0.1);
border-radius: 25rpx;
padding: 8rpx;
backdrop-filter: blur(10px);
}
.tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
border-radius: 20rpx;
transition: all 0.3s ease;
}
.tab.active {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
font-weight: bold;
}
/* 资产列表 */
.assets-list {
margin-bottom: 120rpx;
}
.asset-item {
display: flex;
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
margin-bottom: 20rpx;
padding: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
min-height: 220rpx;
}
.asset-item:active {
transform: scale(0.98);
}
.asset-cover {
position: relative;
width: 180rpx;
height: 180rpx;
margin-right: 20rpx;
}
.cover-image {
width: 100%;
height: 100%;
border-radius: 15rpx;
}
.asset-type {
position: absolute;
top: 8rpx;
right: 8rpx;
background: rgba(0, 0, 0, 0.7);
color: #ffffff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
}
.asset-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.asset-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 400rpx;
max-height: 44rpx;
line-height: 44rpx;
}
.asset-desc {
font-size: 26rpx;
color: #666666;
margin-bottom: 12rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
max-width: 400rpx;
max-height: 68rpx;
line-height: 34rpx;
}
.asset-meta {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.meta-item {
font-size: 22rpx;
color: #999999;
}
.asset-status {
font-size: 20rpx;
}
.status-active {
color: #52c41a;
}
.status-processing {
color: #1890ff;
}
.status-failed {
color: #ff4d4f;
}
.asset-actions {
display: flex;
flex-direction: column;
justify-content: center;
gap: 16rpx;
margin-left: 20rpx;
}
.action-btn {
width: 60rpx;
height: 60rpx;
background: rgba(103, 126, 234, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.action-btn:active {
background: rgba(103, 126, 234, 0.2);
transform: scale(0.9);
}
.action-icon {
width: 32rpx;
height: 32rpx;
}
/* 加载状态优化 */
.loading-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 400rpx;
text-align: center;
padding: 60rpx 40rpx;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 60rpx;
}
/* 加载动画 */
.loading-spinner {
position: relative;
width: 120rpx;
height: 120rpx;
margin-bottom: 40rpx;
}
.spinner-ring {
position: absolute;
width: 100%;
height: 100%;
border: 4rpx solid transparent;
border-radius: 50%;
animation: spin 2s linear infinite;
}
.spinner-ring:nth-child(1) {
border-top-color: #42ca4d;
animation-delay: 0s;
}
.spinner-ring:nth-child(2) {
border-right-color: #2aa53a;
animation-delay: 0.3s;
width: 80%;
height: 80%;
top: 10%;
left: 10%;
}
.spinner-ring:nth-child(3) {
border-bottom-color: #1e7e34;
animation-delay: 0.6s;
width: 60%;
height: 60%;
top: 20%;
left: 20%;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 32rpx;
color: #ffffff;
font-weight: bold;
margin-bottom: 16rpx;
animation: pulse 1.5s ease-in-out infinite;
}
.loading-subtitle {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
font-weight: normal;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Vue过渡动画 */
.fade-enter-active, .fade-leave-active {
transition: all 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
transform: translateY(20rpx);
}
/* 骨架屏 */
.skeleton-container {
width: 100%;
margin-top: 40rpx;
}
.skeleton-item {
display: flex;
background: rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
margin-bottom: 20rpx;
padding: 20rpx;
backdrop-filter: blur(10px);
}
.skeleton-cover {
width: 180rpx;
height: 180rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%);
background-size: 200% 100%;
border-radius: 15rpx;
margin-right: 20rpx;
animation: shimmer 1.5s infinite;
}
.skeleton-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.skeleton-title {
height: 44rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%);
background-size: 200% 100%;
border-radius: 8rpx;
margin-bottom: 16rpx;
animation: shimmer 1.5s infinite;
animation-delay: 0.1s;
}
.skeleton-desc {
height: 68rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%);
background-size: 200% 100%;
border-radius: 8rpx;
margin-bottom: 16rpx;
animation: shimmer 1.5s infinite;
animation-delay: 0.2s;
}
.skeleton-meta {
display: flex;
gap: 16rpx;
}
.skeleton-meta-item {
height: 32rpx;
width: 120rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%);
background-size: 200% 100%;
border-radius: 6rpx;
animation: shimmer 1.5s infinite;
animation-delay: 0.3s;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* 超时状态 */
.timeout-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 80rpx 40rpx;
}
.timeout-icon {
font-size: 80rpx;
margin-bottom: 30rpx;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10rpx); }
60% { transform: translateY(-5rpx); }
}
.timeout-text {
font-size: 32rpx;
color: #ffffff;
font-weight: bold;
margin-bottom: 16rpx;
}
.timeout-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 40rpx;
line-height: 1.5;
}
.retry-btn {
background: linear-gradient(45deg, #42ca4d, #2aa53a);
color: #ffffff;
border: none;
border-radius: 50rpx;
padding: 24rpx 48rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 8rpx 24rpx rgba(66, 202, 77, 0.3);
transition: all 0.3s ease;
}
.retry-btn:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 12rpx rgba(66, 202, 77, 0.3);
}
/* 响应式优化 */
@media screen and (max-width: 750rpx) {
.loading-spinner {
width: 100rpx;
height: 100rpx;
}
.skeleton-item {
flex-direction: column;
}
.skeleton-cover {
width: 100%;
height: 200rpx;
margin-right: 0;
margin-bottom: 20rpx;
}
}
/* 无障碍访问优化 */
@media (prefers-reduced-motion: reduce) {
.spinner-ring,
.loading-text,
.skeleton-cover,
.skeleton-title,
.skeleton-desc,
.skeleton-meta-item,
.timeout-icon {
animation: none;
}
.fade-enter-active,
.fade-leave-active {
transition: none;
}
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 100rpx 40rpx;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
}
.empty-text {
display: block;
font-size: 32rpx;
color: #ffffff;
margin-bottom: 16rpx;
font-weight: bold;
}
.empty-desc {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 40rpx;
}
.create-btn {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: #ffffff;
border: none;
border-radius: 50rpx;
padding: 24rpx 48rpx;
font-size: 28rpx;
font-weight: bold;
}
/* 浮动按钮 */
.fab {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 107, 0.4);
z-index: 999;
}
.fab-icon {
width: 48rpx;
height: 48rpx;
}
</style>