411 lines
8.3 KiB
Vue
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>
|