2026-03-05 14:29:21 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="plaza-container">
|
|
|
|
|
|
<view class="header">
|
|
|
|
|
|
<text class="title">作品广场</text>
|
|
|
|
|
|
<text class="subtitle">发现大家发布的作品</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<scroll-view scroll-y class="works-list" @scrolltolower="loadMore">
|
|
|
|
|
|
<view v-if="loading && works.length === 0" class="works-grid skeleton-grid">
|
|
|
|
|
|
<view v-for="n in 6" :key="n" class="work-card skeleton-card">
|
|
|
|
|
|
<view class="work-cover skeleton-cover"></view>
|
|
|
|
|
|
<view class="work-info">
|
|
|
|
|
|
<view class="skeleton-line skeleton-title"></view>
|
|
|
|
|
|
<view class="skeleton-line skeleton-sub"></view>
|
|
|
|
|
|
<view class="skeleton-line skeleton-sub short"></view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-else-if="!loading && works.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 works"
|
|
|
|
|
|
:key="work.id"
|
|
|
|
|
|
class="work-card"
|
|
|
|
|
|
@click="viewWork(work)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="work-cover">
|
|
|
|
|
|
<image v-if="work.coverUrl" :src="work.coverUrl" mode="aspectFill" class="cover-img" />
|
|
|
|
|
|
<view v-else class="cover-placeholder">
|
|
|
|
|
|
<text class="placeholder-icon">🎬</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="work-type">{{ work.workTypeLabel }}</view>
|
2026-03-06 18:05:51 +08:00
|
|
|
|
<!-- AI生成提示标签 -->
|
|
|
|
|
|
<view class="ai-tag">
|
|
|
|
|
|
<text class="ai-tag-text">AI生成</text>
|
|
|
|
|
|
</view>
|
2026-03-05 14:29:21 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="work-info">
|
|
|
|
|
|
<text class="work-title">{{ work.title }}</text>
|
|
|
|
|
|
<text class="work-author">{{ work.userNickname || '匿名' }}</text>
|
|
|
|
|
|
<text class="work-time">{{ formatTime(work.publishedAt || work.createdAt) }}</text>
|
|
|
|
|
|
<view class="work-actions" @click.stop>
|
|
|
|
|
|
<view class="like-btn" :class="{ liked: work.liked }" @click.stop="toggleLike(work)">
|
|
|
|
|
|
<text class="like-icon">{{ work.liked ? '❤️' : '🤍' }}</text>
|
|
|
|
|
|
<text class="like-count">{{ work.likeCount || 0 }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-if="loading && works.length > 0" class="loading-more">
|
|
|
|
|
|
<text class="loading-more-text">⏳ 加载更多...</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="!hasMore && works.length > 0" class="no-more">
|
|
|
|
|
|
<text class="no-more-text">没有更多了</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import { API_BASE, API_ENDPOINTS } from '@/config/api.js';
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
API_BASE,
|
|
|
|
|
|
works: [],
|
|
|
|
|
|
page: 0,
|
|
|
|
|
|
size: 10,
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
hasMore: true,
|
|
|
|
|
|
loadedOnce: false,
|
|
|
|
|
|
hasCachedWorks: false,
|
|
|
|
|
|
enableVideoPreload: false,
|
|
|
|
|
|
preloadQueue: [],
|
|
|
|
|
|
activePreloads: 0,
|
|
|
|
|
|
maxConcurrentPreloads: 2,
|
|
|
|
|
|
preloadingKeys: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const cached = uni.getStorageSync('plaza_works_cache_v1');
|
|
|
|
|
|
if (Array.isArray(cached) && cached.length > 0) {
|
|
|
|
|
|
this.works = cached;
|
|
|
|
|
|
this.hasCachedWorks = true;
|
|
|
|
|
|
this.loadedOnce = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.hasCachedWorks = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loadWorks(true);
|
|
|
|
|
|
},
|
|
|
|
|
|
onShow() {
|
|
|
|
|
|
if (!this.loadedOnce || this.works.length === 0) {
|
|
|
|
|
|
this.loadWorks(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
normalizeMediaUrl(url) {
|
|
|
|
|
|
if (!url) return url;
|
|
|
|
|
|
if (url.startsWith('/')) {
|
|
|
|
|
|
return `${this.API_BASE}${url}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (url.startsWith('http://115.190.167.176:20002')) {
|
|
|
|
|
|
return url.replace('http://115.190.167.176:20002', this.API_BASE);
|
|
|
|
|
|
}
|
|
|
|
|
|
return url;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
generateCacheKey(url) {
|
|
|
|
|
|
let hash = 0;
|
|
|
|
|
|
for (let i = 0; i < url.length; i++) {
|
|
|
|
|
|
const char = url.charCodeAt(i);
|
|
|
|
|
|
hash = ((hash << 5) - hash) + char;
|
|
|
|
|
|
hash = hash & hash;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'video_' + Math.abs(hash);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
enqueuePreload(url) {
|
|
|
|
|
|
if (!this.enableVideoPreload) return;
|
|
|
|
|
|
if (!url) return;
|
|
|
|
|
|
const cacheKey = this.generateCacheKey(url);
|
|
|
|
|
|
if (this.preloadingKeys[cacheKey]) return;
|
|
|
|
|
|
const cachedPath = uni.getStorageSync(cacheKey);
|
|
|
|
|
|
if (cachedPath) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.preloadingKeys[cacheKey] = true;
|
|
|
|
|
|
this.preloadQueue.push({ url, cacheKey });
|
|
|
|
|
|
this.processPreloadQueue();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
processPreloadQueue() {
|
|
|
|
|
|
while (this.activePreloads < this.maxConcurrentPreloads && this.preloadQueue.length > 0) {
|
|
|
|
|
|
const item = this.preloadQueue.shift();
|
|
|
|
|
|
this.activePreloads += 1;
|
|
|
|
|
|
this.preloadVideoToCache(item.url, item.cacheKey)
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.activePreloads = Math.max(0, this.activePreloads - 1);
|
|
|
|
|
|
this.processPreloadQueue();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
preloadVideoToCache(url, cacheKey) {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
uni.downloadFile({
|
|
|
|
|
|
url,
|
|
|
|
|
|
timeout: 60000,
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.statusCode !== 200) {
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uni.saveFile({
|
|
|
|
|
|
tempFilePath: res.tempFilePath,
|
|
|
|
|
|
success: (saveRes) => {
|
|
|
|
|
|
const savedPath = saveRes.savedFilePath;
|
|
|
|
|
|
uni.setStorageSync(cacheKey, savedPath);
|
|
|
|
|
|
const cacheInfo = uni.getStorageSync('video_cache_info') || {};
|
|
|
|
|
|
cacheInfo[cacheKey] = {
|
|
|
|
|
|
path: savedPath,
|
|
|
|
|
|
url,
|
|
|
|
|
|
time: Date.now()
|
|
|
|
|
|
};
|
|
|
|
|
|
uni.setStorageSync('video_cache_info', cacheInfo);
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
},
|
|
|
|
|
|
complete: () => {
|
|
|
|
|
|
if (this.preloadingKeys && cacheKey) {
|
|
|
|
|
|
this.preloadingKeys[cacheKey] = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
preloadWorks(works) {
|
|
|
|
|
|
if (!Array.isArray(works) || works.length === 0) return;
|
|
|
|
|
|
uni.getNetworkType({
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (!res || res.networkType !== 'wifi') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
works.forEach((w) => {
|
|
|
|
|
|
if (w && w.contentUrl) {
|
|
|
|
|
|
this.enqueuePreload(w.contentUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getWorkTypeLabel(type) {
|
|
|
|
|
|
const map = {
|
|
|
|
|
|
VOICE_CLONE: '声音克隆',
|
|
|
|
|
|
PHOTO_REVIVAL: '照片复活',
|
|
|
|
|
|
VIDEO_CALL: '视频对话'
|
|
|
|
|
|
};
|
|
|
|
|
|
return map[type] || '作品';
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
toggleLike(work) {
|
|
|
|
|
|
const token = uni.getStorageSync('token') || '';
|
|
|
|
|
|
const userId = uni.getStorageSync('userId') || '';
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
|
uni.showToast({ title: '请先登录', icon: 'none' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uni.request({
|
|
|
|
|
|
url: `${this.API_BASE}/api/works/${work.id}/like`,
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
header: {
|
|
|
|
|
|
'X-User-Id': userId,
|
|
|
|
|
|
'Authorization': token ? `Bearer ${token}` : ''
|
|
|
|
|
|
},
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.statusCode === 200 && res.data && res.data.success && res.data.data) {
|
|
|
|
|
|
work.liked = !!res.data.data.liked;
|
|
|
|
|
|
work.likeCount = res.data.data.likeCount || 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uni.showToast({ title: res.data?.message || '操作失败', icon: 'none' });
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
uni.showToast({ title: '操作失败', icon: 'none' });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
formatTime(value) {
|
|
|
|
|
|
if (!value) return '';
|
|
|
|
|
|
const d = new Date(value);
|
|
|
|
|
|
if (Number.isNaN(d.getTime())) return String(value);
|
|
|
|
|
|
const pad = (n) => (n < 10 ? `0${n}` : `${n}`);
|
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
loadMore() {
|
|
|
|
|
|
if (this.loading || !this.hasMore) return;
|
|
|
|
|
|
this.loadWorks(false);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
loadWorks(reset) {
|
|
|
|
|
|
if (this.loading) return;
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
|
|
|
|
|
|
const token = uni.getStorageSync('token') || '';
|
|
|
|
|
|
const userId = uni.getStorageSync('userId') || '';
|
|
|
|
|
|
|
|
|
|
|
|
if (reset) {
|
|
|
|
|
|
this.page = 0;
|
|
|
|
|
|
this.hasMore = true;
|
|
|
|
|
|
if (!this.hasCachedWorks) {
|
|
|
|
|
|
this.works = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uni.request({
|
|
|
|
|
|
url: `${this.API_BASE}${API_ENDPOINTS.works.plaza}?page=${this.page}&size=${this.size}`,
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
header: {
|
|
|
|
|
|
'X-User-Id': userId,
|
|
|
|
|
|
'Authorization': token ? `Bearer ${token}` : ''
|
|
|
|
|
|
},
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.statusCode === 200 && res.data) {
|
|
|
|
|
|
const content = res.data.content || [];
|
|
|
|
|
|
const mapped = content.map((w) => ({
|
|
|
|
|
|
...w,
|
|
|
|
|
|
coverUrl: this.normalizeMediaUrl(w.coverUrl),
|
|
|
|
|
|
contentUrl: this.normalizeMediaUrl(w.contentUrl),
|
|
|
|
|
|
audioUrl: this.normalizeMediaUrl(w.audioUrl),
|
|
|
|
|
|
workTypeLabel: this.getWorkTypeLabel(w.workType),
|
|
|
|
|
|
likeCount: w.likeCount || 0,
|
|
|
|
|
|
liked: !!w.liked
|
|
|
|
|
|
}));
|
|
|
|
|
|
if (reset) {
|
|
|
|
|
|
this.works = mapped;
|
|
|
|
|
|
this.hasCachedWorks = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
uni.setStorageSync('plaza_works_cache_v1', mapped);
|
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.works = this.works.concat(mapped);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.page += 1;
|
|
|
|
|
|
this.hasMore = !res.data.last;
|
|
|
|
|
|
this.loadedOnce = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: `加载失败(${res.statusCode})`,
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '加载失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
complete: () => {
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
viewWork(work) {
|
|
|
|
|
|
if (!work || (!work.contentUrl && !work.editedVideoUrl && !work.edited_video_url)) {
|
|
|
|
|
|
uni.showToast({ title: '作品不可用', icon: 'none' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const videoUrl = work.editedVideoUrl || work.edited_video_url || work.contentUrl;
|
|
|
|
|
|
let url = `/pages/video-player/video-player?audioInVideo=1&url=${encodeURIComponent(videoUrl)}&title=${encodeURIComponent(work.title || '作品')}`;
|
|
|
|
|
|
uni.navigateTo({ url });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.plaza-container {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background: #FDF8F2;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
padding: 32upx 32upx 20upx 40upx;
|
|
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
|
font-size: 44upx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #8B7355;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 8upx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
|
font-size: 24upx;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.works-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 0 24upx 40upx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 16upx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-grid {
|
|
|
|
|
|
padding-top: 16upx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-card {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-cover {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding-top: 150%;
|
|
|
|
|
|
background: #f1f1f1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-line {
|
|
|
|
|
|
height: 22upx;
|
|
|
|
|
|
background: #f1f1f1;
|
|
|
|
|
|
border-radius: 10upx;
|
|
|
|
|
|
margin-top: 12upx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-title {
|
|
|
|
|
|
height: 26upx;
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-sub {
|
|
|
|
|
|
height: 20upx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-sub.short {
|
|
|
|
|
|
width: 60%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-card {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 16upx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: 0 4upx 12upx rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
transform: translateY(-4upx);
|
|
|
|
|
|
box-shadow: 0 8upx 20upx rgba(0, 0, 0, 0.12);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-cover {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding-top: 150%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #F5F5F5;
|
|
|
|
|
|
|
|
|
|
|
|
.cover-img {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cover-placeholder {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder-icon {
|
|
|
|
|
|
font-size: 60upx;
|
|
|
|
|
|
color: #ccc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-type {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 12upx;
|
|
|
|
|
|
right: 12upx;
|
|
|
|
|
|
padding: 6upx 16upx;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 20upx;
|
|
|
|
|
|
border-radius: 20upx;
|
|
|
|
|
|
}
|
2026-03-06 18:05:51 +08:00
|
|
|
|
|
|
|
|
|
|
/* AI生成提示标签 */
|
|
|
|
|
|
.ai-tag {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 12upx;
|
|
|
|
|
|
left: 12upx;
|
|
|
|
|
|
padding: 6upx 16upx;
|
|
|
|
|
|
background: rgba(255, 165, 0, 0.85);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 20upx;
|
|
|
|
|
|
border-radius: 20upx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
2026-03-05 14:29:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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-author {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 22upx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-bottom: 10upx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-time {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 22upx;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-actions {
|
|
|
|
|
|
margin-top: 14upx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.like-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10upx;
|
|
|
|
|
|
padding: 10upx 16upx;
|
|
|
|
|
|
border-radius: 999upx;
|
|
|
|
|
|
background: rgba(139, 115, 85, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.like-btn.liked {
|
|
|
|
|
|
background: rgba(220, 60, 90, 0.10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.like-icon {
|
|
|
|
|
|
font-size: 26upx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.like-count {
|
|
|
|
|
|
font-size: 22upx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-more-text,
|
|
|
|
|
|
.no-more-text {
|
|
|
|
|
|
font-size: 24upx;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|