9a215a89e9
- novel_done 事件处理中调用 store.fetchScripts() 刷新剧本列表 - typeTabs 从硬编码改为 computed,根据实际 length 值动态生成 - activeType 默认值从 'long' 改为 'all'(显示全部) - 过滤逻辑改为精确匹配 length 字段 - 移除无效的 '风格' 硬编码分类标签 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1035 lines
25 KiB
Vue
1035 lines
25 KiB
Vue
<template>
|
||
<view class="script-library" @click="closeScriptMenu">
|
||
<view class="page-head">
|
||
<view class="back-title" @click="backToScript">
|
||
<text class="back-arrow">‹</text>
|
||
<text class="back-text">返回</text>
|
||
</view>
|
||
<view class="head-actions">
|
||
<view class="circle-btn" @click="openSearch">
|
||
<view class="search-icon"></view>
|
||
</view>
|
||
<view class="circle-btn" @click="openMoreMenu">
|
||
<view class="more-icon">
|
||
<view></view>
|
||
<view></view>
|
||
<view></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="type-tabs">
|
||
<text
|
||
v-for="tab in typeTabs"
|
||
:key="tab.value"
|
||
class="type-tab"
|
||
:class="{ active: activeType === tab.value }"
|
||
@click="activeType = tab.value"
|
||
>{{ tab.label }}</text>
|
||
<view class="new-script" @click="createScript">
|
||
<text class="plus">+</text>
|
||
<text>新建剧本</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-bar">
|
||
<scroll-view class="status-scroll" scroll-x :show-scrollbar="false">
|
||
<view class="status-row">
|
||
<text
|
||
v-for="filter in statusFilters"
|
||
:key="filter.value"
|
||
class="status-chip"
|
||
:class="{ active: activeStatus === filter.value }"
|
||
@click="activeStatus = filter.value"
|
||
>{{ filter.label }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="sort-tools">
|
||
<text class="sort-text" @click="toggleSort">{{ sortLabel }}</text>
|
||
<view class="grid-icon" :class="{ active: viewMode === 'grid' }" @click="toggleViewMode">
|
||
<view v-for="i in 4" :key="i"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="visibleScripts.length" class="script-list" :class="{ grid: viewMode === 'grid' }">
|
||
<view
|
||
v-for="(script, index) in visibleScripts"
|
||
:key="script.id || index"
|
||
class="script-card"
|
||
@click="viewScript(script)"
|
||
>
|
||
<view class="cover" :class="'cover-' + (index % 6)">
|
||
<text>{{ getInitial(script) }}</text>
|
||
</view>
|
||
<view class="card-main">
|
||
<view class="card-top">
|
||
<view class="title-wrap">
|
||
<text class="script-title">{{ script.title }}</text>
|
||
<text class="length-badge">{{ getLengthLabel(script.length) }}</text>
|
||
</view>
|
||
<view class="right-state">
|
||
<text class="state-pill" :class="'state-' + getStatus(script)">{{ getStatusLabel(script) }}</text>
|
||
<view class="menu-wrap">
|
||
<text class="ellipsis" :class="{ active: activeMenuId === String(script.id) }" @click.stop="toggleScriptMenu(script)">•••</text>
|
||
<view v-if="activeMenuId === String(script.id)" class="card-menu" @click.stop>
|
||
<view class="menu-action" @click="toggleFavorite(script)">
|
||
<text class="menu-dot"></text>
|
||
<text>{{ isFavorite(script) ? '取消收藏' : '收藏剧本' }}</text>
|
||
</view>
|
||
<view class="menu-action" @click="continueScript(script)">
|
||
<text class="menu-dot"></text>
|
||
<text>继续生成</text>
|
||
</view>
|
||
<view class="menu-action" @click="openScriptDetailFromMenu(script)">
|
||
<text class="menu-dot"></text>
|
||
<text>查看详情</text>
|
||
</view>
|
||
<view class="menu-action danger" @click="requestDeleteScript(script)">
|
||
<text class="menu-dot"></text>
|
||
<text>删除剧本</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="tag-row">
|
||
<text v-for="tag in getTags(script)" :key="tag" class="tag">{{ tag }}</text>
|
||
</view>
|
||
|
||
<text class="summary">{{ script.summary || script.content || '一段正在生成中的平行人生剧本。' }}</text>
|
||
|
||
<view class="meta-row">
|
||
<text>{{ getChapterCount(script) }}章</text>
|
||
<text>|</text>
|
||
<text>{{ getWordCount(script) }}</text>
|
||
<text>|</text>
|
||
<text>{{ getDateText(script) }}</text>
|
||
</view>
|
||
|
||
<view v-if="getStatus(script) === 'progress'" class="progress-row">
|
||
<view class="progress-track">
|
||
<view class="progress-fill" :style="{ width: getProgress(script) + '%' }"></view>
|
||
</view>
|
||
<text>{{ getProgress(script) }}%</text>
|
||
</view>
|
||
<view v-else-if="isFavorite(script)" class="favorite-row">
|
||
<text class="favorite-star">★</text>
|
||
<text>已收藏</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else class="empty-card">
|
||
<view class="empty-book">
|
||
<view></view>
|
||
<view></view>
|
||
</view>
|
||
<text class="empty-title">还没有人生剧本</text>
|
||
<text class="empty-text">去爽文生成页写下一句灵感,生成你的第一段平行人生。</text>
|
||
<view class="empty-action" @click="createScript">新建剧本</view>
|
||
</view>
|
||
|
||
<view v-if="deleteTarget" class="confirm-mask" @click.stop="cancelDeleteScript">
|
||
<view class="confirm-panel" @click.stop>
|
||
<text class="confirm-title">删除剧本</text>
|
||
<text class="confirm-copy">确定删除《{{ deleteTarget.title || '未命名剧本' }}》吗?删除后将无法在历史剧本中查看。</text>
|
||
<view class="confirm-actions">
|
||
<view class="confirm-btn secondary" @click="cancelDeleteScript">取消</view>
|
||
<view class="confirm-btn danger" :class="{ disabled: deletingScript }" @click="confirmDeleteScript">{{ deletingScript ? '删除中' : '确认删除' }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref, watch } from 'vue'
|
||
import { useAppStore } from '../../stores/app.js'
|
||
import { toggleFavoriteScript, checkFavoriteScript } from '../../services/epicScript.js'
|
||
|
||
const store = useAppStore()
|
||
const activeType = ref('all')
|
||
const activeStatus = ref('all')
|
||
const keyword = ref('')
|
||
const sortMode = ref('updated')
|
||
const viewMode = ref('list')
|
||
const favoriteStatus = ref({})
|
||
const activeMenuId = ref('')
|
||
const deleteTarget = ref(null)
|
||
const deletingScript = ref(false)
|
||
|
||
// length 字段值 → 显示标签映射
|
||
const LENGTH_LABELS = { short: '短篇', medium: '中篇', long: '长篇' }
|
||
|
||
// 从实际剧本数据中提取分类标签,禁止硬编码
|
||
const typeTabs = computed(() => {
|
||
const lengths = new Set(scripts.value.map(s => s.length || 'medium'))
|
||
const tabs = [{ label: '全部', value: 'all' }]
|
||
lengths.forEach(l => {
|
||
tabs.push({ label: LENGTH_LABELS[l] || l, value: l })
|
||
})
|
||
return tabs
|
||
})
|
||
|
||
const statusFilters = [
|
||
{ label: '全部', value: 'all' },
|
||
{ label: '进行中', value: 'progress' },
|
||
{ label: '已完成', value: 'done' },
|
||
{ label: '草稿箱', value: 'draft' },
|
||
{ label: '收藏夹', value: 'favorite' }
|
||
]
|
||
|
||
const scripts = computed(() => store.scripts || [])
|
||
|
||
const visibleScripts = computed(() => {
|
||
const filtered = scripts.value.filter(script => {
|
||
const status = getStatus(script)
|
||
if (keyword.value) {
|
||
const haystack = [script.title, script.summary, script.content, script.style, ...(script.tags || [])].join(' ')
|
||
if (!haystack.includes(keyword.value)) return false
|
||
}
|
||
if (activeStatus.value === 'favorite') return isFavorite(script)
|
||
if (activeStatus.value !== 'all' && status !== activeStatus.value) return false
|
||
// 精确匹配 length 字段,禁止硬编码分类
|
||
if (activeType.value !== 'all') return script.length === activeType.value
|
||
return true
|
||
})
|
||
return [...filtered].sort((a, b) => {
|
||
if (sortMode.value === 'words') return Number(b.wordCount || 0) - Number(a.wordCount || 0)
|
||
if (sortMode.value === 'progress') return getProgress(b) - getProgress(a)
|
||
return String(b.updateTime || b.updatedAt || b.createTime || b.date || '').localeCompare(String(a.updateTime || a.updatedAt || a.createTime || a.date || ''))
|
||
})
|
||
})
|
||
|
||
// 列表加载后批量检查收藏状态
|
||
const loadFavoriteStatus = async () => {
|
||
const ids = scripts.value.map(s => String(s.id)).filter(id => !id.startsWith('demo-'))
|
||
if (!ids.length) return
|
||
const status = {}
|
||
await Promise.all(ids.map(async (id) => {
|
||
try {
|
||
status[id] = await checkFavoriteScript(id)
|
||
} catch {
|
||
status[id] = false
|
||
}
|
||
}))
|
||
favoriteStatus.value = status
|
||
}
|
||
|
||
// 监听 scripts 变化时重新加载收藏状态
|
||
watch(scripts, () => {
|
||
loadFavoriteStatus()
|
||
}, { immediate: true })
|
||
|
||
const sortLabel = computed(() => {
|
||
const map = { updated: '最近更新⌄', words: '字数最多⌄', progress: '进度最高⌄' }
|
||
return map[sortMode.value] || '最近更新⌄'
|
||
})
|
||
|
||
const getStatus = (script) => {
|
||
if (script.status) return script.status
|
||
if (script.isDraft) return 'draft'
|
||
if (script.isCompleted || script.completedAt) return 'done'
|
||
return script.progress ? 'progress' : 'done'
|
||
}
|
||
|
||
const getStatusLabel = (script) => {
|
||
const map = { progress: '进行中', done: '已完成', draft: '草稿' }
|
||
return map[getStatus(script)] || '已完成'
|
||
}
|
||
|
||
const getLengthLabel = (length) => {
|
||
return length === 'short' ? '短篇' : '长篇'
|
||
}
|
||
|
||
const getTags = (script) => {
|
||
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
|
||
return []
|
||
}
|
||
|
||
const getChapterCount = (script) => script.chapterCount || script.chapters || 0
|
||
|
||
const getWordCount = (script) => {
|
||
const count = Number(script.wordCount || 0)
|
||
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
|
||
return `${count}字`
|
||
}
|
||
|
||
const getDateText = (script) => {
|
||
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || ''}`
|
||
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || ''}`
|
||
return `最近更新:${script.updatedAt || script.date || ''}`
|
||
}
|
||
|
||
const getProgress = (script) => Math.max(0, Math.min(99, Number(script.progress || 0)))
|
||
|
||
const getInitial = (script) => (script.title || '剧').slice(0, 1)
|
||
|
||
const isFavorite = (script) => {
|
||
return Boolean(favoriteStatus.value[String(script.id)])
|
||
}
|
||
|
||
const openScriptChat = (script) => {
|
||
if (!script?.id || String(script.id).startsWith('demo-')) return
|
||
uni.setStorageSync('pending_open_script_chat', {
|
||
id: script.id
|
||
})
|
||
uni.$emit('switchTab', 'script')
|
||
setTimeout(() => {
|
||
uni.$emit('openScriptChat', { id: script.id, script })
|
||
}, 80)
|
||
}
|
||
|
||
const viewScript = (script) => {
|
||
openScriptChat(script)
|
||
}
|
||
|
||
const openScriptDetail = (script) => {
|
||
if (!script?.id || String(script.id).startsWith('demo-')) return
|
||
uni.navigateTo({ url: `/pages/main/ScriptDetailView?id=${script.id}` })
|
||
}
|
||
|
||
const createScript = () => {
|
||
uni.$emit('switchTab', 'script')
|
||
}
|
||
|
||
const backToScript = () => {
|
||
uni.$emit('switchTab', 'script')
|
||
}
|
||
|
||
const openSearch = () => {
|
||
uni.showModal({
|
||
title: '搜索剧本',
|
||
editable: true,
|
||
placeholderText: '输入标题、标签或关键词',
|
||
success: (res) => {
|
||
if (res.confirm) keyword.value = String(res.content || '').trim()
|
||
}
|
||
})
|
||
}
|
||
|
||
const openMoreMenu = () => {
|
||
uni.showActionSheet({
|
||
itemList: ['清空搜索', '只看收藏', '查看全部'],
|
||
success: ({ tapIndex }) => {
|
||
if (tapIndex === 0) keyword.value = ''
|
||
if (tapIndex === 1) activeStatus.value = 'favorite'
|
||
if (tapIndex === 2) {
|
||
keyword.value = ''
|
||
activeStatus.value = 'all'
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const toggleSort = () => {
|
||
const order = ['updated', 'words', 'progress']
|
||
sortMode.value = order[(order.indexOf(sortMode.value) + 1) % order.length]
|
||
}
|
||
|
||
const toggleViewMode = () => {
|
||
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list'
|
||
}
|
||
|
||
const closeScriptMenu = () => {
|
||
activeMenuId.value = ''
|
||
}
|
||
|
||
const toggleScriptMenu = (script) => {
|
||
const id = String(script?.id || '')
|
||
activeMenuId.value = activeMenuId.value === id ? '' : id
|
||
}
|
||
|
||
const toggleFavorite = async (script) => {
|
||
const wasFav = isFavorite(script)
|
||
// 乐观更新:先改内存状态
|
||
const next = { ...favoriteStatus.value }
|
||
if (wasFav) {
|
||
delete next[String(script.id)]
|
||
} else {
|
||
next[String(script.id)] = true
|
||
}
|
||
favoriteStatus.value = next
|
||
closeScriptMenu()
|
||
|
||
try {
|
||
const result = await toggleFavoriteScript(script.id)
|
||
uni.showToast({ title: result.isFavorited ? '已收藏' : '已取消收藏', icon: 'success' })
|
||
} catch (error) {
|
||
// 回滚乐观更新
|
||
const restored = { ...favoriteStatus.value }
|
||
if (wasFav) {
|
||
restored[String(script.id)] = true
|
||
} else {
|
||
delete restored[String(script.id)]
|
||
}
|
||
favoriteStatus.value = restored
|
||
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const continueScript = (script) => {
|
||
closeScriptMenu()
|
||
viewScript(script)
|
||
}
|
||
|
||
const openScriptDetailFromMenu = (script) => {
|
||
closeScriptMenu()
|
||
openScriptDetail(script)
|
||
}
|
||
|
||
const requestDeleteScript = (script) => {
|
||
closeScriptMenu()
|
||
if (!script?.id || String(script.id).startsWith('demo-')) {
|
||
uni.showToast({ title: '示例剧本不可删除', icon: 'none' })
|
||
return
|
||
}
|
||
deleteTarget.value = script
|
||
}
|
||
|
||
const cancelDeleteScript = () => {
|
||
if (deletingScript.value) return
|
||
deleteTarget.value = null
|
||
}
|
||
|
||
const confirmDeleteScript = async () => {
|
||
if (!deleteTarget.value?.id || deletingScript.value) return
|
||
deletingScript.value = true
|
||
const id = String(deleteTarget.value.id)
|
||
const res = await store.deleteScript(id)
|
||
deletingScript.value = false
|
||
|
||
if (!res.success) {
|
||
uni.showToast({ title: res.error || '删除失败', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const next = { ...favoriteStatus.value }
|
||
delete next[id]
|
||
favoriteStatus.value = next
|
||
const conversationId = deleteTarget.value.conversationId || deleteTarget.value.plotJson?.conversationId
|
||
if (conversationId) uni.removeStorageSync(`script_conversation_history_${conversationId}`)
|
||
uni.removeStorageSync(`script_chat_history_${id}`)
|
||
const pending = uni.getStorageSync('pending_open_script_chat')
|
||
if (pending?.id === id) uni.removeStorageSync('pending_open_script_chat')
|
||
deleteTarget.value = null
|
||
uni.showToast({ title: '已删除剧本', icon: 'success' })
|
||
}
|
||
|
||
</script>
|
||
|
||
<style scoped>
|
||
.script-library {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
padding-bottom: 26rpx;
|
||
}
|
||
|
||
.page-head,
|
||
.back-title,
|
||
.head-actions,
|
||
.type-tabs,
|
||
.filter-bar,
|
||
.sort-tools,
|
||
.card-top,
|
||
.title-wrap,
|
||
.right-state,
|
||
.meta-row,
|
||
.progress-row,
|
||
.favorite-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.page-head {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.back-title {
|
||
height: 60rpx;
|
||
gap: 10rpx;
|
||
color: rgba(255, 255, 255, 0.94);
|
||
font-weight: 900;
|
||
}
|
||
|
||
.back-arrow {
|
||
font-size: 58rpx;
|
||
line-height: 1;
|
||
transform: translateY(-2rpx);
|
||
}
|
||
|
||
.back-text {
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.head-actions {
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.circle-btn {
|
||
width: 58rpx;
|
||
height: 58rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1rpx solid rgba(142, 105, 255, 0.36);
|
||
background: rgba(10, 11, 38, 0.72);
|
||
}
|
||
|
||
.search-icon {
|
||
width: 25rpx;
|
||
height: 25rpx;
|
||
border: 4rpx solid #fff;
|
||
border-radius: 50%;
|
||
position: relative;
|
||
}
|
||
|
||
.search-icon::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: -9rpx;
|
||
bottom: -8rpx;
|
||
width: 14rpx;
|
||
height: 4rpx;
|
||
border-radius: 999rpx;
|
||
background: #fff;
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.more-icon {
|
||
display: flex;
|
||
gap: 5rpx;
|
||
}
|
||
|
||
.more-icon view {
|
||
width: 6rpx;
|
||
height: 6rpx;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
}
|
||
|
||
.type-tabs {
|
||
justify-content: space-between;
|
||
border-bottom: 1rpx solid rgba(126, 87, 255, 0.18);
|
||
padding-bottom: 16rpx;
|
||
}
|
||
|
||
.type-tab {
|
||
position: relative;
|
||
color: rgba(224, 214, 243, 0.7);
|
||
font-size: 31rpx;
|
||
font-weight: 900;
|
||
padding: 0 20rpx 14rpx;
|
||
}
|
||
|
||
.type-tab.active {
|
||
color: #fff;
|
||
}
|
||
|
||
.type-tab.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 20rpx;
|
||
right: 20rpx;
|
||
bottom: -17rpx;
|
||
height: 5rpx;
|
||
border-radius: 999rpx;
|
||
background: #b246ff;
|
||
box-shadow: 0 0 18rpx rgba(178, 70, 255, 0.8);
|
||
}
|
||
|
||
.new-script {
|
||
margin-left: auto;
|
||
height: 64rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
font-weight: 800;
|
||
background: linear-gradient(135deg, #b346ff, #7330ff);
|
||
box-shadow: 0 0 26rpx rgba(168, 85, 247, 0.54);
|
||
}
|
||
|
||
.plus {
|
||
font-size: 32rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.filter-bar {
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.status-scroll {
|
||
flex: 1;
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.status-row {
|
||
display: inline-flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.status-chip {
|
||
height: 52rpx;
|
||
min-width: 88rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 999rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: rgba(224, 214, 243, 0.78);
|
||
font-size: 23rpx;
|
||
border: 1rpx solid rgba(151, 111, 255, 0.42);
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
.status-chip.active {
|
||
color: #fff;
|
||
border-color: rgba(206, 82, 255, 0.92);
|
||
background: rgba(130, 48, 220, 0.42);
|
||
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.46);
|
||
}
|
||
|
||
.sort-tools {
|
||
gap: 14rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sort-text {
|
||
color: #c99fff;
|
||
font-size: 23rpx;
|
||
}
|
||
|
||
.grid-icon {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
border-radius: 18rpx;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 6rpx;
|
||
padding: 11rpx;
|
||
box-sizing: border-box;
|
||
border: 1rpx solid rgba(151, 111, 255, 0.32);
|
||
}
|
||
|
||
.grid-icon view {
|
||
border: 2rpx solid rgba(230, 222, 250, 0.78);
|
||
border-radius: 3rpx;
|
||
}
|
||
|
||
.script-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18rpx;
|
||
}
|
||
|
||
.script-list.grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
|
||
.script-list.grid .script-card {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.script-list.grid .cover {
|
||
width: 100%;
|
||
}
|
||
|
||
.grid-icon.active {
|
||
border-color: rgba(206, 82, 255, 0.9);
|
||
background: rgba(130, 48, 220, 0.28);
|
||
}
|
||
|
||
.script-card {
|
||
display: grid;
|
||
grid-template-columns: 150rpx 1fr;
|
||
gap: 22rpx;
|
||
min-height: 196rpx;
|
||
padding: 20rpx;
|
||
border-radius: 24rpx;
|
||
border: 1rpx solid rgba(105, 79, 210, 0.34);
|
||
background:
|
||
radial-gradient(circle at 100% 0%, rgba(112, 72, 255, 0.14), transparent 38%),
|
||
rgba(9, 12, 42, 0.72);
|
||
box-shadow: inset 0 0 28rpx rgba(92, 57, 197, 0.08);
|
||
}
|
||
|
||
.cover {
|
||
width: 150rpx;
|
||
height: 150rpx;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-size: 54rpx;
|
||
font-weight: 900;
|
||
overflow: hidden;
|
||
background: linear-gradient(135deg, #3b1a90, #d65cff);
|
||
}
|
||
|
||
.cover-0 { background: linear-gradient(135deg, #29135f, #9037ff 48%, #191b5e); }
|
||
.cover-1 { background: linear-gradient(135deg, #3c1c4a, #f2b3cc 48%, #16143b); }
|
||
.cover-2 { background: linear-gradient(135deg, #1a225f, #7d4cff 48%, #0a0f2c); }
|
||
.cover-3 { background: linear-gradient(135deg, #2f240b, #f7b44a 48%, #0d0a16); }
|
||
.cover-4 { background: linear-gradient(135deg, #3f2417, #d8b58a 48%, #17101d); }
|
||
.cover-5 { background: linear-gradient(135deg, #141451, #cc46ff 48%, #0c0b28); }
|
||
|
||
.card-main {
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-top {
|
||
justify-content: space-between;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.title-wrap {
|
||
min-width: 0;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.script-title {
|
||
color: #fff;
|
||
font-size: 27rpx;
|
||
line-height: 1.25;
|
||
font-weight: 900;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.length-badge {
|
||
flex-shrink: 0;
|
||
padding: 4rpx 9rpx;
|
||
border-radius: 8rpx;
|
||
color: #c985ff;
|
||
font-size: 18rpx;
|
||
border: 1rpx solid rgba(182, 92, 255, 0.5);
|
||
background: rgba(128, 55, 204, 0.22);
|
||
}
|
||
|
||
.right-state {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.state-pill {
|
||
height: 34rpx;
|
||
padding: 0 14rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 19rpx;
|
||
}
|
||
|
||
.state-progress {
|
||
color: #ffbf4c;
|
||
background: rgba(170, 103, 20, 0.22);
|
||
}
|
||
|
||
.state-done {
|
||
color: #79e6a9;
|
||
background: rgba(44, 146, 88, 0.2);
|
||
}
|
||
|
||
.state-draft {
|
||
color: rgba(224, 214, 243, 0.76);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.ellipsis {
|
||
min-width: 48rpx;
|
||
height: 44rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 18rpx;
|
||
color: rgba(224, 214, 243, 0.66);
|
||
font-size: 24rpx;
|
||
letter-spacing: 3rpx;
|
||
}
|
||
|
||
.ellipsis.active {
|
||
color: #fff;
|
||
background: rgba(149, 55, 255, 0.22);
|
||
box-shadow: 0 0 20rpx rgba(168, 67, 255, 0.28);
|
||
}
|
||
|
||
.menu-wrap {
|
||
position: relative;
|
||
}
|
||
|
||
.card-menu {
|
||
position: absolute;
|
||
top: 52rpx;
|
||
right: 0;
|
||
z-index: 30;
|
||
width: 202rpx;
|
||
padding: 10rpx;
|
||
border-radius: 22rpx;
|
||
border: 1rpx solid rgba(192, 132, 252, 0.32);
|
||
background:
|
||
radial-gradient(circle at 90% 0%, rgba(168, 85, 247, 0.22), transparent 36%),
|
||
rgba(12, 8, 34, 0.96);
|
||
box-shadow:
|
||
inset 0 1rpx 0 rgba(255, 255, 255, 0.08),
|
||
0 18rpx 42rpx rgba(0, 0, 0, 0.34);
|
||
backdrop-filter: blur(22rpx);
|
||
-webkit-backdrop-filter: blur(22rpx);
|
||
}
|
||
|
||
.card-menu::before {
|
||
content: '';
|
||
position: absolute;
|
||
right: 22rpx;
|
||
top: -10rpx;
|
||
width: 18rpx;
|
||
height: 18rpx;
|
||
border-left: 1rpx solid rgba(192, 132, 252, 0.3);
|
||
border-top: 1rpx solid rgba(192, 132, 252, 0.3);
|
||
background: rgba(12, 8, 34, 0.96);
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.menu-action {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: 58rpx;
|
||
padding: 0 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
border-radius: 16rpx;
|
||
color: rgba(240, 232, 255, 0.9);
|
||
font-size: 23rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.menu-action:active {
|
||
background: rgba(149, 55, 255, 0.18);
|
||
}
|
||
|
||
.menu-action.danger {
|
||
color: #ff8fa3;
|
||
}
|
||
|
||
.menu-dot {
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.tag-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.tag {
|
||
height: 34rpx;
|
||
padding: 0 14rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
color: #d49cff;
|
||
font-size: 19rpx;
|
||
background: rgba(149, 55, 255, 0.2);
|
||
}
|
||
|
||
.summary {
|
||
display: -webkit-box;
|
||
margin-top: 14rpx;
|
||
color: rgba(226, 215, 246, 0.72);
|
||
font-size: 22rpx;
|
||
line-height: 1.55;
|
||
overflow: hidden;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
|
||
.meta-row {
|
||
gap: 14rpx;
|
||
margin-top: 14rpx;
|
||
color: rgba(218, 204, 243, 0.66);
|
||
font-size: 21rpx;
|
||
}
|
||
|
||
.progress-row {
|
||
justify-content: flex-end;
|
||
gap: 14rpx;
|
||
margin-top: 14rpx;
|
||
color: #bd72ff;
|
||
font-size: 22rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.progress-track {
|
||
width: 118rpx;
|
||
height: 6rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(173, 160, 210, 0.18);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
border-radius: inherit;
|
||
background: linear-gradient(90deg, #b246ff, #d878ff);
|
||
}
|
||
|
||
.favorite-row {
|
||
justify-content: flex-end;
|
||
gap: 8rpx;
|
||
margin-top: 14rpx;
|
||
color: #b768ff;
|
||
font-size: 23rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.favorite-star {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.empty-card {
|
||
margin-top: 30rpx;
|
||
border-radius: 26rpx;
|
||
padding: 44rpx 30rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
border: 1rpx solid rgba(105, 79, 210, 0.34);
|
||
background: rgba(9, 12, 42, 0.72);
|
||
}
|
||
|
||
.empty-book {
|
||
display: flex;
|
||
gap: 6rpx;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.empty-book view {
|
||
width: 32rpx;
|
||
height: 46rpx;
|
||
border: 4rpx solid #b768ff;
|
||
border-radius: 8rpx 4rpx 4rpx 8rpx;
|
||
}
|
||
|
||
.empty-title {
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.empty-text {
|
||
margin-top: 12rpx;
|
||
color: rgba(226, 215, 246, 0.68);
|
||
font-size: 22rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.empty-action {
|
||
margin-top: 22rpx;
|
||
height: 56rpx;
|
||
padding: 0 30rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
color: #fff;
|
||
font-size: 23rpx;
|
||
font-weight: 800;
|
||
background: linear-gradient(135deg, #b346ff, #7330ff);
|
||
}
|
||
|
||
.confirm-mask {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 80;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 48rpx;
|
||
background: rgba(3, 2, 13, 0.62);
|
||
backdrop-filter: blur(12rpx);
|
||
-webkit-backdrop-filter: blur(12rpx);
|
||
}
|
||
|
||
.confirm-panel {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 34rpx 30rpx 28rpx;
|
||
border-radius: 28rpx;
|
||
border: 1rpx solid rgba(192, 132, 252, 0.34);
|
||
background:
|
||
radial-gradient(circle at 92% 0%, rgba(168, 85, 247, 0.2), transparent 36%),
|
||
linear-gradient(145deg, rgba(22, 13, 58, 0.98), rgba(8, 7, 26, 0.98));
|
||
box-shadow: 0 24rpx 72rpx rgba(0, 0, 0, 0.42);
|
||
}
|
||
|
||
.confirm-title {
|
||
display: block;
|
||
color: #fff;
|
||
font-size: 32rpx;
|
||
font-weight: 900;
|
||
text-align: center;
|
||
}
|
||
|
||
.confirm-copy {
|
||
display: block;
|
||
margin-top: 18rpx;
|
||
color: rgba(229, 219, 247, 0.76);
|
||
font-size: 24rpx;
|
||
line-height: 1.55;
|
||
text-align: center;
|
||
}
|
||
|
||
.confirm-actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 18rpx;
|
||
margin-top: 30rpx;
|
||
}
|
||
|
||
.confirm-btn {
|
||
height: 70rpx;
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 25rpx;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.confirm-btn.secondary {
|
||
color: #e8ccff;
|
||
border: 1rpx solid rgba(192, 132, 252, 0.3);
|
||
background: rgba(88, 28, 135, 0.2);
|
||
}
|
||
|
||
.confirm-btn.danger {
|
||
color: #fff;
|
||
background: linear-gradient(145deg, #e05276, #8d2444);
|
||
box-shadow: 0 12rpx 30rpx rgba(224, 82, 118, 0.24);
|
||
}
|
||
|
||
.confirm-btn.disabled {
|
||
opacity: 0.55;
|
||
}
|
||
</style>
|