ai-clone/frontend-ai/pages/my-works/my-works.vue
2026-03-05 14:29:21 +08:00

411 lines
8.3 KiB
Vue

<template>
<view class="works-container">
<!-- 分类标签 -->
<view class="tabs">
<view
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', activeTab === index ? 'active' : '']"
@click="activeTab = index"
>
{{ tab.name }}
</view>
</view>
<!-- 作品列表 -->
<scroll-view scroll-y class="works-list" @scrolltolower="loadMore">
<view v-if="loading" class="loading-box">
<text class="loading-text">⏳ 加载中...</text>
</view>
<view v-else-if="currentWorks.length === 0" class="empty-box">
<text class="empty-icon">📦</text>
<text class="empty-text">暂无作品</text>
<text class="empty-hint">快去创建你的第一个作品吧</text>
</view>
<view v-else class="works-grid">
<view
v-for="work in currentWorks"
:key="work.id"
class="work-card"
@click="viewWork(work)"
>
<view class="work-cover">
<image v-if="work.cover" :src="work.cover" mode="aspectFill" class="cover-img" />
<view v-else class="cover-placeholder">
<text class="placeholder-icon">{{ work.icon }}</text>
</view>
<view class="work-type">{{ work.typeName }}</view>
</view>
<view class="work-info">
<text class="work-title">{{ work.title }}</text>
<text class="work-time">{{ work.time }}</text>
</view>
<view class="work-actions">
<view class="action-btn" @click.stop="shareWork(work)">
<text class="action-icon">📤</text>
</view>
<view class="action-btn" @click.stop="deleteWork(work)">
<image src="/static/iconfont/delete.svg" class="delete-icon" mode="aspectFit"></image>
</view>
</view>
</view>
</view>
<view v-if="!hasMore && currentWorks.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
</view>
</template>
<script>
import { API_BASE, API_ENDPOINTS, buildURL } from '@/config/api.js';
export default {
data() {
return {
API_BASE,
activeTab: 0,
loading: false,
page: 0,
size: 20,
hasMore: true,
tabs: [
{ name: '全部', type: 'all' },
{ name: '声音克隆', type: 'voice' },
{ name: '照片复活', type: 'photo' },
{ name: '视频对话', type: 'video' }
],
allWorks: [
{
id: 1,
type: 'voice',
typeName: '声音克隆',
title: '妈妈的声音',
time: '2024-12-01 10:30',
icon: '🎤',
cover: ''
},
{
id: 2,
type: 'photo',
typeName: '照片复活',
title: '爷爷的照片',
time: '2024-11-28 15:20',
icon: '📸',
cover: ''
},
{
id: 3,
type: 'video',
typeName: '视频对话',
title: '与奶奶的对话',
time: '2024-11-25 09:15',
icon: '🎬',
cover: ''
}
]
};
},
computed: {
currentWorks() {
if (this.activeTab === 0) {
return this.allWorks;
}
const type = this.tabs[this.activeTab].type;
return this.allWorks.filter(work => work.type === type);
}
},
onLoad() {
this.loadWorks(true);
},
methods: {
loadMore() {
if (this.loading || !this.hasMore) return;
this.loadWorks(false);
},
loadWorks(reset) {
if (this.loading) return;
this.loading = true;
const userId = uni.getStorageSync('userId') || '';
const token = uni.getStorageSync('token') || '';
if (reset) {
this.page = 0;
this.hasMore = true;
this.allWorks = [];
}
uni.request({
url: `${this.API_BASE}/api/works?userId=${userId}&page=${this.page}&size=${this.size}`,
method: 'GET',
header: {
'X-User-Id': userId,
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
console.log('[Works] 用户ID:', userId);
console.log('[Works] API响应:', res.data);
if (res.data && res.data.content) {
const mapped = res.data.content.map(item => ({
id: item.id,
type: this.getWorkTypeKey(item.workType),
typeName: this.getWorkTypeName(item.workType),
title: item.title || '作品',
time: new Date(item.createdAt).toLocaleString('zh-CN'),
icon: this.getIconByWorkType(item.workType),
cover: item.coverUrl || ''
}));
this.allWorks = (this.allWorks || []).concat(mapped);
this.page += 1;
this.hasMore = !res.data.last;
}
},
fail: (err) => {
console.error('[Works] 加载失败:', err);
// 保留模拟数据作为后备
},
complete: () => {
this.loading = false;
}
});
},
// 获取作品类型键值
getWorkTypeKey(type) {
const typeMap = {
'VOICE_CLONE': 'voice',
'PHOTO_REVIVAL': 'photo',
'VIDEO_CALL': 'video'
};
return typeMap[type] || 'voice';
},
// 获取作品类型名称
getWorkTypeName(type) {
const nameMap = {
'VOICE_CLONE': '声音克隆',
'PHOTO_REVIVAL': '照片复活',
'VIDEO_CALL': '视频对话'
};
return nameMap[type] || '作品';
},
// 根据类型获取图标
getIconByWorkType(type) {
const icons = {
'VOICE_CLONE': '🎤',
'PHOTO_REVIVAL': '📸',
'VIDEO_CALL': '📞'
};
return icons[type] || '📦';
},
viewWork(work) {
uni.showToast({
title: '查看作品:' + work.title,
icon: 'none'
});
},
shareWork(work) {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '复制链接'],
success: (res) => {
uni.showToast({
title: '分享成功',
icon: 'success'
});
}
});
},
deleteWork(work) {
uni.showModal({
title: '确认删除',
content: `确定要删除作品"${work.title}"吗?`,
success: (res) => {
if (res.confirm) {
const index = this.allWorks.findIndex(w => w.id === work.id);
if (index > -1) {
this.allWorks.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
}
}
});
}
}
};
</script>
<style lang="scss" scoped>
.works-container {
min-height: 100vh;
background: #FDF8F2;
display: flex;
flex-direction: column;
}
/* 分类标签 */
.tabs {
display: flex;
background: white;
padding: 20upx 30upx;
box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.05);
}
.tab-item {
flex: 1;
text-align: center;
padding: 20upx 0;
font-size: 28upx;
color: #666;
border-radius: 20upx;
transition: all 0.3s;
}
.tab-item.active {
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
color: white;
font-weight: bold;
}
/* 作品列表 */
.works-list {
flex: 1;
padding: 30upx;
}
.loading-box,
.empty-box {
padding: 200upx 40upx;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-text {
font-size: 28upx;
color: #999;
}
.empty-icon {
font-size: 120upx;
margin-bottom: 30upx;
opacity: 0.5;
}
.empty-text {
font-size: 32upx;
color: #666;
margin-bottom: 16upx;
}
.empty-hint {
font-size: 26upx;
color: #999;
}
/* 作品网格 */
.works-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30upx;
}
.work-card {
background: white;
border-radius: 30upx;
overflow: hidden;
box-shadow: 0 8upx 30upx rgba(0, 0, 0, 0.08);
transition: all 0.3s;
&:active {
transform: translateY(-8upx);
box-shadow: 0 16upx 40upx rgba(0, 0, 0, 0.12);
}
}
.work-cover {
position: relative;
width: 100%;
height: 300upx;
background: linear-gradient(135deg, rgba(139, 115, 85, 0.1) 0%, rgba(109, 139, 139, 0.1) 100%);
.cover-img {
width: 100%;
height: 100%;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.placeholder-icon {
font-size: 100upx;
}
}
.work-type {
position: absolute;
top: 16upx;
right: 16upx;
padding: 8upx 20upx;
background: rgba(0, 0, 0, 0.6);
color: white;
font-size: 22upx;
border-radius: 20upx;
backdrop-filter: blur(10upx);
}
}
.work-info {
padding: 20upx;
.work-title {
display: block;
font-size: 28upx;
font-weight: bold;
color: #333;
margin-bottom: 12upx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.work-time {
display: block;
font-size: 22upx;
color: #999;
}
}
.work-actions {
display: flex;
border-top: 1upx solid #f0f0f0;
.action-btn {
flex: 1;
padding: 20upx;
text-align: center;
font-size: 32upx;
&:first-child {
border-right: 1upx solid #f0f0f0;
}
&:active {
background: #f5f5f5;
}
}
}
</style>