4 Commits

Author SHA1 Message Date
7e0087a7db 支持带章节作品 网站功能基本完善 🥳
All checks were successful
Build / build-and-test (push) Successful in 39s
[UX]
增加对格式错误的链接检测
在搜索或者切换章节时地址栏和标题会跟着变动

[UI]
不再使用 mdui-collapse 作为作品信息容器
SSR 时作品文段渲染增加到 20 行

[Fix]
SSR 时 Category 与 Fandom 标识错误

[Base]
Summary 不再使用 v-for

[NewBug]
在搜索不到东西时会显示搜索完成, 推断是状态转移不完全
2025-05-27 23:59:27 +08:00
8ff45e210c [BugFix] 修复页面自动尝试加载更多的问题
All checks were successful
Build / build-and-test (push) Successful in 26s
2025-05-26 18:24:46 +08:00
f60bf907b1 增加搜索页面自动加载更多 (之前只能加载一页)
All checks were successful
Build / build-and-test (push) Successful in 30s
~~其实就是太激动了没做完就提交了~~
2025-05-26 18:15:53 +08:00
f5d3e7ea88 更新了搜索功能 (终于!) 1.1.0 版本 Release!
All checks were successful
Build / build-and-test (push) Successful in 33s
2025-05-26 11:23:24 +08:00
10 changed files with 284 additions and 81 deletions

View File

@ -16,5 +16,6 @@ export function createApp() {
.component('Hr', Hr) .component('Hr', Hr)
.component('BetterHr', Hr) .component('BetterHr', Hr)
.component('Form', Form) .component('Form', Form)
.component('BetterForm', Form)
return { app, pinia } return { app, pinia }
} }

View File

@ -39,21 +39,29 @@ export function createSSRRouter() {
title: '阅读', title: '阅读',
hidden: true hidden: true
} }
},{
path: '/search/simple',
name: '搜索',
component: () => import('./views/SimpleSearch.vue'),
meta: {
title: '搜索',
order: 2
}
},{ },{
path: '/settings', path: '/settings',
name: '设置', name: '设置',
component: () => import('./views/Settings.vue'), component: () => import('./views/Settings.vue'),
meta: { meta: {
title: '设置', title: '设置',
order: 2 order: 90
}, },
},{ },{
path: '/about', path: '/about',
name: '关于', name: '关于',
component: () => import('./views/About.vue'), component: () => import('./views/About.md'),
meta: { meta: {
title: '', title: '',
order: 3 order: 100
}, },
},{ },{
path: '/developer', path: '/developer',

58
src/stores/search.js Normal file
View File

@ -0,0 +1,58 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { useApiStore } from '@/stores/api.js'
export const useSimpleSearchState = defineStore('simpleSearch', () => {
const api = useApiStore()
const keyword = ref('')
const result = ref([])
const count = ref(0)
const pageCount = ref(0)
const currentPage = ref(0)
const state = ref(null)
async function load() {
if (pageCount.value && currentPage.value >= pageCount.value){ state.value = 'finish'; return }
let res = await api.workSimpleSearch(keyword.value, currentPage.value)
res = res.data
if( pageCount.value ) {
currentPage.value++
if (currentPage.value > pageCount.value) currentPage.value = pageCount.value
}
if (res.code == 0) {
if ( !pageCount.value ) {
pageCount.value = res.pageCount
currentPage.value = res.page
}
count.value = res.count
state.value = import.meta.env.SSR ? 'ssrready' : 'ready'
result.value.push(...res.works)
} else if (res.code == 1) {
if ( count.value ) {
state.value = 'finish'
} else {
currentPage.value = 0
state.value = 'notfound'
}
}
}
async function start(key) {
if (key == keyword.value) return
keyword.value = key
result.value = []
state.value = 'loading'
currentPage.value = 1
pageCount.value = 0
await load()
}
return {
keyword,
result,
count,
pageCount,
currentPage,
state,
load, start
}
})

View File

@ -10,7 +10,7 @@ export const useWorkReadState = defineStore('workRead', () => {
const id = ref(null) const id = ref(null)
const cid = ref(null) const cid = ref(null)
const summary = ref(null) const summary = ref(null)
const pesud = ref(null) const pseud = ref(null)
const title = ref(null) const title = ref(null)
const text = ref(null) const text = ref(null)
const state = ref('') const state = ref('')
@ -21,11 +21,14 @@ export const useWorkReadState = defineStore('workRead', () => {
const category = ref([]) const category = ref([])
const fandom = ref([]) const fandom = ref([])
const lang = ref(null) const lang = ref(null)
const chapters = ref([])
const chapterIndex = ref(null)
function setData(data) { function setData(data) {
cid.value = data.chapterId
id.value = data.workId id.value = data.workId
title.value = data.title title.value = data.title
summary.value = [escapeAndFormatText(data.summary)] summary.value = data.summary ? escapeAndFormatText(data.summary) : null
pesud.value = data.pesud pseud.value = data.pseud
text.value = data.text text.value = data.text
publishedTime.value = data.stats.publishedTime publishedTime.value = data.stats.publishedTime
wordCount.value = data.stats.wordCount wordCount.value = data.stats.wordCount
@ -34,11 +37,26 @@ export const useWorkReadState = defineStore('workRead', () => {
category.value = data.category category.value = data.category
fandom.value = data.fandom fandom.value = data.fandom
lang.value = data.lang lang.value = data.lang
chapters.value = data.chapters || []
chapterIndex.value = data.chapterIndex ?? null
} }
async function loadWork(target, targetc) { async function loadWork(target, targetc) {
if (target == id.value && targetc == cid.value || state.value == 'loading') return const itarget = parseInt(target)
if (isNaN(itarget)) {
console.log('a')
state.value = 'errformat'
return
}
const itargetc = parseInt(targetc)
if (
itarget === id.value &&
(itargetc === cid.value || (isNaN(itargetc) && cid.value === null)) &&
state.value !== 'loading'
) return
id.value = itarget
cid.value = isNaN(itargetc) ? null : itargetc
state.value = 'loading' state.value = 'loading'
const result = await api.getWork(target, targetc) const result = await api.getWork(id.value, cid.value)
if (result.status == 200) { if (result.status == 200) {
setData(result.data) setData(result.data)
state.value = 'ready' state.value = 'ready'
@ -51,7 +69,7 @@ export const useWorkReadState = defineStore('workRead', () => {
id, cid, id, cid,
title, title,
summary, summary,
pesud, pseud,
text, text,
state, state,
publishedTime, publishedTime,
@ -61,6 +79,8 @@ export const useWorkReadState = defineStore('workRead', () => {
category, category,
fandom, fandom,
lang, lang,
chapters,
chapterIndex,
setData, setData,
loadWork loadWork
} }

View File

@ -1,9 +1,3 @@
# 欢迎来到 AO3 Mirror! 👋👋
一个基于重构渲染的 AO3 镜像站
---
## 这是什么🤨 ## 这是什么🤨
本网站是对 ArchiveOfOurOwn (AO3) 的一个镜像网站 本网站是对 ArchiveOfOurOwn (AO3) 的一个镜像网站
@ -45,3 +39,4 @@
- ✅ 作品详细数据 - ✅ 作品详细数据
- 📝 搜索 - 📝 搜索
- ❌ 书签 (本地) **不再支持! [详情](/about#deprecated-feature-bookmark)** - ❌ 书签 (本地) **不再支持! [详情](/about#deprecated-feature-bookmark)**

View File

@ -1,3 +1,18 @@
<script setup>
import About from '../texts/about.md'
import FunAnimation from '../components/FunAnimation.vue'
import 'mdui/components/avatar.js'
/*import { onBeforeMount, onMounted, onUnmounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
console.log('Setup')
onBeforeMount(() => console.log('Before mount'))
onMounted(() => console.log('Mounted'))
onDeactivated(() => console.log('Deactivated'))
onActivated(() => console.log('Activated'))
onBeforeUnmount(() => console.log('Before unmount'))
onUnmounted(() => console.log('Unmounted'))*/
</script>
# 关于 # 关于
这是什么, 有口舍用 ? 这是什么, 有口舍用 ?
@ -43,3 +58,5 @@
本站支持 "Server Side Rendering" by Vite SSR 本站支持 "Server Side Rendering" by Vite SSR
CDN by Cloudflare (赛博佛祖😭) CDN by Cloudflare (赛博佛祖😭)
<FunAnimation />

View File

@ -1,19 +0,0 @@
<script setup>
import About from '../texts/about.md'
import FunAnimation from '../components/FunAnimation.vue'
import 'mdui/components/avatar.js'
/*import { onBeforeMount, onMounted, onUnmounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
console.log('Setup')
onBeforeMount(() => console.log('Before mount'))
onMounted(() => console.log('Mounted'))
onDeactivated(() => console.log('Deactivated'))
onActivated(() => console.log('Activated'))
onBeforeUnmount(() => console.log('Before unmount'))
onUnmounted(() => console.log('Unmounted'))*/
</script>
<template>
<About/>
<FunAnimation />
</template>

View File

@ -9,9 +9,6 @@ import Intro from '../texts/intro.md'
const router = useRouter() const router = useRouter()
const err = ref(false)
const srcText = ref(null)
function convert(from) { function convert(from) {
if( Number(from) ) { if( Number(from) ) {
return { return {
@ -20,7 +17,6 @@ function convert(from) {
} }
} else if (from.includes('https://archiveofourown.org/works/')) { } else if (from.includes('https://archiveofourown.org/works/')) {
const sid = from.split('https://archiveofourown.org/works/')[1]; const sid = from.split('https://archiveofourown.org/works/')[1];
console.log(sid)
if ( sid ) { if ( sid ) {
const id = Number(sid) const id = Number(sid)
if (id) { if (id) {
@ -41,16 +37,15 @@ function convert(from) {
} }
} }
} }
return { type: null } return { type: 'f', key: from }
} }
function onConvert(data) { function onConvert(data) {
const { type, id, cid } = convert(data.src) const { type, id, cid, key } = convert(data.src)
if (type == null) { if (type == null) {
err.value = true } else if ( type == 'f') {
srcText.value?.focus() router.push(`/search/simple?keyword=${encodeURIComponent(key)}`)
} else { } else {
err.value = false
if (cid) router.push(`/work/${id}/${cid}`) if (cid) router.push(`/work/${id}/${cid}`)
else router.push(`/work/${id}`) else router.push(`/work/${id}`)
} }
@ -60,14 +55,14 @@ function onConvert(data) {
<template> <template>
<img style="display: block; margin: 0px auto 10px;" height="200px" alt="logo" src="/favicon.svg" /> <img style="display: block; margin: 0px auto 10px;" height="200px" alt="logo" src="/favicon.svg" />
<Intro /> <h1>欢迎来到 AO3 Mirror! 👋👋</h1>
<br/><Hr/> <p>一个基于重构渲染的 AO3 镜像站</p>
<Hr />
<section id="converter"> <section id="converter">
<h2>链接转换</h2> <h2>链接转换</h2>
<p>输入完整链接或者 ID</p> <p>AO3 链接 关键词搜索</p>
<Form @submit="onConvert"><ClientOnly> <Form @submit="onConvert"><ClientOnly>
<mdui-text-field variant="filled" label="链接" name="src" placeholder="https://archiveofourown.org/works/114514" ref='srcText'> <mdui-text-field variant="filled" label="链接 / 关键词" name="src">
<span v-if='err' slot="helper" class='warn-text'>链接格式错误!</span>
</mdui-text-field><br/> </mdui-text-field><br/>
<div style="display: flex"> <div style="display: flex">
<div style="flex-grow: 1"></div> <div style="flex-grow: 1"></div>
@ -78,4 +73,7 @@ function onConvert(data) {
<input type="submit" /> <input type="submit" />
</template></ClientOnly></Form> </template></ClientOnly></Form>
</section> </section>
<Hr/>
<Intro />
</template> </template>

View File

@ -0,0 +1,97 @@
<script setup>
import { ref, watch, onMounted, nextTick, onServerPrefetch, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import 'mdui/components/text-field.js'
import 'mdui/components/card.js'
import { escapeAndFormatText } from '../utils.js'
import { useSimpleSearchState } from '../stores/search.js'
const route = useRoute()
const router = useRouter()
const simpleSearchState = useSimpleSearchState()
const inputField = ref('')
const label = ref(null)
const stateLabel = {
'loading': '加载中',
'finish': '加载完成',
'ready': '就绪',
'ssrready': '就绪',
'notfound': '未找到',
'ssrnotfound': '未找到',
}
let isObserver = null
onServerPrefetch(async () => {
if (route.query.keyword) {
await simpleSearchState.start(route.query.keyword)
}
})
onMounted(async () => {
watch(() => simpleSearchState.keyword, () => document.title = simpleSearchState.keyword)
inputField.value = route.query.keyword || ''
if (inputField.value && simpleSearchState != 'ssrready') simpleSearchState.start(inputField.value)
isObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (simpleSearchState.state == 'ready') simpleSearchState.load()
}
})
}, { threshold: 1 })
await nextTick()
isObserver.observe(label.value)
})
onBeforeUnmount(() => {
isObserver.disconnect();
})
function onSubmit(data) {
if (simpleSearchState) {
simpleSearchState.start(data.src)
router.replace(`/search/simple?keyword=${encodeURIComponent(data.src)}`)
}
}
</script>
<template>
<h1>搜索</h1>
<Form @submit="onSubmit"><ClientOnly>
<mdui-text-field variant="filled" :value="inputField" label="链接 / 关键词" name="src">
</mdui-text-field><br/>
<div style="display: flex">
<div style="flex-grow: 1"></div>
<mdui-button type="submit">-></mdui-button>
</div>
<template #ssr>
<input type="text" id="src" name="src" />
<input type="submit" />
</template></ClientOnly></Form><Hr />
<template v-if="simpleSearchState.state == 'ready' || simpleSearchState.state == 'finish' || simpleSearchState.state == 'ssrready'">
<p>找到 {{ simpleSearchState.count }}</p><Hr/>
</template>
<template v-if="simpleSearchState.result" v-for="work in simpleSearchState.result" :key="work.workId">
<ClientOnly><mdui-card style="margin: 8px 0px;"><article>
<router-link :to="`/work/${work.workId}`"><h3>{{ work.title }}</h3></router-link>
<h4>{{ work.pseud }}</h4>
<Hr />
<p v-html="escapeAndFormatText(work.summary)"></p>
</article></mdui-card><template #ssr>
<router-link :to="`/work/${work.workId}`"><h3>{{ work.title }}</h3></router-link>
<h4>{{ work.pseud }}</h4>
<p>{{ work.summary }}</p>
<Hr />
</template></ClientOnly>
</template>
<p style="display: flex;" ref='label'>
{{ stateLabel[simpleSearchState.state] }} ({{ simpleSearchState.count }})
<span style="flex: 1;"/>
{{ simpleSearchState.currentPage }} / {{ simpleSearchState.pageCount }}
</p>
</template>

View File

@ -19,8 +19,6 @@ import 'mdui/components/button.js'
import 'mdui/components/dropdown.js' import 'mdui/components/dropdown.js'
import 'mdui/components/menu.js' import 'mdui/components/menu.js'
import 'mdui/components/menu-item.js' import 'mdui/components/menu-item.js'
import 'mdui/components/collapse.js'
import 'mdui/components/collapse-item.js'
import 'mdui/components/card.js' import 'mdui/components/card.js'
import '@mdui/icons/bookmark.js' import '@mdui/icons/bookmark.js'
@ -41,6 +39,7 @@ let lastCloseTimer = null
let isObserver = null let isObserver = null
let paragraphs = [] let paragraphs = []
let currentParagraph = null let currentParagraph = null
const chapterDialog = ref(null)
const categoryName = { const categoryName = {
mm: "男/男", mm: "男/男",
@ -48,21 +47,25 @@ const categoryName = {
fm: '女/男' fm: '女/男'
} }
onServerPrefetch(async () => { onServerPrefetch(async () => {
await workReadState.loadWork(route.params.id, route.params.cid) await workReadState.loadWork(route.params.id, route.params.cid)
}) })
onMounted(async () => { onMounted(async () => {
watch(() => workReadState.state, (value) => { if (value == 'ready') routeState.customTitle = workReadState.title })
if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id, route.params.cid) if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id, route.params.cid)
if (workReadState.state == 'ready') { if (workReadState.state == 'ready') {
routeState.customTitle = workReadState.title routeState.customTitle = workReadState.title
if (parseInt(route.params.cid) != workReadState.cid) {
router.replace(`/work/${workReadState.id}/${workReadState.cid}`)
return;
}
const paraCount = workReadState.text.length - 1 const paraCount = workReadState.text.length - 1
isObserver = new IntersectionObserver((entries) => { isObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
currentParagraph = entry.target currentParagraph = entry.target
readIndex = entry.target.dataset.index; readIndex = entry.target.dataset.index
readPercent.value = parseInt(readIndex / paraCount * 100) readPercent.value = parseInt(readIndex / paraCount * 100)
if (lastPercent == 0) { if (lastPercent == 0) {
lastPercent = readPercent.value lastPercent = readPercent.value
@ -89,8 +92,13 @@ onMounted(async () => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
isObserver.disconnect(); if(isObserver) isObserver.disconnect();
}) })
async function switchWorkWithIndex(target) {
workReadState.loadWork(workReadState.id,workReadState.chapters[target].chapterId);
router.replace(`/work/${workReadState.id}/${workReadState.cid}`)
}
</script> </script>
<template> <template>
@ -108,14 +116,20 @@ onBeforeUnmount(() => {
</template> </template>
<a @click="$router.back()">返回</a> <a @click="$router.back()">返回</a>
</template> </template>
<template v-if="workReadState.state == 'errformat'">
<h2>路径格式错误</h2>
ID: {{ $route.params.id }}<br/>
<template v-if="$route.params.id">
CID: {{ $route.params.id }}
</template><br/>
<a @click="$router.back()">返回</a>
</template>
<template v-if="workReadState.state == 'ready'"> <template v-if="workReadState.state == 'ready'">
<article> <article>
<h1 style="margin: auto">{{ workReadState.title }}</h1> <h1 style="margin: auto">{{ workReadState.title }}</h1>
<h4>{{ workReadState.pesud }}</h4> <h4>{{ workReadState.pesud }}</h4>
<mdui-card style="margin: 8px; padding: 0px;"><mdui-collapse acc> <mdui-card style="margin: 8px; padding: 16px;">
<mdui-collapse-item value="info"><mdui-list-item class="infoblockhead" slot="header"> <strong>作品信息</strong><dl>
作品信息
</mdui-list-item><div class="infoblock"><dl>
<template v-if="workReadState.category"><dt>分类</dt><ul> <template v-if="workReadState.category"><dt>分类</dt><ul>
<li v-for="item in workReadState.category" :key="item"> <li v-for="item in workReadState.category" :key="item">
{{ categoryName[item] }}</li> {{ categoryName[item] }}</li>
@ -127,10 +141,8 @@ onBeforeUnmount(() => {
<dt>语言</dt><dd> <dt>语言</dt><dd>
{{ workReadState.lang }} {{ workReadState.lang }}
</dd> </dd>
</dl></div></mdui-collapse-item> </dl><Hr/>
<mdui-collapse-item value="stats"><mdui-list-item class="infoblockhead" slot="header"> <strong>作品状态</strong><dl>
作品状态
</mdui-list-item><div class="infoblock"><dl>
<dt>发布时间</dt><dd> <dt>发布时间</dt><dd>
{{ workReadState.publishedTime.year }} - {{ workReadState.publishedTime.year }} -
{{ workReadState.publishedTime.month }} - {{ workReadState.publishedTime.month }} -
@ -142,20 +154,44 @@ onBeforeUnmount(() => {
<dt>点击</dt><dd> <dt>点击</dt><dd>
{{ workReadState.hitCount }} {{ workReadState.hitCount }}
</dd> </dd>
</dl></div></mdui-collapse-item> </dl>
</mdui-collapse></mdui-card> </mdui-card>
<blockquote> <template v-if="workReadState.chapters">
<p v-for="para in workReadState.summary" :key="para" v-html='para'></p> <p> {{ workReadState.chapterIndex + 1 }} / {{ workReadState.chapters.length }} : {{ workReadState.chapters[workReadState.chapterIndex].title }}</p>
<mdui-button variant='filled' @click="chapterDialog.open = true">章节列表</mdui-button>
</template>
<blockquote v-if="workReadState.summary">
<p v-html='workReadState.summary'></p>
</blockquote> </blockquote>
<Hr /> <Hr />
<article ref='content'> <article ref='content'>
<p v-for="(para, index) in workReadState.text" :key="para" :data-index="index">{{ para }}</p> <p v-for="(para, index) in workReadState.text" :key="para" :data-index="index">{{ para }}</p>
</article> </article>
</article> </article>
<Hr/>
<p style="display: flex;" v-if="workReadState.chapters">
<mdui-button variant="filled" v-if="workReadState.chapterIndex != 0" @click="switchWorkWithIndex(workReadState.chapterIndex - 1)">上一章</mdui-button>
<span style="flex: 1;"/>
{{ workReadState.chapterIndex + 1 }} / {{ workReadState.chapters.length }}
<span style="flex: 1;"/>
<mdui-button variant="filled" v-if="workReadState.chapterIndex != workReadState.chapters.length - 1" @click="switchWorkWithIndex(workReadState.chapterIndex + 1)">下一章</mdui-button>
</p>
<div style="height: 64px" />
<mdui-fab class="mdui-fab" :extended="fabExtended"> <mdui-fab class="mdui-fab" :extended="fabExtended">
<mdui-icon-bookmark slot="icon"></mdui-icon-bookmark> <mdui-icon-bookmark slot="icon"></mdui-icon-bookmark>
{{ readPercent }}% {{ readPercent }}%
</mdui-fab> </mdui-fab>
<mdui-dialog ref='chapterDialog' close-on-overlay-click>
<span slot="headline">章节列表</span>
<span slot="description">
{{ workReadState.chapters.length }}
<br/>
点击跳转
</span>
<mdui-list><mdui-list-item v-for="(chapter,index) in workReadState.chapters" :key="chapter.chapterId" @click="switchWorkWithIndex(index)">
{{index + 1}}. {{ chapter.title }}
</mdui-list-item></mdui-list>
</mdui-dialog>
</template> </template>
<template #ssr> <template #ssr>
<template v-if="workReadState.state == 'notfound' || workReadState.state == 'ssrnotfound'"> <template v-if="workReadState.state == 'notfound' || workReadState.state == 'ssrnotfound'">
@ -168,14 +204,14 @@ onBeforeUnmount(() => {
<a @click="$router.back()">返回</a> <a @click="$router.back()">返回</a>
</template> </template>
<template v-if="workReadState.state == 'ready'"> <template v-if="workReadState.state == 'ready'">
<h1>{{ workReadState.title }}</h1> <h2>{{ workReadState.title }}</h2>
<h2>{{ workReadState.pesud }}</h2> <h4>{{ workReadState.pesud }}</h4>
<dl> <dl>
<template v-if="workReadState.category"><dt>作品圈</dt><ul> <template v-if="workReadState.category"><dt>分类</dt><ul>
<li v-for="item in workReadState.category" :key="item"> <li v-for="item in workReadState.category" :key="item">
{{ categoryName[item] }}</li> {{ categoryName[item] }}</li>
</ul></template> </ul></template>
<template v-if="workReadState.fandom"><dt>原著</dt><ul> <template v-if="workReadState.fandom"><dt>作品圈</dt><ul>
<li v-for="item in workReadState.fandom" :key="item"> <li v-for="item in workReadState.fandom" :key="item">
{{ item }}</li> {{ item }}</li>
</ul></template> </ul></template>
@ -195,11 +231,11 @@ onBeforeUnmount(() => {
</dd> </dd>
</dl> </dl>
<Hr /> <Hr />
<blockquote> <template v-if="workReadState.summary"><blockquote>
<p v-for="para in workReadState.summary" :key="para" v-html='para'></p> <p v-html='workReadState.summary'></p>
</blockquote> </blockquote>
<Hr/> <Hr /></template>
<article><p v-for="para in workReadState.text.slice(0, 10)" :key="para">{{ para }}</p></article> <article><p v-for="para in workReadState.text.slice(0, 20)" :key="para">{{ para }}</p></article>
</template> </template>
</template></ClientOnly> </template></ClientOnly>
</template> </template>
@ -212,12 +248,4 @@ onBeforeUnmount(() => {
z-index: 1000; /* 确保悬浮按钮在其他内容上方 */ z-index: 1000; /* 确保悬浮按钮在其他内容上方 */
animation: slideInFromRight var(--mdui-motion-duration-medium2) var(--mdui-motion-easing-standard); /* 动画时长和缓动效果 */ animation: slideInFromRight var(--mdui-motion-duration-medium2) var(--mdui-motion-easing-standard); /* 动画时长和缓动效果 */
} }
.infoblock {
margin: 8px 16px;
}
.infoblockhead {
background-color: rgb(var(--mdui-color-primary-container));
}
</style> </style>