2026-03-27 22:20:25 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="work-detail-page">
|
|
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<template v-if="work">
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<!-- 顶部 -->
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<div class="detail-header">
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<button class="back-btn" @click="$router.back()">
|
|
|
|
|
|
<left-outlined />
|
|
|
|
|
|
</button>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<h1>{{ work.title }}</h1>
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<span :class="['status-tag', work.status]">{{ statusTextMap[work.status] }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 拒绝原因(仅作者 + rejected)-->
|
|
|
|
|
|
<div v-if="isOwner && work.status === 'rejected' && work.reviewNote" class="reject-card">
|
|
|
|
|
|
<warning-filled class="reject-icon" />
|
|
|
|
|
|
<div class="reject-body">
|
|
|
|
|
|
<div class="reject-title">未通过审核</div>
|
|
|
|
|
|
<div class="reject-content">{{ work.reviewNote }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 草稿提示(仅作者 + draft)-->
|
|
|
|
|
|
<div v-else-if="isOwner && work.status === 'draft'" class="info-card draft-card">
|
|
|
|
|
|
<info-circle-outlined class="info-icon" />
|
|
|
|
|
|
<div class="info-body">
|
|
|
|
|
|
<div class="info-title">这是一个未完成的草稿</div>
|
|
|
|
|
|
<div class="info-desc">继续完成创作后才能公开发布</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 未发布提示(仅作者 + unpublished)-->
|
|
|
|
|
|
<div v-else-if="isOwner && work.status === 'unpublished'" class="info-card unpublished-card">
|
|
|
|
|
|
<info-circle-outlined class="info-icon" />
|
|
|
|
|
|
<div class="info-body">
|
|
|
|
|
|
<div class="info-title">作品仅你自己可见</div>
|
|
|
|
|
|
<div class="info-desc">点击下方「公开发布」提交审核,通过后将在「发现」页展示</div>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 绘本阅读器 -->
|
|
|
|
|
|
<div class="book-reader" v-if="work.pages && work.pages.length > 0">
|
|
|
|
|
|
<div class="page-display">
|
|
|
|
|
|
<img v-if="currentPageData?.imageUrl" :src="currentPageData.imageUrl" :alt="`第${currentPageIndex + 1}页`" class="page-image" />
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<div v-else class="page-placeholder">
|
|
|
|
|
|
<picture-outlined />
|
|
|
|
|
|
</div>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="page-text" v-if="currentPageData?.text">
|
2026-04-09 18:48:14 +08:00
|
|
|
|
{{ currentPageData.text }}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="page-audio" v-if="currentPageData?.audioUrl">
|
|
|
|
|
|
<audio :src="currentPageData.audioUrl" controls class="audio-player"></audio>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="page-nav">
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<button class="nav-btn" :disabled="currentPageIndex === 0" @click="prevPage">
|
|
|
|
|
|
<left-outlined />
|
|
|
|
|
|
</button>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<span class="page-indicator">{{ currentPageIndex + 1 }} / {{ work.pages.length }}</span>
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<button class="nav-btn" :disabled="currentPageIndex === work.pages.length - 1" @click="nextPage">
|
|
|
|
|
|
<right-outlined />
|
|
|
|
|
|
</button>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-09 19:29:31 +08:00
|
|
|
|
<!-- 画作原图 -->
|
|
|
|
|
|
<div v-if="work.originalImageUrl" class="original-card">
|
|
|
|
|
|
<div class="original-thumb" @click="previewOriginal = work.originalImageUrl || ''">
|
|
|
|
|
|
<img :src="work.originalImageUrl" alt="画作原图" />
|
|
|
|
|
|
<div class="zoom-hint"><zoom-in-outlined /></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="original-text">
|
|
|
|
|
|
<div class="original-title">画作原图</div>
|
|
|
|
|
|
<div class="original-desc">AI 根据这张画作生成的绘本</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<!-- 作品信息 -->
|
|
|
|
|
|
<div class="info-section">
|
|
|
|
|
|
<div class="author-row">
|
|
|
|
|
|
<a-avatar :size="36" :src="work.creator?.avatar">
|
|
|
|
|
|
{{ work.creator?.nickname?.charAt(0) }}
|
|
|
|
|
|
</a-avatar>
|
|
|
|
|
|
<div class="author-info">
|
|
|
|
|
|
<span class="author-name">{{ work.creator?.nickname }}</span>
|
|
|
|
|
|
<span class="create-time">{{ formatDate(work.createTime) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="work.description" class="description">{{ work.description }}</div>
|
|
|
|
|
|
<div v-if="work.tags?.length" class="tags-row">
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<span v-for="t in work.tags" :key="t.tag.id" class="info-tag">{{ t.tag.name }}</span>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
2026-03-31 13:56:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<!-- 互动栏:仅在已发布作品上显示 -->
|
|
|
|
|
|
<div v-if="work.status === 'published'" class="interaction-bar">
|
|
|
|
|
|
<div :class="['action-btn', { active: interaction.liked }]" @click="handleLike">
|
2026-03-31 13:56:20 +08:00
|
|
|
|
<heart-filled v-if="interaction.liked" />
|
|
|
|
|
|
<heart-outlined v-else />
|
|
|
|
|
|
<span>{{ displayLikeCount }}</span>
|
|
|
|
|
|
</div>
|
2026-04-09 18:48:14 +08:00
|
|
|
|
<div :class="['action-btn', { active: interaction.favorited }]" @click="handleFavorite">
|
2026-03-31 13:56:20 +08:00
|
|
|
|
<star-filled v-if="interaction.favorited" />
|
|
|
|
|
|
<star-outlined v-else />
|
|
|
|
|
|
<span>{{ displayFavoriteCount }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="action-btn">
|
|
|
|
|
|
<eye-outlined />
|
|
|
|
|
|
<span>{{ work.viewCount || 0 }}</span>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-09 18:48:14 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 作者私有操作 -->
|
|
|
|
|
|
<div v-if="isOwner" class="owner-actions">
|
|
|
|
|
|
<!-- 主操作:根据 status 切换 -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="work.status === 'unpublished'"
|
|
|
|
|
|
class="op-btn primary"
|
|
|
|
|
|
:disabled="actionLoading"
|
|
|
|
|
|
@click="handlePublish"
|
|
|
|
|
|
>
|
|
|
|
|
|
<send-outlined />
|
|
|
|
|
|
<span>公开发布</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="work.status === 'rejected'"
|
|
|
|
|
|
class="op-btn primary"
|
|
|
|
|
|
:disabled="actionLoading"
|
|
|
|
|
|
@click="handleResubmit"
|
|
|
|
|
|
>
|
|
|
|
|
|
<send-outlined />
|
|
|
|
|
|
<span>修改后重交</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="work.status === 'draft'"
|
|
|
|
|
|
class="op-btn primary"
|
|
|
|
|
|
@click="handleContinue"
|
|
|
|
|
|
>
|
|
|
|
|
|
<edit-outlined />
|
|
|
|
|
|
<span>继续创作</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="work.status === 'pending_review'"
|
|
|
|
|
|
class="op-btn outline"
|
|
|
|
|
|
:disabled="actionLoading"
|
|
|
|
|
|
@click="handleWithdraw"
|
|
|
|
|
|
>
|
|
|
|
|
|
<undo-outlined />
|
|
|
|
|
|
<span>撤回审核</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="work.status === 'published'"
|
|
|
|
|
|
class="op-btn outline"
|
|
|
|
|
|
:disabled="actionLoading"
|
|
|
|
|
|
@click="handleUnpublish"
|
|
|
|
|
|
>
|
|
|
|
|
|
<inbox-outlined />
|
|
|
|
|
|
<span>下架</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 编辑信息(unpublished 状态)-->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="work.status === 'unpublished'"
|
|
|
|
|
|
class="op-btn outline-soft"
|
|
|
|
|
|
@click="handleEditInfo"
|
|
|
|
|
|
>
|
|
|
|
|
|
<edit-outlined />
|
|
|
|
|
|
<span>编辑信息</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 删除(所有状态)-->
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="op-btn ghost-danger"
|
|
|
|
|
|
:disabled="actionLoading"
|
|
|
|
|
|
@click="handleDelete"
|
|
|
|
|
|
>
|
|
|
|
|
|
<delete-outlined />
|
|
|
|
|
|
<span>删除</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<a-empty v-else-if="!loading" description="作品不存在" style="padding: 80px 0" />
|
|
|
|
|
|
</a-spin>
|
2026-04-09 18:48:14 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 二次确认弹窗 -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="confirmVisible"
|
|
|
|
|
|
:title="confirmTitle"
|
|
|
|
|
|
:ok-text="confirmOkText"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
:confirm-loading="actionLoading"
|
|
|
|
|
|
@ok="handleConfirmOk"
|
|
|
|
|
|
@cancel="handleConfirmCancel"
|
|
|
|
|
|
>
|
|
|
|
|
|
<p>{{ confirmContent }}</p>
|
|
|
|
|
|
</a-modal>
|
2026-04-09 19:29:31 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 原图全屏预览 -->
|
|
|
|
|
|
<Transition name="fade">
|
|
|
|
|
|
<div v-if="previewOriginal" class="preview-overlay" @click="previewOriginal = ''">
|
|
|
|
|
|
<img :src="previewOriginal" class="preview-full-img" alt="画作原图" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
2026-03-31 13:56:20 +08:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-03-27 22:20:25 +08:00
|
|
|
|
import { message } from 'ant-design-vue'
|
2026-03-31 13:56:20 +08:00
|
|
|
|
import {
|
2026-04-09 18:48:14 +08:00
|
|
|
|
LeftOutlined, RightOutlined,
|
|
|
|
|
|
HeartOutlined, HeartFilled,
|
|
|
|
|
|
StarOutlined, StarFilled,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
PictureOutlined,
|
|
|
|
|
|
WarningFilled,
|
|
|
|
|
|
InfoCircleOutlined,
|
|
|
|
|
|
SendOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
UndoOutlined,
|
|
|
|
|
|
InboxOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
2026-04-09 19:29:31 +08:00
|
|
|
|
ZoomInOutlined,
|
2026-03-31 13:56:20 +08:00
|
|
|
|
} from '@ant-design/icons-vue'
|
2026-04-09 18:48:14 +08:00
|
|
|
|
import {
|
|
|
|
|
|
publicUserWorksApi,
|
|
|
|
|
|
publicGalleryApi,
|
|
|
|
|
|
publicInteractionApi,
|
|
|
|
|
|
type UserWork,
|
|
|
|
|
|
} from '@/api/public'
|
|
|
|
|
|
import { getMockWorkDetail, isMockWorkId } from './_dev-mock'
|
2026-03-27 22:20:25 +08:00
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
2026-03-31 13:56:20 +08:00
|
|
|
|
const router = useRouter()
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const workId = Number(route.params.id)
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
const isDev = import.meta.env.DEV
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const work = ref<UserWork | null>(null)
|
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const currentPageIndex = ref(0)
|
2026-03-31 13:56:20 +08:00
|
|
|
|
const interaction = ref({ liked: false, favorited: false })
|
|
|
|
|
|
const actionLoading = ref(false)
|
2026-04-09 19:29:31 +08:00
|
|
|
|
const previewOriginal = ref('')
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
|
|
|
|
|
const currentPageData = computed(() => work.value?.pages?.[currentPageIndex.value] || null)
|
|
|
|
|
|
|
2026-03-31 13:56:20 +08:00
|
|
|
|
const isLoggedIn = computed(() => !!localStorage.getItem('public_token'))
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const isOwner = computed(() => {
|
2026-04-09 18:48:14 +08:00
|
|
|
|
// dev mock 模式:mock 作品默认是当前用户作品
|
|
|
|
|
|
if (isDev && work.value && isMockWorkId(work.value.id)) return true
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const u = localStorage.getItem('public_user')
|
|
|
|
|
|
if (!u || !work.value) return false
|
|
|
|
|
|
try { return JSON.parse(u).id === work.value.userId } catch { return false }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-31 13:56:20 +08:00
|
|
|
|
const displayLikeCount = computed(() => work.value?.likeCount || 0)
|
|
|
|
|
|
const displayFavoriteCount = computed(() => work.value?.favoriteCount || 0)
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const statusTextMap: Record<string, string> = {
|
2026-04-09 18:48:14 +08:00
|
|
|
|
draft: '草稿',
|
|
|
|
|
|
unpublished: '未发布',
|
|
|
|
|
|
pending_review: '审核中',
|
|
|
|
|
|
published: '已发布',
|
|
|
|
|
|
rejected: '被拒绝',
|
|
|
|
|
|
taken_down: '已下架',
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
|
|
|
|
|
|
|
|
|
|
|
|
const prevPage = () => { if (currentPageIndex.value > 0) currentPageIndex.value-- }
|
|
|
|
|
|
const nextPage = () => { if (work.value?.pages && currentPageIndex.value < work.value.pages.length - 1) currentPageIndex.value++ }
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
// ─── 互动 ───
|
2026-03-31 13:56:20 +08:00
|
|
|
|
const handleLike = async () => {
|
|
|
|
|
|
if (!isLoggedIn.value) { router.push('/p/login'); return }
|
|
|
|
|
|
if (actionLoading.value) return
|
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
|
const wasLiked = interaction.value.liked
|
|
|
|
|
|
interaction.value.liked = !wasLiked
|
|
|
|
|
|
if (work.value) work.value.likeCount = (work.value.likeCount || 0) + (wasLiked ? -1 : 1)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await publicInteractionApi.like(workId)
|
|
|
|
|
|
interaction.value.liked = res.liked
|
|
|
|
|
|
if (work.value) work.value.likeCount = res.likeCount
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
interaction.value.liked = wasLiked
|
|
|
|
|
|
if (work.value) work.value.likeCount = (work.value.likeCount || 0) + (wasLiked ? 1 : -1)
|
|
|
|
|
|
message.error('操作失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleFavorite = async () => {
|
|
|
|
|
|
if (!isLoggedIn.value) { router.push('/p/login'); return }
|
|
|
|
|
|
if (actionLoading.value) return
|
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
|
const wasFavorited = interaction.value.favorited
|
|
|
|
|
|
interaction.value.favorited = !wasFavorited
|
|
|
|
|
|
if (work.value) work.value.favoriteCount = (work.value.favoriteCount || 0) + (wasFavorited ? -1 : 1)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await publicInteractionApi.favorite(workId)
|
|
|
|
|
|
interaction.value.favorited = res.favorited
|
|
|
|
|
|
if (work.value) work.value.favoriteCount = res.favoriteCount
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
interaction.value.favorited = wasFavorited
|
|
|
|
|
|
if (work.value) work.value.favoriteCount = (work.value.favoriteCount || 0) + (wasFavorited ? 1 : -1)
|
|
|
|
|
|
message.error('操作失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
// ─── 作者操作 ───
|
|
|
|
|
|
|
|
|
|
|
|
const isMock = computed(() => isDev && work.value && isMockWorkId(work.value.id))
|
|
|
|
|
|
|
|
|
|
|
|
/** 公开发布:unpublished → pending_review */
|
|
|
|
|
|
async function handlePublish() {
|
|
|
|
|
|
if (!work.value) return
|
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isMock.value) {
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 300))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await publicUserWorksApi.publish(workId)
|
|
|
|
|
|
}
|
|
|
|
|
|
work.value.status = 'pending_review'
|
|
|
|
|
|
message.success('已提交审核,等待超管确认')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e.message || '发布失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 修改后重交:rejected → 跳到编辑信息页 */
|
|
|
|
|
|
function handleResubmit() {
|
|
|
|
|
|
// TODO: 真实场景需要 leai workId 跳到 EditInfoView,等后端 work.leaiWorkId 字段确认后接入
|
|
|
|
|
|
message.info('编辑功能待后端联调,dev 模式暂无法跳转')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 继续创作:draft → 跳回创作流程 */
|
|
|
|
|
|
function handleContinue() {
|
|
|
|
|
|
router.push('/p/create')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 撤回审核:pending_review → unpublished */
|
|
|
|
|
|
function handleWithdraw() {
|
|
|
|
|
|
showConfirm(
|
|
|
|
|
|
'撤回审核',
|
|
|
|
|
|
'撤回后作品将回到「未发布」状态,可继续编辑或重新提交审核',
|
|
|
|
|
|
'确认撤回',
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
if (!work.value) return
|
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isMock.value) {
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 300))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// TODO: 后端需要新增 POST /public/works/{id}/withdraw 接口
|
|
|
|
|
|
message.warning('撤回接口待后端联调')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
work.value.status = 'unpublished'
|
|
|
|
|
|
message.success('已撤回审核')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e.message || '撤回失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 下架作品:published → unpublished */
|
|
|
|
|
|
function handleUnpublish() {
|
|
|
|
|
|
showConfirm(
|
|
|
|
|
|
'下架作品',
|
|
|
|
|
|
'下架后作品将从「发现」页移除,回到「未发布」状态。下架后仍可重新提交审核',
|
|
|
|
|
|
'确认下架',
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
if (!work.value) return
|
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isMock.value) {
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 300))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// TODO: 后端需要新增 POST /public/works/{id}/unpublish 接口
|
|
|
|
|
|
message.warning('下架接口待后端联调')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
work.value.status = 'unpublished'
|
|
|
|
|
|
message.success('已下架到「未发布」')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e.message || '下架失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 编辑信息:跳到 EditInfoView */
|
|
|
|
|
|
function handleEditInfo() {
|
|
|
|
|
|
// TODO: 真实场景需要 work.leaiWorkId 字段,等后端确认后接入
|
|
|
|
|
|
message.info('编辑信息功能待后端联调')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 删除作品 */
|
|
|
|
|
|
function handleDelete() {
|
|
|
|
|
|
showConfirm(
|
|
|
|
|
|
'删除作品',
|
|
|
|
|
|
'删除后无法恢复,确认要删除这个作品吗?',
|
|
|
|
|
|
'确认删除',
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isMock.value) {
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 300))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await publicUserWorksApi.delete(workId)
|
|
|
|
|
|
}
|
|
|
|
|
|
message.success('已删除')
|
|
|
|
|
|
router.push('/p/works')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e.message || '删除失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── 二次确认弹窗 ───
|
|
|
|
|
|
const confirmVisible = ref(false)
|
|
|
|
|
|
const confirmTitle = ref('')
|
|
|
|
|
|
const confirmContent = ref('')
|
|
|
|
|
|
const confirmOkText = ref('确认')
|
|
|
|
|
|
let confirmHandler: (() => Promise<void>) | null = null
|
|
|
|
|
|
|
|
|
|
|
|
function showConfirm(title: string, content: string, okText: string, handler: () => Promise<void>) {
|
|
|
|
|
|
confirmTitle.value = title
|
|
|
|
|
|
confirmContent.value = content
|
|
|
|
|
|
confirmOkText.value = okText
|
|
|
|
|
|
confirmHandler = handler
|
|
|
|
|
|
confirmVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
async function handleConfirmOk() {
|
|
|
|
|
|
if (confirmHandler) {
|
|
|
|
|
|
await confirmHandler()
|
|
|
|
|
|
confirmHandler = null
|
|
|
|
|
|
}
|
|
|
|
|
|
confirmVisible.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
function handleConfirmCancel() {
|
|
|
|
|
|
confirmVisible.value = false
|
|
|
|
|
|
confirmHandler = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── 加载作品 ───
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const fetchWork = async () => {
|
|
|
|
|
|
loading.value = true
|
2026-04-09 18:48:14 +08:00
|
|
|
|
|
|
|
|
|
|
// dev 兜底:mock id 直接用 mock 数据
|
|
|
|
|
|
if (isDev && isMockWorkId(workId)) {
|
|
|
|
|
|
const mock = getMockWorkDetail(workId)
|
|
|
|
|
|
if (mock) {
|
|
|
|
|
|
work.value = mock
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
try {
|
2026-03-31 13:56:20 +08:00
|
|
|
|
// 优先尝试广场接口(公开作品),失败再尝试自己作品库
|
|
|
|
|
|
try {
|
|
|
|
|
|
work.value = await publicGalleryApi.detail(workId)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
work.value = await publicUserWorksApi.detail(workId)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isLoggedIn.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
interaction.value = await publicInteractionApi.getInteraction(workId)
|
|
|
|
|
|
} catch { /* 忽略 */ }
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
} catch {
|
2026-04-09 18:48:14 +08:00
|
|
|
|
// dev 兜底:真实接口失败时尝试 mock 数据
|
|
|
|
|
|
if (isDev) {
|
|
|
|
|
|
const mock = getMockWorkDetail(workId) || getMockWorkDetail(101)
|
|
|
|
|
|
if (mock) {
|
|
|
|
|
|
work.value = mock
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('获取作品详情失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('获取作品详情失败')
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(fetchWork)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
$primary: #6366f1;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
$accent: #ec4899;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
|
|
|
|
|
.work-detail-page {
|
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
|
margin: 0 auto;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
padding-bottom: 24px;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
/* ---------- 顶部 ---------- */
|
2026-03-27 22:20:25 +08:00
|
|
|
|
.detail-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
gap: 10px;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
h1 {
|
|
|
|
|
|
font-size: 17px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #1e1b4b;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.back-btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: rgba($primary, 0.08);
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.anticon) { font-size: 15px; }
|
|
|
|
|
|
|
|
|
|
|
|
&:hover { background: rgba($primary, 0.14); }
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-tag {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 3px 10px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
letter-spacing: 0.3px;
|
|
|
|
|
|
|
|
|
|
|
|
&.draft { background: rgba(107, 114, 128, 0.85); }
|
|
|
|
|
|
&.unpublished { background: rgba(99, 102, 241, 0.9); }
|
|
|
|
|
|
&.pending_review { background: rgba(245, 158, 11, 0.92); }
|
|
|
|
|
|
&.published { background: rgba(16, 185, 129, 0.92); }
|
|
|
|
|
|
&.rejected { background: rgba(239, 68, 68, 0.92); }
|
|
|
|
|
|
&.taken_down { background: rgba(107, 114, 128, 0.85); }
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
/* ---------- 拒绝原因 / 信息提示卡片 ---------- */
|
|
|
|
|
|
.reject-card,
|
|
|
|
|
|
.info-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.reject-card {
|
|
|
|
|
|
background: rgba(239, 68, 68, 0.06);
|
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
.reject-icon {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.reject-body { flex: 1; }
|
|
|
|
|
|
.reject-title { font-size: 13px; font-weight: 700; color: #b91c1c; margin-bottom: 4px; }
|
|
|
|
|
|
.reject-content { font-size: 13px; color: #4b5563; line-height: 1.6; }
|
|
|
|
|
|
|
|
|
|
|
|
.info-card {
|
|
|
|
|
|
background: linear-gradient(135deg, rgba($primary, 0.08), rgba($accent, 0.05));
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-icon {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-body { flex: 1; }
|
|
|
|
|
|
.info-title { font-size: 13px; font-weight: 700; color: #1e1b4b; margin-bottom: 3px; }
|
|
|
|
|
|
.info-desc { font-size: 12px; color: #6b7280; line-height: 1.6; }
|
|
|
|
|
|
|
2026-04-09 19:29:31 +08:00
|
|
|
|
/* ---------- 画作原图卡片 ---------- */
|
|
|
|
|
|
.original-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.06);
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
.original-thumb {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 84px;
|
|
|
|
|
|
aspect-ratio: 1 / 1;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 2px solid rgba($primary, 0.18);
|
|
|
|
|
|
cursor: zoom-in;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
background: #f5f3ff;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: $primary;
|
|
|
|
|
|
transform: scale(1.03);
|
|
|
|
|
|
.zoom-hint { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.zoom-hint {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(15, 12, 41, 0.4);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.original-text {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.original-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #1e1b4b;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.original-desc {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- 全屏原图预览 ---------- */
|
|
|
|
|
|
.preview-overlay {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 999;
|
|
|
|
|
|
background: rgba(15, 12, 41, 0.88);
|
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: zoom-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
.preview-full-img {
|
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
|
max-height: 80vh;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
.fade-enter-active,
|
|
|
|
|
|
.fade-leave-active { transition: opacity 0.2s; }
|
|
|
|
|
|
.fade-enter-from,
|
|
|
|
|
|
.fade-leave-to { opacity: 0; }
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
/* ---------- 绘本阅读器 ---------- */
|
2026-03-27 22:20:25 +08:00
|
|
|
|
.book-reader {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
overflow: hidden;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
margin-bottom: 14px;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
border: 1px solid rgba($primary, 0.06);
|
2026-04-09 18:48:14 +08:00
|
|
|
|
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
|
|
|
|
|
.page-display {
|
2026-04-09 10:16:23 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 100%;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
background: #1e1b4b;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
2026-04-09 10:16:23 +08:00
|
|
|
|
.page-image {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
max-height: min(72vh, 85vw);
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-placeholder {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
width: 100%;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
|
font-size: 36px;
|
2026-04-09 10:16:23 +08:00
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-text {
|
|
|
|
|
|
padding: 16px 20px;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
border-top: 1px solid rgba($primary, 0.06);
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-audio {
|
|
|
|
|
|
padding: 0 20px 12px;
|
|
|
|
|
|
.audio-player { width: 100%; height: 36px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-nav {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 12px 20px;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
border-top: 1px solid rgba($primary, 0.06);
|
|
|
|
|
|
|
|
|
|
|
|
.nav-btn {
|
|
|
|
|
|
width: 34px;
|
|
|
|
|
|
height: 34px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.2);
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.anticon) { font-size: 14px; }
|
|
|
|
|
|
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
|
|
|
border-color: $primary;
|
|
|
|
|
|
background: rgba($primary, 0.04);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:disabled {
|
|
|
|
|
|
opacity: 0.35;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
.page-indicator {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
/* ---------- 作品信息 ---------- */
|
2026-03-27 22:20:25 +08:00
|
|
|
|
.info-section {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 16px;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
padding: 16px 18px;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
border: 1px solid rgba($primary, 0.06);
|
2026-04-09 18:48:14 +08:00
|
|
|
|
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
|
|
|
|
|
margin-bottom: 14px;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
|
|
|
|
|
.author-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
.author-info { display: flex; flex-direction: column; }
|
|
|
|
|
|
.author-name { font-size: 14px; font-weight: 600; color: #1e1b4b; }
|
|
|
|
|
|
.create-time { font-size: 11px; color: #9ca3af; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
.description {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #4b5563;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tags-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-tag {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 3px 10px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: rgba($primary, 0.08);
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.15);
|
|
|
|
|
|
}
|
2026-03-31 13:56:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
/* ---------- 互动栏 ---------- */
|
2026-03-31 13:56:20 +08:00
|
|
|
|
.interaction-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-around;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
margin-bottom: 14px;
|
2026-03-31 13:56:20 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 16px;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
padding: 12px 0;
|
2026-03-31 13:56:20 +08:00
|
|
|
|
border: 1px solid rgba($primary, 0.06);
|
2026-04-09 18:48:14 +08:00
|
|
|
|
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
2026-03-31 13:56:20 +08:00
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
2026-03-27 22:20:25 +08:00
|
|
|
|
display: flex;
|
2026-03-31 13:56:20 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
2026-04-09 18:48:14 +08:00
|
|
|
|
padding: 8px 18px;
|
|
|
|
|
|
border-radius: 22px;
|
2026-03-31 13:56:20 +08:00
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
2026-04-09 18:48:14 +08:00
|
|
|
|
span { font-size: 13px; font-weight: 500; }
|
2026-03-31 13:56:20 +08:00
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba($primary, 0.04);
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
2026-04-09 18:48:14 +08:00
|
|
|
|
color: $accent;
|
|
|
|
|
|
&:hover { background: rgba($accent, 0.06); }
|
2026-03-31 13:56:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active :deep(.anticon) {
|
|
|
|
|
|
animation: pop 0.3s ease;
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 13:56:20 +08:00
|
|
|
|
|
|
|
|
|
|
@keyframes pop {
|
|
|
|
|
|
0% { transform: scale(1); }
|
|
|
|
|
|
50% { transform: scale(1.3); }
|
|
|
|
|
|
100% { transform: scale(1); }
|
|
|
|
|
|
}
|
2026-04-09 18:48:14 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---------- 作者私有操作区 ---------- */
|
|
|
|
|
|
.owner-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.06);
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
.op-btn {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 100px;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 11px 12px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.anticon) { font-size: 13px; }
|
|
|
|
|
|
|
|
|
|
|
|
&:active { transform: scale(0.97); }
|
|
|
|
|
|
&:disabled { opacity: 0.4; pointer-events: none; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.op-btn.primary {
|
|
|
|
|
|
background: linear-gradient(135deg, $primary 0%, $accent 100%);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
box-shadow: 0 4px 14px rgba($primary, 0.32);
|
|
|
|
|
|
|
|
|
|
|
|
&:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba($primary, 0.4); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.op-btn.outline {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
border: 1.5px solid rgba($primary, 0.4);
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: $primary;
|
|
|
|
|
|
background: rgba($primary, 0.04);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.op-btn.outline-soft {
|
|
|
|
|
|
background: rgba($primary, 0.04);
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba($primary, 0.08);
|
|
|
|
|
|
border-color: rgba($primary, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.op-btn.ghost-danger {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
padding: 11px 16px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: rgba(239, 68, 68, 0.04);
|
|
|
|
|
|
border-color: rgba(239, 68, 68, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</style>
|