第一次提交
This commit is contained in:
19
src/views/About.vue
Normal file
19
src/views/About.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<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>
|
||||
|
74
src/views/Developer.vue
Normal file
74
src/views/Developer.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useApiStore } from '@/stores/api.js'
|
||||
import { useRouteStore } from '../stores/route.js'
|
||||
|
||||
const router = useRouter()
|
||||
const api = useApiStore()
|
||||
const routeStore = useRouteStore()
|
||||
|
||||
const ua = ref('')
|
||||
const language = ref('')
|
||||
const platform = ref('')
|
||||
const screenSize = ref('')
|
||||
const viewportSize = ref('')
|
||||
const pixelRatio = ref(1)
|
||||
const touchSupport = ref(false)
|
||||
const visibility = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
ua.value = navigator.userAgent
|
||||
language.value = navigator.language
|
||||
platform.value = navigator.platform
|
||||
screenSize.value = `${screen.width} × ${screen.height}`
|
||||
viewportSize.value = `${window.innerWidth} × ${window.innerHeight}`
|
||||
pixelRatio.value = window.devicePixelRatio
|
||||
touchSupport.value = 'ontouchstart' in window
|
||||
visibility.value = document.visibilityState
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<h1 class="warn-text">注意本页面仅为测试用!</h1>
|
||||
<blockquote>
|
||||
当然如果你是乱点那个唐鬼小人进来的, 这就是个彩蛋? (神金
|
||||
</blockquote>
|
||||
<Hr />
|
||||
<section>
|
||||
<h2>页面信息</h2>
|
||||
<h3>可见路由信息:</h3>
|
||||
<pre>{{ routeStore.allRoutes }}</pre>
|
||||
</section>
|
||||
<Hr />
|
||||
<ClientOnly>
|
||||
<section>
|
||||
<h2>浏览器信息</h2>
|
||||
<dl>
|
||||
<dt>User-Agent:</dt>
|
||||
<dd>{{ ua }}</dd>
|
||||
<dt>语言:</dt>
|
||||
<dd>{{ language }}</dd>
|
||||
<dt>平台:</dt>
|
||||
<dd>{{ platform }}</dd>
|
||||
<dt>屏幕尺寸:</dt>
|
||||
<dd>{{ screenSize }}</dd>
|
||||
<dt>视口尺寸:</dt>
|
||||
<dd>{{ viewportSize }}</dd>
|
||||
<dt>像素比:</dt>
|
||||
<dd>{{ pixelRatio }}</dd>
|
||||
<dt>是否触屏设备:</dt>
|
||||
<dd>{{ touchSupport ? '是' : '否' }}</dd>
|
||||
<dt>页面可见性状态:</dt>
|
||||
<dd>{{ visibility }}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<Hr />
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
</style>
|
24
src/views/Mask.vue
Normal file
24
src/views/Mask.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onServerPrefetch, onBeforeMount} from 'vue'
|
||||
import { useRouter, useRoute, RouterView } from 'vue-router'
|
||||
const router = useRouter()
|
||||
import { useApiStore } from '@/stores/api.js'
|
||||
const api = useApiStore()
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
// Load data
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
// Re apply data
|
||||
})
|
||||
onMounted(() => {
|
||||
// Render other
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Padding! No content cause hydration mismatch!
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
72
src/views/Root.vue
Normal file
72
src/views/Root.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import 'mdui/components/text-field.js'
|
||||
import 'mdui/components/button.js'
|
||||
|
||||
import Intro from '../texts/intro.md'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const src = ref('')
|
||||
const srcText = ref(null)
|
||||
const err = ref(false)
|
||||
|
||||
function convert(from) {
|
||||
if( Number(from) ) {
|
||||
return {
|
||||
id: Number(from)
|
||||
}
|
||||
} else if (from.includes('https://archiveofourown.org/works/')) {
|
||||
const sid = from.split('https://archiveofourown.org/works/')[1];
|
||||
if ( sid ) {
|
||||
const id = Number(sid)
|
||||
if (id) {
|
||||
return {
|
||||
type: 's',
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: null,
|
||||
}
|
||||
}
|
||||
|
||||
function onConvert() {
|
||||
const { id, cid } = convert(src.value)
|
||||
if (id == null) {
|
||||
err.value = true
|
||||
srcText.value?.focus()
|
||||
} else {
|
||||
err.value = false
|
||||
if (cid) router.push(`/work/${id}/${cid}`)
|
||||
else router.push(`/work/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img style="display: block; margin: 0px auto 10px;" height="200px" alt="logo" src="/favicon.svg" />
|
||||
<Intro />
|
||||
<br/><Hr/>
|
||||
<section id="converter">
|
||||
<h2>链接转换</h2>
|
||||
<p>输入完整链接或者 ID</p>
|
||||
<ClientOnly>
|
||||
<mdui-text-field variant="filled" label="链接" placeholder="https://archiveofourown.org/works/114514" @input="src = $event.target.value" ref='srcText'>
|
||||
<span v-if='err' slot="helper" class='warn-text'>链接格式错误!</span>
|
||||
</mdui-text-field><br/>
|
||||
<div style="display: flex">
|
||||
<div style="flex-grow: 1"></div>
|
||||
<mdui-button @click='onConvert'>-></mdui-button>
|
||||
</div>
|
||||
{{ src }}
|
||||
<template #ssr>
|
||||
Padding...
|
||||
</template></ClientOnly>
|
||||
</section>
|
||||
</template>
|
257
src/views/Work.vue
Normal file
257
src/views/Work.vue
Normal file
@ -0,0 +1,257 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onServerPrefetch, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute, RouterView } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
import { useWorkReadState } from '@/stores/workRead.js'
|
||||
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'
|
||||
import 'mdui/components/button.js'
|
||||
import 'mdui/components/dropdown.js'
|
||||
import 'mdui/components/menu.js'
|
||||
import 'mdui/components/menu-item.js'
|
||||
|
||||
import '@mdui/icons/bookmark.js'
|
||||
|
||||
import { confirm } from 'mdui/functions/confirm.js'
|
||||
import { snackbar } from 'mdui/functions/snackbar.js'
|
||||
import { prompt } from 'mdui/functions/prompt.js'
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
bookmarkSelect.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
await workReadState.loadWork(route.params.id)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
bookmarkStore = useBookmarkStore()
|
||||
if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id)
|
||||
if (workReadState.state == 'ready') {
|
||||
routeState.customTitle = workReadState.title
|
||||
const paraCount = workReadState.text.length - 2
|
||||
isObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
currentParagraph = entry.target
|
||||
readIndex = entry.target.dataset.index;
|
||||
readPercent.value = parseInt(readIndex / paraCount * 100)
|
||||
if (lastPercent == 0) {
|
||||
lastPercent = readPercent.value
|
||||
return
|
||||
}
|
||||
if (Math.abs(lastPercent - readPercent.value) > 10) {
|
||||
lastPercent = readPercent.value
|
||||
fabExtended.value = true
|
||||
if (lastCloseTimer) clearTimeout(lastCloseTimer)
|
||||
lastCloseTimer = setTimeout(() => {
|
||||
fabExtended.value = false
|
||||
lastPercent = readPercent.value
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, {
|
||||
threshold: 0.5
|
||||
})
|
||||
await nextTick()
|
||||
paragraphs = content.value?.querySelectorAll('p');
|
||||
paragraphs?.forEach(p => isObserver.observe(p));
|
||||
bookmarks.value = await bookmarkStore.getAll(workReadState.id)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isObserver.disconnect();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<template v-if="workReadState.state == 'loading'">
|
||||
加载中...<br/>
|
||||
<mdui-linear-progress></mdui-linear-progress>
|
||||
</template>
|
||||
<template v-if="workReadState.state == 'notfound' || workReadState.state == 'ssrnotfound'">
|
||||
<h2>文章不存在...</h2>
|
||||
是不是链接没有复制完全?<br/>
|
||||
ID: {{workReadState.id}}<br/>
|
||||
<a @click="$router.back()">返回</a>
|
||||
</template>
|
||||
<template v-if="workReadState.state == 'ready'">
|
||||
<article>
|
||||
<h1 style="margin: auto">{{ workReadState.title }}</h1>
|
||||
<h4>{{ workReadState.pesud }}</h4>
|
||||
<blockquote>
|
||||
<p v-for="para in workReadState.summary" :key="para" v-html='para'></p>
|
||||
</blockquote>
|
||||
<Hr/>
|
||||
<div ref='content'>
|
||||
<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-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/>
|
||||
<a @click="$router.back()">返回</a>
|
||||
</template>
|
||||
<template v-if="workReadState.state == 'ready'">
|
||||
<h1>{{ workReadState.title }}</h1>
|
||||
<h2>{{ workReadState.pesud }}</h2>
|
||||
<blockquote>
|
||||
<p v-for="para in workReadState.summary" :key="para" v-html='para'></p>
|
||||
</blockquote>
|
||||
<Hr/>
|
||||
<p v-for="para in workReadState.text.slice(0, 10)" :key="para">{{ para }}</p>
|
||||
</template>
|
||||
</template></ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mdui-fab {
|
||||
position: fixed;
|
||||
bottom: 16px; /* 调整垂直位置 */
|
||||
right: 16px; /* 调整水平位置 */
|
||||
z-index: 1000; /* 确保悬浮按钮在其他内容上方 */
|
||||
animation: slideInFromRight var(--mdui-motion-duration-medium2) var(--mdui-motion-easing-standard); /* 动画时长和缓动效果 */
|
||||
}
|
||||
</style>
|
11
src/views/fallback/NotFound.vue
Normal file
11
src/views/fallback/NotFound.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
import 'mdui/components/divider.js'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>404 - 页面未找到</h1>
|
||||
<p>抱歉,您访问的页面不存在。</p>
|
||||
<router-link to="/">返回主页</router-link>
|
||||
<Hr/>
|
||||
Page of AO3 Mirror
|
||||
</template>
|
Reference in New Issue
Block a user