移除书签机制 更新了项目描述 增加对有章节作品阅读支持 [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",
|
"compress-json": "^3.1.1",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"idb": "^8.0.3",
|
|
||||||
"mdui": "^2.1.3",
|
"mdui": "^2.1.3",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
@ -23,6 +22,8 @@
|
|||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"markdown-it-anchor": "^9.2.0",
|
||||||
|
"markdown-it-attrs": "^4.3.1",
|
||||||
"sass": "^1.88.0",
|
"sass": "^1.88.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-md": "^0.21.5",
|
"vite-plugin-md": "^0.21.5",
|
||||||
@ -3797,11 +3798,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.2.tgz",
|
||||||
@ -4144,6 +4140,28 @@
|
|||||||
"markdown-it": "bin/markdown-it.js"
|
"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": {
|
"node_modules/markdown-it/node_modules/entities": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz",
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"compress-json": "^3.1.1",
|
"compress-json": "^3.1.1",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"idb": "^8.0.3",
|
|
||||||
"mdui": "^2.1.3",
|
"mdui": "^2.1.3",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
@ -26,6 +25,8 @@
|
|||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"markdown-it-anchor": "^9.2.0",
|
||||||
|
"markdown-it-attrs": "^4.3.1",
|
||||||
"sass": "^1.88.0",
|
"sass": "^1.88.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-md": "^0.21.5",
|
"vite-plugin-md": "^0.21.5",
|
||||||
|
@ -14,6 +14,7 @@ export function createApp() {
|
|||||||
app
|
app
|
||||||
.component('ClientOnly', ClientOnly)
|
.component('ClientOnly', ClientOnly)
|
||||||
.component('Hr', Hr)
|
.component('Hr', Hr)
|
||||||
|
.component('BetterHr', Hr)
|
||||||
.component('Form', Form)
|
.component('Form', Form)
|
||||||
return { app, pinia }
|
return { app, pinia }
|
||||||
}
|
}
|
||||||
|
@ -3,20 +3,40 @@ import { createMemoryHistory, createWebHistory, createRouter } from 'vue-router'
|
|||||||
export function createSSRRouter() {
|
export function createSSRRouter() {
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
|
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: [{
|
routes: [{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: '前言',
|
name: '前言',
|
||||||
component: () => import('./views/Root.vue'),
|
component: () => import('./views/Root.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: "首页",
|
title: '首页',
|
||||||
order: 1
|
order: 1
|
||||||
},
|
},
|
||||||
},{
|
},{
|
||||||
path: '/work/:id',
|
path: '/work/:id',
|
||||||
name: '阅读',
|
name: 'work',
|
||||||
component: () => import('./views/Work.vue'),
|
component: () => import('./views/Work.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: "",
|
title: '阅读',
|
||||||
|
hidden: true
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
path: '/work/:id/:cid',
|
||||||
|
name: 'workChapter',
|
||||||
|
component: () => import('./views/Work.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '阅读',
|
||||||
hidden: true
|
hidden: true
|
||||||
}
|
}
|
||||||
},{
|
},{
|
||||||
@ -24,7 +44,7 @@ export function createSSRRouter() {
|
|||||||
name: '关于',
|
name: '关于',
|
||||||
component: () => import('./views/About.vue'),
|
component: () => import('./views/About.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: "",
|
title: '',
|
||||||
order: 2
|
order: 2
|
||||||
},
|
},
|
||||||
},{
|
},{
|
||||||
@ -32,7 +52,7 @@ export function createSSRRouter() {
|
|||||||
name: '开发人员选项',
|
name: '开发人员选项',
|
||||||
component: () => import('./views/Developer.vue'),
|
component: () => import('./views/Developer.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: "",
|
title: '',
|
||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
},{
|
},{
|
||||||
@ -40,7 +60,7 @@ export function createSSRRouter() {
|
|||||||
name: 'NotFound',
|
name: 'NotFound',
|
||||||
component: () => import('./views/fallback/NotFound.vue'),
|
component: () => import('./views/fallback/NotFound.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: "页面未找到",
|
title: '页面未找到',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
code: 404
|
code: 404
|
||||||
}
|
}
|
||||||
|
@ -102,8 +102,9 @@ export const useApiStore = defineStore('api', () => {
|
|||||||
inited = false
|
inited = false
|
||||||
await init()
|
await init()
|
||||||
}
|
}
|
||||||
async function getWork(workId) {
|
async function getWork(workId, chapterId) {
|
||||||
return await apiGet('work',{ workId })
|
if (chapterId) return await apiGet(`work/${workId}/${chapterId}`)
|
||||||
|
return await apiGet(`work/${workId}`)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
init,
|
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', () => {
|
export const useWorkReadState = defineStore('workRead', () => {
|
||||||
const api = useApiStore()
|
const api = useApiStore()
|
||||||
const id = ref(null)
|
const id = ref(null)
|
||||||
|
const cid = ref(null)
|
||||||
const summary = ref(null)
|
const summary = ref(null)
|
||||||
const pesud = ref(null)
|
const pesud = ref(null)
|
||||||
const title = ref(null)
|
const title = ref(null)
|
||||||
@ -25,7 +26,7 @@ export const useWorkReadState = defineStore('workRead', () => {
|
|||||||
title.value = data.title
|
title.value = data.title
|
||||||
summary.value = [escapeAndFormatText(data.summary)]
|
summary.value = [escapeAndFormatText(data.summary)]
|
||||||
pesud.value = data.pesud
|
pesud.value = data.pesud
|
||||||
text.value = data.text.split('\n\n')
|
text.value = data.text
|
||||||
publishedTime.value = data.stats.publishedTime
|
publishedTime.value = data.stats.publishedTime
|
||||||
wordCount.value = data.stats.wordCount
|
wordCount.value = data.stats.wordCount
|
||||||
kudoCount.value = data.stats.kudoCount
|
kudoCount.value = data.stats.kudoCount
|
||||||
@ -34,10 +35,10 @@ export const useWorkReadState = defineStore('workRead', () => {
|
|||||||
fandom.value = data.fandom
|
fandom.value = data.fandom
|
||||||
lang.value = data.lang
|
lang.value = data.lang
|
||||||
}
|
}
|
||||||
async function loadWork(target) {
|
async function loadWork(target, targetc) {
|
||||||
if (target == id.value || state.value == 'loading') return
|
if (target == id.value && targetc == cid.value || state.value == 'loading') return
|
||||||
state.value = 'loading'
|
state.value = 'loading'
|
||||||
const result = await api.getWork(target)
|
const result = await api.getWork(target, targetc)
|
||||||
if (result.status == 200) {
|
if (result.status == 200) {
|
||||||
setData(result.data)
|
setData(result.data)
|
||||||
state.value = 'ready'
|
state.value = 'ready'
|
||||||
@ -47,7 +48,7 @@ export const useWorkReadState = defineStore('workRead', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id,
|
id, cid,
|
||||||
title,
|
title,
|
||||||
summary,
|
summary,
|
||||||
pesud,
|
pesud,
|
||||||
|
@ -18,6 +18,13 @@
|
|||||||
- Vue 3 [vuejs.org](https://vuejs.org)
|
- Vue 3 [vuejs.org](https://vuejs.org)
|
||||||
- Vite 6 [vitejs.dev](https://vite.dev)
|
- Vite 6 [vitejs.dev](https://vite.dev)
|
||||||
|
|
||||||
|
废弃特性
|
||||||
|
---
|
||||||
|
|
||||||
|
### 书签 {#deprecated-feature-bookmark}
|
||||||
|
|
||||||
|
因为底层 IndexedDB 更新困难和作品段落解析困难问题, 所以在 **v1.0.7** 以后的版本废弃了书签机制
|
||||||
|
|
||||||
其他
|
其他
|
||||||
---
|
---
|
||||||
本站支持 "Server Side Rendering" by Vite SSR
|
本站支持 "Server Side Rendering" by Vite SSR
|
||||||
|
@ -14,17 +14,24 @@
|
|||||||
|
|
||||||
现在这个站点还处于测试阶段, 只有一种使用方法 *(后面会扩展)*
|
现在这个站点还处于测试阶段, 只有一种使用方法 *(后面会扩展)*
|
||||||
|
|
||||||
首先你需要一个 AO3 链接
|
首先你需要一个 AO3 链接 (或者数字 ID)
|
||||||
比如:
|
比如:
|
||||||
|
|
||||||
https://archiveofourown.org/works/114514
|
https://archiveofourown.org/works/114514
|
||||||
|
|
||||||
|
带章节的话就是
|
||||||
|
|
||||||
|
https://archiveofourown.org/works/114514/chapters/1919810
|
||||||
|
|
||||||
|
|
||||||
接着将前面的部分替换为本站点的链接 (保留数字ID部分):
|
接着将前面的部分替换为本站点的链接 (保留数字ID部分):
|
||||||
即:
|
即:
|
||||||
|
|
||||||
https://ao3.unknownmp.top/work/114514
|
https://ao3.unknownmp.top/work/114514
|
||||||
|
|
||||||
这里的数字ID即为`114514`
|
章节:
|
||||||
|
|
||||||
|
https://ao3.unknownmp.top/work/114514/1919810
|
||||||
|
|
||||||
浏览器打开它, OK 你会用了🤓👆!
|
浏览器打开它, OK 你会用了🤓👆!
|
||||||
|
|
||||||
@ -33,7 +40,7 @@
|
|||||||
## 功能与特性 🤗
|
## 功能与特性 🤗
|
||||||
|
|
||||||
- ✅ 预览
|
- ✅ 预览
|
||||||
- ✅ 书签 (本地)
|
- ✅ 作品详细数据
|
||||||
- 📝 历史记录 (本地)
|
|
||||||
- 📝 搜索
|
- 📝 搜索
|
||||||
|
- ❌ 不再支持! [详情](/about#deprecated-feature-bookmark) 书签 (本地)
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ defineProps(['class']) // 接收外部 class 属性
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<!-- 将 class 传入 mdui-divider 中 -->
|
|
||||||
<mdui-divider :class="['hr-divider', $attrs.class]"></mdui-divider>
|
<mdui-divider :class="['hr-divider', $attrs.class]"></mdui-divider>
|
||||||
<template #ssr><hr :class="['hr-divider', $attrs.class]" /></template>
|
<template #ssr><hr :class="['hr-divider', $attrs.class]" /></template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
@ -20,6 +20,7 @@ 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) {
|
||||||
@ -27,17 +28,24 @@ function convert(from) {
|
|||||||
type: 's',
|
type: 's',
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
const splited = sid.split('/chapters/')
|
||||||
}
|
const id = Number(splited[0])
|
||||||
|
const cid = Number(splited[1])
|
||||||
|
if (id && cid) {
|
||||||
return {
|
return {
|
||||||
type: null,
|
type: 'c',
|
||||||
|
id, cid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: null }
|
||||||
|
}
|
||||||
|
|
||||||
function onConvert(data) {
|
function onConvert(data) {
|
||||||
const { type, id, cid } = convert(data.src)
|
const { type, id, cid } = convert(data.src)
|
||||||
console.log(type, id, cid)
|
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
err.value = true
|
err.value = true
|
||||||
srcText.value?.focus()
|
srcText.value?.focus()
|
||||||
|
@ -10,11 +10,8 @@ const workReadState = useWorkReadState()
|
|||||||
import { useRouteStore } from '@/stores/route.js'
|
import { useRouteStore } from '@/stores/route.js'
|
||||||
const routeState = useRouteStore()
|
const routeState = useRouteStore()
|
||||||
|
|
||||||
import { useBookmarkStore } from '../stores/db.js'
|
|
||||||
|
|
||||||
import 'mdui/components/list.js'
|
import 'mdui/components/list.js'
|
||||||
import 'mdui/components/list-item.js'
|
import 'mdui/components/list-item.js'
|
||||||
import 'mdui/components/dialog.js'
|
|
||||||
import 'mdui/components/divider.js'
|
import 'mdui/components/divider.js'
|
||||||
import 'mdui/components/linear-progress.js'
|
import 'mdui/components/linear-progress.js'
|
||||||
import 'mdui/components/fab.js'
|
import 'mdui/components/fab.js'
|
||||||
@ -37,16 +34,11 @@ import { mduiSnackbar } from '../utils.js'
|
|||||||
const fabExtended = ref(false)
|
const fabExtended = ref(false)
|
||||||
const content = ref(null)
|
const content = ref(null)
|
||||||
const readPercent = ref(0)
|
const readPercent = ref(0)
|
||||||
const bookmarkDialog = ref(null)
|
|
||||||
const bookmarks = ref([])
|
|
||||||
const bookmarkMenu = ref(false)
|
|
||||||
const bookmarkSelect = ref(null)
|
|
||||||
|
|
||||||
let readIndex = 0
|
let readIndex = 0
|
||||||
let lastPercent = 0
|
let lastPercent = 0
|
||||||
let lastCloseTimer = null
|
let lastCloseTimer = null
|
||||||
let isObserver = null
|
let isObserver = null
|
||||||
let bookmarkStore = null
|
|
||||||
let paragraphs = []
|
let paragraphs = []
|
||||||
let currentParagraph = null
|
let currentParagraph = null
|
||||||
|
|
||||||
@ -56,94 +48,16 @@ const categoryName = {
|
|||||||
fm: '女/男'
|
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 () => {
|
onServerPrefetch(async () => {
|
||||||
await workReadState.loadWork(route.params.id)
|
await workReadState.loadWork(route.params.id, route.params.cid)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
bookmarkStore = useBookmarkStore()
|
if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id, route.params.cid)
|
||||||
if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id)
|
|
||||||
if (workReadState.state == 'ready') {
|
if (workReadState.state == 'ready') {
|
||||||
routeState.customTitle = workReadState.title
|
routeState.customTitle = workReadState.title
|
||||||
const paraCount = workReadState.text.length - 2
|
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) {
|
||||||
@ -171,7 +85,6 @@ onMounted(async () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
paragraphs = content.value?.querySelectorAll('p');
|
paragraphs = content.value?.querySelectorAll('p');
|
||||||
paragraphs?.forEach(p => isObserver.observe(p));
|
paragraphs?.forEach(p => isObserver.observe(p));
|
||||||
bookmarks.value = await bookmarkStore.getAll(workReadState.id)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -190,6 +103,9 @@ onBeforeUnmount(() => {
|
|||||||
<h2>文章不存在...</h2>
|
<h2>文章不存在...</h2>
|
||||||
是不是链接没有复制完全?<br/>
|
是不是链接没有复制完全?<br/>
|
||||||
ID: {{ workReadState.id }}<br/>
|
ID: {{ workReadState.id }}<br/>
|
||||||
|
<template v-if="workReadState.cid">
|
||||||
|
CID: {{ workReadState.cid }}
|
||||||
|
</template>
|
||||||
<a @click="$router.back()">返回</a>
|
<a @click="$router.back()">返回</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="workReadState.state == 'ready'">
|
<template v-if="workReadState.state == 'ready'">
|
||||||
@ -200,14 +116,14 @@ onBeforeUnmount(() => {
|
|||||||
<mdui-collapse-item value="info"><mdui-list-item class="infoblockhead" slot="header">
|
<mdui-collapse-item value="info"><mdui-list-item class="infoblockhead" slot="header">
|
||||||
作品信息
|
作品信息
|
||||||
</mdui-list-item><div class="infoblock"><dl>
|
</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">
|
<li v-for="item in workReadState.category" :key="item">
|
||||||
{{ categoryName[item] }}</li>
|
{{ categoryName[item] }}</li>
|
||||||
</ul>
|
</ul></template>
|
||||||
<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>
|
</ul></template>
|
||||||
<dt>语言</dt><dd>
|
<dt>语言</dt><dd>
|
||||||
{{ workReadState.lang }}
|
{{ workReadState.lang }}
|
||||||
</dd>
|
</dd>
|
||||||
@ -236,57 +152,33 @@ onBeforeUnmount(() => {
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</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>
|
<mdui-icon-bookmark slot="icon"></mdui-icon-bookmark>
|
||||||
{{ readPercent }}%
|
{{ readPercent }}%
|
||||||
</mdui-fab>
|
</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>
|
||||||
<template #ssr>
|
<template #ssr>
|
||||||
<template v-if="workReadState.state == 'notfound' || workReadState.state == 'ssrnotfound'">
|
<template v-if="workReadState.state == 'notfound' || workReadState.state == 'ssrnotfound'">
|
||||||
<h2>文章不存在...</h2>
|
<h2>文章不存在...</h2>
|
||||||
是不是链接没有复制完全?<br/>
|
是不是链接没有复制完全?<br/>
|
||||||
ID: {{workReadState.id}}<br/>
|
ID: {{workReadState.id}}<br/>
|
||||||
|
<template v-if="workReadState.cid">
|
||||||
|
CID: {{ workReadState.cid }}
|
||||||
|
</template>
|
||||||
<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>
|
<h1>{{ workReadState.title }}</h1>
|
||||||
<h2>{{ workReadState.pesud }}</h2>
|
<h2>{{ workReadState.pesud }}</h2>
|
||||||
<dl>
|
<dl>
|
||||||
<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>
|
</ul></template>
|
||||||
<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>
|
</ul></template>
|
||||||
<dt>语言</dt><dd>
|
<dt>语言</dt><dd>
|
||||||
{{ workReadState.lang }}
|
{{ workReadState.lang }}
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -5,6 +5,8 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
import markdown from 'vite-plugin-md'
|
import markdown from 'vite-plugin-md'
|
||||||
|
import markdownItAnchor from 'markdown-it-anchor'
|
||||||
|
import markdownItAttrs from 'markdown-it-attrs'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -20,8 +22,57 @@ export default defineConfig({
|
|||||||
vueJsx(),
|
vueJsx(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
markdown({
|
markdown({
|
||||||
markdownItSetup(md) {
|
markdownItSetup(mdit) {
|
||||||
md.renderer.rules.hr = () => "<Hr />"
|
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