移除书签机制 更新了项目描述 增加对有章节作品阅读支持 [BugFix] BetterHr 无法在 Markdown 显示的问题 [Base] 增加 Markdown 渲染锚点支持
This commit is contained in:
30
package-lock.json
generated
30
package-lock.json
generated
@ -13,7 +13,6 @@
|
||||
"compress-json": "^3.1.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"express": "^5.1.0",
|
||||
"idb": "^8.0.3",
|
||||
"mdui": "^2.1.3",
|
||||
"pinia": "^3.0.2",
|
||||
"vue": "^3.5.13",
|
||||
@ -23,6 +22,8 @@
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-attrs": "^4.3.1",
|
||||
"sass": "^1.88.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-md": "^0.21.5",
|
||||
@ -3797,11 +3798,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz",
|
||||
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.2.tgz",
|
||||
@ -4144,6 +4140,28 @@
|
||||
"markdown-it": "bin/markdown-it.js"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-anchor": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-it-anchor/-/markdown-it-anchor-9.2.0.tgz",
|
||||
"integrity": "sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@types/markdown-it": "*",
|
||||
"markdown-it": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-attrs": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-it-attrs/-/markdown-it-attrs-4.3.1.tgz",
|
||||
"integrity": "sha512-/ko6cba+H6gdZ0DOw7BbNMZtfuJTRp9g/IrGIuz8lYc/EfnmWRpaR3CFPnNbVz0LDvF8Gf1hFGPqrQqq7De0rg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"markdown-it": ">= 9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it/node_modules/entities": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"compress-json": "^3.1.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"express": "^5.1.0",
|
||||
"idb": "^8.0.3",
|
||||
"mdui": "^2.1.3",
|
||||
"pinia": "^3.0.2",
|
||||
"vue": "^3.5.13",
|
||||
@ -26,6 +25,8 @@
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-attrs": "^4.3.1",
|
||||
"sass": "^1.88.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-md": "^0.21.5",
|
||||
|
@ -14,6 +14,7 @@ export function createApp() {
|
||||
app
|
||||
.component('ClientOnly', ClientOnly)
|
||||
.component('Hr', Hr)
|
||||
.component('BetterHr', Hr)
|
||||
.component('Form', Form)
|
||||
return { app, pinia }
|
||||
}
|
||||
|
@ -3,20 +3,40 @@ import { createMemoryHistory, createWebHistory, createRouter } from 'vue-router'
|
||||
export function createSSRRouter() {
|
||||
const router = createRouter({
|
||||
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else if (to.hash) {
|
||||
return {
|
||||
el: to.hash,
|
||||
behavior: 'smooth',
|
||||
}
|
||||
} else {
|
||||
return { top: 0 }
|
||||
}
|
||||
},
|
||||
routes: [{
|
||||
path: '/',
|
||||
name: '前言',
|
||||
component: () => import('./views/Root.vue'),
|
||||
meta: {
|
||||
title: "首页",
|
||||
title: '首页',
|
||||
order: 1
|
||||
},
|
||||
},{
|
||||
path: '/work/:id',
|
||||
name: '阅读',
|
||||
name: 'work',
|
||||
component: () => import('./views/Work.vue'),
|
||||
meta: {
|
||||
title: "",
|
||||
title: '阅读',
|
||||
hidden: true
|
||||
}
|
||||
},{
|
||||
path: '/work/:id/:cid',
|
||||
name: 'workChapter',
|
||||
component: () => import('./views/Work.vue'),
|
||||
meta: {
|
||||
title: '阅读',
|
||||
hidden: true
|
||||
}
|
||||
},{
|
||||
@ -24,7 +44,7 @@ export function createSSRRouter() {
|
||||
name: '关于',
|
||||
component: () => import('./views/About.vue'),
|
||||
meta: {
|
||||
title: "",
|
||||
title: '',
|
||||
order: 2
|
||||
},
|
||||
},{
|
||||
@ -32,7 +52,7 @@ export function createSSRRouter() {
|
||||
name: '开发人员选项',
|
||||
component: () => import('./views/Developer.vue'),
|
||||
meta: {
|
||||
title: "",
|
||||
title: '',
|
||||
hidden: true
|
||||
},
|
||||
},{
|
||||
@ -40,7 +60,7 @@ export function createSSRRouter() {
|
||||
name: 'NotFound',
|
||||
component: () => import('./views/fallback/NotFound.vue'),
|
||||
meta: {
|
||||
title: "页面未找到",
|
||||
title: '页面未找到',
|
||||
hidden: true,
|
||||
code: 404
|
||||
}
|
||||
|
@ -102,8 +102,9 @@ export const useApiStore = defineStore('api', () => {
|
||||
inited = false
|
||||
await init()
|
||||
}
|
||||
async function getWork(workId) {
|
||||
return await apiGet('work',{ workId })
|
||||
async function getWork(workId, chapterId) {
|
||||
if (chapterId) return await apiGet(`work/${workId}/${chapterId}`)
|
||||
return await apiGet(`work/${workId}`)
|
||||
}
|
||||
return {
|
||||
init,
|
||||
|
@ -1,58 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { openDB } from 'idb';
|
||||
|
||||
export const useDB = defineStore('_db', () => {
|
||||
const dbPromise = openDB('data', 1, {
|
||||
upgrade(db) {
|
||||
const bookmarkStore = db.createObjectStore('bookmarks', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
})
|
||||
bookmarkStore.createIndex('by-workId', 'workId');
|
||||
},
|
||||
})
|
||||
return {
|
||||
db: dbPromise
|
||||
}
|
||||
})
|
||||
|
||||
export const useBookmarkStore = defineStore('bookmark', () => {
|
||||
const db = useDB().db
|
||||
async function getAll(workId) {
|
||||
return (await db).getAllFromIndex('bookmarks', 'by-workId', workId);
|
||||
}
|
||||
async function get(id) {
|
||||
return (await db).get('bookmarks', id);
|
||||
}
|
||||
async function add(workId, index, para, name ) {
|
||||
return (await db).add('bookmarks', {
|
||||
workId, name, para, index
|
||||
});
|
||||
}
|
||||
async function del(id) {
|
||||
(await db).delete('bookmarks', id);
|
||||
}
|
||||
async function delByWork(workId) {
|
||||
(await getAll(workId)).forEach(async (item) => {
|
||||
del(item.id)
|
||||
})
|
||||
}
|
||||
async function updateName(id, name) {
|
||||
const raw = await get(id)
|
||||
if (raw) {
|
||||
raw.name = name
|
||||
console.log(name)
|
||||
await (await db).put('bookmarks', raw);
|
||||
}
|
||||
}
|
||||
return {
|
||||
get,
|
||||
add,
|
||||
del,
|
||||
getAll,
|
||||
delByWork,
|
||||
updateName
|
||||
}
|
||||
})
|
@ -8,6 +8,7 @@ import { useApiStore } from '@/stores/api.js'
|
||||
export const useWorkReadState = defineStore('workRead', () => {
|
||||
const api = useApiStore()
|
||||
const id = ref(null)
|
||||
const cid = ref(null)
|
||||
const summary = ref(null)
|
||||
const pesud = ref(null)
|
||||
const title = ref(null)
|
||||
@ -25,7 +26,7 @@ export const useWorkReadState = defineStore('workRead', () => {
|
||||
title.value = data.title
|
||||
summary.value = [escapeAndFormatText(data.summary)]
|
||||
pesud.value = data.pesud
|
||||
text.value = data.text.split('\n\n')
|
||||
text.value = data.text
|
||||
publishedTime.value = data.stats.publishedTime
|
||||
wordCount.value = data.stats.wordCount
|
||||
kudoCount.value = data.stats.kudoCount
|
||||
@ -34,10 +35,10 @@ export const useWorkReadState = defineStore('workRead', () => {
|
||||
fandom.value = data.fandom
|
||||
lang.value = data.lang
|
||||
}
|
||||
async function loadWork(target) {
|
||||
if (target == id.value || state.value == 'loading') return
|
||||
async function loadWork(target, targetc) {
|
||||
if (target == id.value && targetc == cid.value || state.value == 'loading') return
|
||||
state.value = 'loading'
|
||||
const result = await api.getWork(target)
|
||||
const result = await api.getWork(target, targetc)
|
||||
if (result.status == 200) {
|
||||
setData(result.data)
|
||||
state.value = 'ready'
|
||||
@ -47,7 +48,7 @@ export const useWorkReadState = defineStore('workRead', () => {
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
id, cid,
|
||||
title,
|
||||
summary,
|
||||
pesud,
|
||||
|
@ -18,6 +18,13 @@
|
||||
- Vue 3 [vuejs.org](https://vuejs.org)
|
||||
- Vite 6 [vitejs.dev](https://vite.dev)
|
||||
|
||||
废弃特性
|
||||
---
|
||||
|
||||
### 书签 {#deprecated-feature-bookmark}
|
||||
|
||||
因为底层 IndexedDB 更新困难和作品段落解析困难问题, 所以在 **v1.0.7** 以后的版本废弃了书签机制
|
||||
|
||||
其他
|
||||
---
|
||||
本站支持 "Server Side Rendering" by Vite SSR
|
||||
|
@ -14,17 +14,24 @@
|
||||
|
||||
现在这个站点还处于测试阶段, 只有一种使用方法 *(后面会扩展)*
|
||||
|
||||
首先你需要一个 AO3 链接
|
||||
首先你需要一个 AO3 链接 (或者数字 ID)
|
||||
比如:
|
||||
|
||||
https://archiveofourown.org/works/114514
|
||||
|
||||
带章节的话就是
|
||||
|
||||
https://archiveofourown.org/works/114514/chapters/1919810
|
||||
|
||||
|
||||
接着将前面的部分替换为本站点的链接 (保留数字ID部分):
|
||||
即:
|
||||
|
||||
https://ao3.unknownmp.top/work/114514
|
||||
|
||||
这里的数字ID即为`114514`
|
||||
章节:
|
||||
|
||||
https://ao3.unknownmp.top/work/114514/1919810
|
||||
|
||||
浏览器打开它, OK 你会用了🤓👆!
|
||||
|
||||
@ -33,7 +40,7 @@
|
||||
## 功能与特性 🤗
|
||||
|
||||
- ✅ 预览
|
||||
- ✅ 书签 (本地)
|
||||
- 📝 历史记录 (本地)
|
||||
- ✅ 作品详细数据
|
||||
- 📝 搜索
|
||||
- ❌ 不再支持! [详情](/about#deprecated-feature-bookmark) 书签 (本地)
|
||||
|
||||
|
@ -5,7 +5,6 @@ defineProps(['class']) // 接收外部 class 属性
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<!-- 将 class 传入 mdui-divider 中 -->
|
||||
<mdui-divider :class="['hr-divider', $attrs.class]"></mdui-divider>
|
||||
<template #ssr><hr :class="['hr-divider', $attrs.class]" /></template>
|
||||
</ClientOnly>
|
||||
|
@ -20,6 +20,7 @@ function convert(from) {
|
||||
}
|
||||
} else if (from.includes('https://archiveofourown.org/works/')) {
|
||||
const sid = from.split('https://archiveofourown.org/works/')[1];
|
||||
console.log(sid)
|
||||
if ( sid ) {
|
||||
const id = Number(sid)
|
||||
if (id) {
|
||||
@ -27,17 +28,24 @@ function convert(from) {
|
||||
type: 's',
|
||||
id
|
||||
}
|
||||
} else {
|
||||
const splited = sid.split('/chapters/')
|
||||
const id = Number(splited[0])
|
||||
const cid = Number(splited[1])
|
||||
if (id && cid) {
|
||||
return {
|
||||
type: 'c',
|
||||
id, cid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: null,
|
||||
}
|
||||
return { type: null }
|
||||
}
|
||||
|
||||
function onConvert(data) {
|
||||
const { type, id, cid } = convert(data.src)
|
||||
console.log(type, id, cid)
|
||||
if (type == null) {
|
||||
err.value = true
|
||||
srcText.value?.focus()
|
||||
|
@ -10,11 +10,8 @@ const workReadState = useWorkReadState()
|
||||
import { useRouteStore } from '@/stores/route.js'
|
||||
const routeState = useRouteStore()
|
||||
|
||||
import { useBookmarkStore } from '../stores/db.js'
|
||||
|
||||
import 'mdui/components/list.js'
|
||||
import 'mdui/components/list-item.js'
|
||||
import 'mdui/components/dialog.js'
|
||||
import 'mdui/components/divider.js'
|
||||
import 'mdui/components/linear-progress.js'
|
||||
import 'mdui/components/fab.js'
|
||||
@ -37,16 +34,11 @@ import { mduiSnackbar } from '../utils.js'
|
||||
const fabExtended = ref(false)
|
||||
const content = ref(null)
|
||||
const readPercent = ref(0)
|
||||
const bookmarkDialog = ref(null)
|
||||
const bookmarks = ref([])
|
||||
const bookmarkMenu = ref(false)
|
||||
const bookmarkSelect = ref(null)
|
||||
|
||||
let readIndex = 0
|
||||
let lastPercent = 0
|
||||
let lastCloseTimer = null
|
||||
let isObserver = null
|
||||
let bookmarkStore = null
|
||||
let paragraphs = []
|
||||
let currentParagraph = null
|
||||
|
||||
@ -56,100 +48,22 @@ const categoryName = {
|
||||
fm: '女/男'
|
||||
}
|
||||
|
||||
async function addBookmark() {
|
||||
if (currentParagraph) {
|
||||
const id = await bookmarkStore.add(workReadState.id, readIndex, currentParagraph.textContent.slice(0,20), '')
|
||||
bookmarks.value.push(await bookmarkStore.get(id))
|
||||
snackbar({
|
||||
message: `在第 ${readIndex} 段 (${readPercent.value}%) 处新建了一个书签`,
|
||||
action: "编辑",
|
||||
onActionClick: () => {
|
||||
prompt({
|
||||
headline: "修改书签",
|
||||
description: "新名字:",
|
||||
confirmText: "完成",
|
||||
cancelText: "算了",
|
||||
onConfirm: (value) => {
|
||||
bookmarkStore.updateName(id, value)
|
||||
bookmarks.value[bookmarks.value.length - 1].name = value
|
||||
}
|
||||
});
|
||||
}})
|
||||
}
|
||||
}
|
||||
|
||||
async function jumpTo(index) {
|
||||
const value = bookmarks.value[index].index
|
||||
const target = paragraphs[value]
|
||||
bookmarkDialog.value.open = false
|
||||
await nextTick()
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
inline: 'nearest'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function delAllBookmark() {
|
||||
confirm({
|
||||
headline: '警告',
|
||||
description: '这会清空所有书签! 不可恢复!',
|
||||
confirmText: '我明白',
|
||||
cancelText: '算了',
|
||||
closeOnOverlayClick: true,
|
||||
closeOnEsc: true,
|
||||
onConfirm: () => {
|
||||
bookmarkStore.delByWork(workReadState.id)
|
||||
bookmarks.value = []
|
||||
mduiSnackbar('书签清空辣!')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function editBookmark() {
|
||||
prompt({
|
||||
headline: "修改书签",
|
||||
description: "新名字:",
|
||||
confirmText: "完成",
|
||||
cancelText: "算了",
|
||||
onConfirm: (value) => {
|
||||
bookmarkStore.updateName(bookmarkSelect.value.bk.id, value)
|
||||
bookmarks.value[bookmarkSelect.value.index].name = value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openBookmarkMenu(bk, index) {
|
||||
bookmarkSelect.value = { bk, index };
|
||||
bookmarkMenu.value.open = true
|
||||
}
|
||||
|
||||
async function deleteBookmark() {
|
||||
if (bookmarkSelect.value) {
|
||||
bookmarkStore.del(bookmarkSelect.value.bk.id)
|
||||
bookmarks.value.splice(bookmarkSelect.value.index,1)
|
||||
bookmarkSelect.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
await workReadState.loadWork(route.params.id)
|
||||
await workReadState.loadWork(route.params.id, route.params.cid)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
bookmarkStore = useBookmarkStore()
|
||||
if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id)
|
||||
if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id, route.params.cid)
|
||||
if (workReadState.state == 'ready') {
|
||||
routeState.customTitle = workReadState.title
|
||||
const paraCount = workReadState.text.length - 2
|
||||
const paraCount = workReadState.text.length - 1
|
||||
isObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
currentParagraph = entry.target
|
||||
readIndex = entry.target.dataset.index;
|
||||
readPercent.value = parseInt(readIndex / paraCount * 100)
|
||||
readPercent.value = parseInt(readIndex / paraCount * 100)
|
||||
if (lastPercent == 0) {
|
||||
lastPercent = readPercent.value
|
||||
return
|
||||
@ -171,7 +85,6 @@ onMounted(async () => {
|
||||
await nextTick()
|
||||
paragraphs = content.value?.querySelectorAll('p');
|
||||
paragraphs?.forEach(p => isObserver.observe(p));
|
||||
bookmarks.value = await bookmarkStore.getAll(workReadState.id)
|
||||
}
|
||||
})
|
||||
|
||||
@ -189,7 +102,10 @@ onBeforeUnmount(() => {
|
||||
<template v-if="workReadState.state == 'notfound' || workReadState.state == 'ssrnotfound'">
|
||||
<h2>文章不存在...</h2>
|
||||
是不是链接没有复制完全?<br/>
|
||||
ID: {{workReadState.id}}<br/>
|
||||
ID: {{ workReadState.id }}<br/>
|
||||
<template v-if="workReadState.cid">
|
||||
CID: {{ workReadState.cid }}
|
||||
</template>
|
||||
<a @click="$router.back()">返回</a>
|
||||
</template>
|
||||
<template v-if="workReadState.state == 'ready'">
|
||||
@ -200,14 +116,14 @@ onBeforeUnmount(() => {
|
||||
<mdui-collapse-item value="info"><mdui-list-item class="infoblockhead" slot="header">
|
||||
作品信息
|
||||
</mdui-list-item><div class="infoblock"><dl>
|
||||
<dt>分类</dt><ul>
|
||||
<template v-if="workReadState.category"><dt>分类</dt><ul>
|
||||
<li v-for="item in workReadState.category" :key="item">
|
||||
{{ categoryName[item] }}</li>
|
||||
</ul>
|
||||
<dt>原著</dt><ul>
|
||||
</ul></template>
|
||||
<template v-if="workReadState.fandom"><dt>作品圈</dt><ul>
|
||||
<li v-for="item in workReadState.fandom" :key="item">
|
||||
{{ item }}</li>
|
||||
</ul>
|
||||
</ul></template>
|
||||
<dt>语言</dt><dd>
|
||||
{{ workReadState.lang }}
|
||||
</dd>
|
||||
@ -236,57 +152,33 @@ onBeforeUnmount(() => {
|
||||
<p v-for="(para, index) in workReadState.text" :key="para" :data-index="index">{{ para }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<mdui-fab class="mdui-fab" :extended="fabExtended" @click="bookmarkDialog.open = true">
|
||||
<mdui-fab class="mdui-fab" :extended="fabExtended">
|
||||
<mdui-icon-bookmark slot="icon"></mdui-icon-bookmark>
|
||||
{{ readPercent }}%
|
||||
</mdui-fab>
|
||||
<mdui-dialog ref='bookmarkDialog' close-on-overlay-click>
|
||||
<span slot="headline">书签</span>
|
||||
<span slot="description">
|
||||
共 {{ bookmarks.length }} 个
|
||||
<br/>
|
||||
点击跳转, 长按条目以 更新/删除
|
||||
</span>
|
||||
<mdui-list v-if="bookmarks.length" style="max-width: 50vh; max-height: 90vh;">
|
||||
<mdui-list-item
|
||||
v-for="(bk, index) in bookmarks"
|
||||
@click="jumpTo(index)"
|
||||
@contextmenu.prevent="openBookmarkMenu(bk, index)"
|
||||
>
|
||||
{{ bk.name || bk.para }}
|
||||
</mdui-list-item>
|
||||
</mdui-list>
|
||||
<span v-else>还没有书签</span>
|
||||
<mdui-dropdown ref='bookmarkMenu' trigger="manual" open-on-pointer>
|
||||
<span slot="trigger" />
|
||||
<mdui-menu>
|
||||
<mdui-menu-item @click="deleteBookmark()">删除</mdui-menu-item>
|
||||
<mdui-menu-item @click="editBookmark()">编辑</mdui-menu-item>
|
||||
</mdui-menu>
|
||||
</mdui-dropdown>
|
||||
<mdui-button slot="action" @click="delAllBookmark" variant="filled">清空</mdui-button>
|
||||
<mdui-button slot="action" @click="addBookmark" variant="text">新建</mdui-button>
|
||||
</mdui-dialog>
|
||||
</template>
|
||||
<template #ssr>
|
||||
<template v-if="workReadState.state == 'notfound' || workReadState.state == 'ssrnotfound'">
|
||||
<h2>文章不存在...</h2>
|
||||
是不是链接没有复制完全?<br/>
|
||||
ID: {{workReadState.id}}<br/>
|
||||
<template v-if="workReadState.cid">
|
||||
CID: {{ workReadState.cid }}
|
||||
</template>
|
||||
<a @click="$router.back()">返回</a>
|
||||
</template>
|
||||
<template v-if="workReadState.state == 'ready'">
|
||||
<h1>{{ workReadState.title }}</h1>
|
||||
<h2>{{ workReadState.pesud }}</h2>
|
||||
<dl>
|
||||
<dt>分类</dt><ul>
|
||||
<template v-if="workReadState.category"><dt>作品圈</dt><ul>
|
||||
<li v-for="item in workReadState.category" :key="item">
|
||||
{{ categoryName[item] }}</li>
|
||||
</ul>
|
||||
<dt>原著</dt><ul>
|
||||
</ul></template>
|
||||
<template v-if="workReadState.fandom"><dt>原著</dt><ul>
|
||||
<li v-for="item in workReadState.fandom" :key="item">
|
||||
{{ item }}</li>
|
||||
</ul>
|
||||
</ul></template>
|
||||
<dt>语言</dt><dd>
|
||||
{{ workReadState.lang }}
|
||||
</dd>
|
||||
|
@ -5,6 +5,8 @@ import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import markdown from 'vite-plugin-md'
|
||||
import markdownItAnchor from 'markdown-it-anchor'
|
||||
import markdownItAttrs from 'markdown-it-attrs'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
@ -20,8 +22,57 @@ export default defineConfig({
|
||||
vueJsx(),
|
||||
vueDevTools(),
|
||||
markdown({
|
||||
markdownItSetup(md) {
|
||||
md.renderer.rules.hr = () => "<Hr />"
|
||||
markdownItSetup(mdit) {
|
||||
mdit.use(markdownItAttrs)
|
||||
mdit.use(markdownItAnchor, {
|
||||
permalink: markdownItAnchor.permalink.ariaHidden({
|
||||
placement: 'before',
|
||||
symbol: '#',
|
||||
level: [1, 2, 3, 4],
|
||||
}),
|
||||
slugify: s => s
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "") // 去除重音符号
|
||||
.replace(/[^\w\s-]/g, "") // 移除 emoji 和特殊符号
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
})
|
||||
mdit.renderer.rules.hr = () => {
|
||||
console.log('Custom <hr> rendered 🚀');
|
||||
return '<div><BetterHr /></div>'
|
||||
}
|
||||
const defaultOpen = mdit.renderer.rules.link_open || ((tokens, idx, options, env, self) => {
|
||||
return self.renderToken(tokens, idx, options)
|
||||
})
|
||||
const defaultClose = mdit.renderer.rules.link_close || ((tokens, idx, options, env, self) => {
|
||||
return self.renderToken(tokens, idx, options)
|
||||
})
|
||||
mdit.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx]
|
||||
const href = token.attrGet('href') || ''
|
||||
const isExternal = /^https?:\/\//.test(href)
|
||||
const isInternal = /^\/(?!\/)/.test(href)
|
||||
if (isInternal) {
|
||||
// 转换为 <router-link> 并设置 `to`
|
||||
token.tag = 'router-link'
|
||||
token.attrSet('to', href)
|
||||
token.attrs = token.attrs?.filter(attr => attr[0] !== 'href') || []
|
||||
} else if (isExternal) {
|
||||
// 站外链接加上 target="_blank" rel="noopener noreferrer"
|
||||
token.attrSet('target', '_blank')
|
||||
token.attrSet('rel', 'noopener noreferrer')
|
||||
}
|
||||
|
||||
return defaultOpen(tokens, idx, options, env, self)
|
||||
}
|
||||
mdit.renderer.rules.link_close = (tokens, idx, options, env, self) => {
|
||||
const previous = tokens[idx - 1]
|
||||
if (previous?.tag === 'router-link') {
|
||||
tokens[idx].tag = 'router-link'
|
||||
}
|
||||
return defaultClose(tokens, idx, options, env, self)
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
|
Reference in New Issue
Block a user