Migrate remaining class/decorator Vue components to Composition API (#3)

* Plan: migrate class-based Vue components to Composition API

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Migrate legacy class-based Vue components to Composition API

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Fix BlogIndex tag key uniqueness after review

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Apply validation feedback and finalize composition migration

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Remove package-lock and validate project with Bun lockfile

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/ee17e721-3915-4dfb-8ccc-cf9b0b715fb0

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Normalize bun.lockb file mode

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/ee17e721-3915-4dfb-8ccc-cf9b0b715fb0

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>
This commit is contained in:
Copilot
2026-04-24 04:44:58 +08:00
committed by GitHub
parent f25f468c99
commit ea9c12eda5
13 changed files with 288 additions and 373 deletions
BIN
View File
Binary file not shown.
-1
View File
@@ -18,7 +18,6 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"tg-blog": "^1.1.7", "tg-blog": "^1.1.7",
"vue": "^3.4.35", "vue": "^3.4.35",
"vue-facing-decorator": "^4.0.1",
"vue-i18n": "^11.1.12", "vue-i18n": "^11.1.12",
"vue-router": "^4.4.0" "vue-router": "^4.4.0"
}, },
+81 -85
View File
@@ -1,21 +1,21 @@
<template> <template>
<div id="nav" class="fbox-v" <div id="nav" class="fbox-v"
:class="(currentRoute) + ' ' + (menuOpen ? 'open' : '')" :class="(currentRoute) + ' ' + (menuOpen ? 'open' : '')"
v-if="currentRoute !== 'colorpicker'"> v-if="currentRoute !== 'colorpicker'">
<div id="menu" @click="showMenu"><i class="fas fa-bars"></i></div> <div id="menu" @click="showMenu"><i class="fas fa-bars"></i></div>
<div id="items" class="fbox-v"> <div id="items" class="fbox-v">
<router-link class="router-link" ref="others" to="/others">{{ $t('nav.others') }}</router-link> <router-link class="router-link" :ref="setNavRef('others')" to="/others">{{ $t('nav.others') }}</router-link>
<div class="dot">·</div> <div class="dot">·</div>
<router-link class="router-link" ref="photo" to="/photo">{{ $t('nav.photo') }}</router-link> <router-link class="router-link" :ref="setNavRef('photo')" to="/photo">{{ $t('nav.photo') }}</router-link>
<div class="dot">·</div> <div class="dot">·</div>
<router-link class="router-link" ref="blog" to="/blog">{{ $t('nav.blog') }}</router-link> <router-link class="router-link" :ref="setNavRef('blog')" to="/blog">{{ $t('nav.blog') }}</router-link>
<div class="dot">·</div> <div class="dot">·</div>
<router-link class="router-link" ref="life" to="/life">{{ $t('nav.life') }}</router-link> <router-link class="router-link" :ref="setNavRef('life')" to="/life">{{ $t('nav.life') }}</router-link>
<div class="dot">·</div> <div class="dot">·</div>
<router-link class="router-link" ref="about" to="/about">{{ $t('nav.about') }}</router-link> <router-link class="router-link" :ref="setNavRef('about')" to="/about">{{ $t('nav.about') }}</router-link>
<div class="dot">·</div> <div class="dot">·</div>
<router-link class="router-link" ref="home" to="/"> <router-link class="router-link" :ref="setNavRef('home')" to="/">
<svg focusable="false" data-prefix="fal" data-icon="house-night" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="svg-inline--fa fa-house-night fa-w-20"><path fill="currentColor" d="M112,224a111.5,111.5,0,0,0,87-41.45,20.51,20.51,0,0,0-19.75-33.08A59.2,59.2,0,0,1,138.84,39.85a20.3,20.3,0,0,0,10.07-21.27,20.26,20.26,0,0,0-16.47-16.7A136,136,0,0,0,112,0a112,112,0,0,0,0,224ZM97.78,33.27a91.21,91.21,0,0,0,54.47,147.9A80,80,0,1,1,97.78,33.27Zm97.15,35.51,39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L317,68.78a5.33,5.33,0,0,0,0-9.54L277.32,42.68,260.76,3a5.33,5.33,0,0,0-9.55,0L234.65,42.68,194.93,59.24a5.34,5.34,0,0,0,0,9.54ZM157,379.24l-39.72-16.57L100.76,323a5.34,5.34,0,0,0-9.55,0L74.65,362.67,34.93,379.24a5.34,5.34,0,0,0,0,9.54l39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L157,388.78a5.33,5.33,0,0,0,0-9.54Zm179-101.9v85.33A21.39,21.39,0,0,0,357.36,384h85.31A21.39,21.39,0,0,0,464,362.67V277.34A21.4,21.4,0,0,0,442.67,256H357.36A21.4,21.4,0,0,0,336,277.34ZM368,288H432v64H368Zm266.49,8L576,244.75V144a16,16,0,0,0-32,0v72.75L410.53,100a16,16,0,0,0-21.07,0l-224,196a16,16,0,0,0,21.07,24.09L224,287.28V464a48.05,48.05,0,0,0,48,48H528a48.06,48.06,0,0,0,48-48V287.28l37.46,32.78A16,16,0,0,0,634.53,296ZM544,464a16,16,0,0,1-16,16H272a16,16,0,0,1-16-16V264a15.94,15.94,0,0,0-.81-4L400,133.27l144,126Z" class=""></path></svg></router-link> <svg focusable="false" data-prefix="fal" data-icon="house-night" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="svg-inline--fa fa-house-night fa-w-20"><path fill="currentColor" d="M112,224a111.5,111.5,0,0,0,87-41.45,20.51,20.51,0,0,0-19.75-33.08A59.2,59.2,0,0,1,138.84,39.85a20.3,20.3,0,0,0,10.07-21.27,20.26,20.26,0,0,0-16.47-16.7A136,136,0,0,0,112,0a112,112,0,0,0,0,224ZM97.78,33.27a91.21,91.21,0,0,0,54.47,147.9A80,80,0,1,1,97.78,33.27Zm97.15,35.51,39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L317,68.78a5.33,5.33,0,0,0,0-9.54L277.32,42.68,260.76,3a5.33,5.33,0,0,0-9.55,0L234.65,42.68,194.93,59.24a5.34,5.34,0,0,0,0,9.54ZM157,379.24l-39.72-16.57L100.76,323a5.34,5.34,0,0,0-9.55,0L74.65,362.67,34.93,379.24a5.34,5.34,0,0,0,0,9.54l39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L157,388.78a5.33,5.33,0,0,0,0-9.54Zm179-101.9v85.33A21.39,21.39,0,0,0,357.36,384h85.31A21.39,21.39,0,0,0,464,362.67V277.34A21.4,21.4,0,0,0,442.67,256H357.36A21.4,21.4,0,0,0,336,277.34ZM368,288H432v64H368Zm266.49,8L576,244.75V144a16,16,0,0,0-32,0v72.75L410.53,100a16,16,0,0,0-21.07,0l-224,196a16,16,0,0,0,21.07,24.09L224,287.28V464a48.05,48.05,0,0,0,48,48H528a48.06,48.06,0,0,0,48-48V287.28l37.46,32.78A16,16,0,0,0,634.53,296ZM544,464a16,16,0,0,1-16,16H272a16,16,0,0,1-16-16V264a15.94,15.94,0,0,0-.81-4L400,133.27l144,126Z" class=""></path></svg></router-link>
</div> </div>
@@ -28,92 +28,88 @@
<router-view/> <router-view/>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { ComponentPublicInstance } from 'vue'; import {nextTick, onMounted, onUnmounted, ref, ComponentPublicInstance} from 'vue';
import { Component, Vue, toNative } from 'vue-facing-decorator'
import router from "@/scripts/router"; import router from "@/scripts/router";
import { RouteLocationNormalized, RouteLocationNormalizedLoaded, Router } from "vue-router"; import {RouteLocationNormalized, RouteLocationRaw, useRoute} from "vue-router";
import { TranslateResult } from "vue-i18n";
@Component const route = useRoute()
class App extends Vue
{
currentRoute = ''
currentLink: Element = null as never as Element
bookmarkCss = ""
bookmarkUpdateIntervalId!: number
lastTop = 0
menuOpen = false const currentRoute = ref('')
const bookmarkCss = ref('')
const lastTop = ref(0)
const menuOpen = ref(false)
const bookmarkUpdateIntervalId = ref<number | null>(null)
const removeAfterEach = ref<(() => void) | null>(null)
declare $t: (arg: string) => TranslateResult const navRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({})
declare $route: RouteLocationNormalizedLoaded
declare $router: Router
showMenu(): void const showMenu = (): void => {
{ menuOpen.value = !menuOpen.value
this.menuOpen = !this.menuOpen
// Auto close if (menuOpen.value) setTimeout(() => menuOpen.value = false, 2000)
if (this.menuOpen) setTimeout(() => this.menuOpen = false, 2000)
}
updateBookmark(to: RouteLocationNormalized): void
{
// Update title
// Use next tick to handle router history correctly
// see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
this.$nextTick(() => {
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
document.title = to.meta.title ? `Aza - ${to.meta.title}` : 'Aza - Home';
})
console.log('AfterEach called', to)
this.currentRoute = ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
this.calculateBookmarkCss()
this.menuOpen = false
}
mounted(): void
{
console.log('Mounted called', this.$route)
router.afterEach(this.updateBookmark)
if (this.$route.name) this.currentRoute = ((this.$route.meta?.navBookmark ?? this.$route.name) as string).toLowerCase()
// Resize listener
window.addEventListener('resize', this.calculateBookmarkCss, true);
// Update every second
this.bookmarkUpdateIntervalId = window.setInterval(this.calculateBookmarkCss, 1000)
}
unmounted(): void
{
window.removeEventListener('resize', this.calculateBookmarkCss)
window.clearInterval(this.bookmarkUpdateIntervalId)
}
calculateBookmarkCss(): void
{
if (this.currentRoute in this.$refs)
this.currentLink = (this.$refs[this.currentRoute] as ComponentPublicInstance).$el
else return
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
let box = this.currentLink.getBoundingClientRect()
if (box.top == this.lastTop) return
this.lastTop = box.top
let h = box.bottom - box.top
let width = Math.round(h / 2) + 8
this.bookmarkCss = `top: ${box.top - 8}px;` +
`border-width: ${width}px 20px ${width}px 50px;`
}
} }
export default toNative(App) const setNavRef = (name: string) => (el: Element | ComponentPublicInstance | null) => {
navRefs.value[name] = el
}
const resolveNavElement = (target: Element | ComponentPublicInstance | null | undefined): Element | null => {
if (!target) return null
if (target instanceof Element) return target
return (target.$el as Element | undefined) ?? null
}
const calculateBookmarkCss = (): void => {
const currentLink = resolveNavElement(navRefs.value[currentRoute.value])
if (!currentLink) return
const box = currentLink.getBoundingClientRect()
if (box.top === lastTop.value) return
lastTop.value = box.top
const h = box.bottom - box.top
const width = Math.round(h / 2) + 8
bookmarkCss.value = `top: ${box.top - 8}px;` +
`border-width: ${width}px 20px ${width}px 50px;`
}
const getRouteBookmark = (to: RouteLocationNormalized): string => {
return ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
}
const updateBookmark = (to: RouteLocationNormalized): void => {
nextTick(() => {
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
document.title = to.meta.title ? `Aza - ${to.meta.title}` : 'Aza - Home';
})
console.log('AfterEach called', to)
currentRoute.value = getRouteBookmark(to)
calculateBookmarkCss()
menuOpen.value = false
}
onMounted(() => {
console.log('Mounted called', route)
removeAfterEach.value = router.afterEach(updateBookmark)
if (route.name) {
currentRoute.value = getRouteBookmark(route as unknown as RouteLocationNormalized)
}
window.addEventListener('resize', calculateBookmarkCss, true)
bookmarkUpdateIntervalId.value = window.setInterval(calculateBookmarkCss, 1000)
})
onUnmounted(() => {
removeAfterEach.value?.()
window.removeEventListener('resize', calculateBookmarkCss, true)
if (bookmarkUpdateIntervalId.value !== null) {
window.clearInterval(bookmarkUpdateIntervalId.value)
}
})
</script> </script>
<style lang="sass"> <style lang="sass">
+13 -21
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="index index-tags" v-if="mode === 'tags'"> <div class="index index-tags" v-if="mode === 'tags'">
<Tag v-for="t in meta.tags" :key="t" :tag-name="t[0]" direction="right" <Tag v-for="t in meta.tags" :key="`${t[0]}-${t[1]}`" :tag-name="t[0]" direction="right"
@click="e => clickTag(e, t)">{{ t[0] }} ({{ t[1] }})</Tag> @click="e => clickTag(e, t)">{{ t[0] }} ({{ t[1] }})</Tag>
</div> </div>
<div class="index index-categories" v-else> <div class="index index-categories" v-else>
@@ -9,35 +9,27 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
import Tag from "@/components/Tag.vue"; import Tag from "@/components/Tag.vue";
import {pushQuery} from "@/scripts/router"; import {pushQuery} from "@/scripts/router";
import {BlogMeta} from "@/scripts/models"; import {BlogMeta} from "@/scripts/models";
import {globals} from "@/scripts/global"; import {globals} from "@/scripts/global";
@Component({components: {Tag}}) withDefaults(defineProps<{ mode?: 'tags' | 'categories' }>(), {
class BlogIndexLinks extends Vue mode: 'tags'
{ })
@Prop({default: 'tags'}) mode: 'tags' | 'categories' = 'tags'
meta: BlogMeta = globals.staticMeta const meta: BlogMeta = globals.staticMeta
clickCat(e: MouseEvent, cat: [string, number]): void const clickCat = (e: MouseEvent, cat: [string, number]): void => {
{ e.stopPropagation()
e.stopPropagation() pushQuery({category: cat[0], tag: null})
pushQuery({category: cat[0], tag: null})
}
clickTag(e: MouseEvent, tag: [string, number]): void
{
e.stopPropagation()
pushQuery({tag: tag[0], category: null})
}
} }
export default toNative(BlogIndexLinks) const clickTag = (e: MouseEvent, tag: [string, number]): void => {
e.stopPropagation()
pushQuery({tag: tag[0], category: null})
}
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
+14 -20
View File
@@ -7,30 +7,24 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator' import {computed, onMounted} from 'vue'
import {$} from '@/scripts/constants'; import {$} from '@/scripts/constants';
@Component const props = withDefaults(defineProps<{ title: string, active?: boolean }>(), {
class Collapse extends Vue active: false
{ })
@Prop title!: string
@Prop({default: false}) active = false
show = false const displayTitle = computed((): string => decodeURIComponent(props.title))
get displayTitle(): string onMounted((): void => {
{ $('.collapse').accordion({
return decodeURIComponent(this.title) collapsible: true,
} header: 'h3',
heightStyle: 'content',
mounted(): void active: props.active
{ })
$('.collapse').accordion({collapsible: true, header: 'h3', heightStyle: 'content', })
active: this.active})
}
}
export default toNative(Collapse)
</script> </script>
<style lang="sass"> <style lang="sass">
+13 -22
View File
@@ -7,33 +7,24 @@
</table> </table>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator' import {computed} from 'vue'
@Component const props = defineProps<{ table: {[id: string]: unknown} }>()
class MetaTable extends Vue
{
@Prop({required: true}) table!: {[id: string]: unknown}
get filteredTable(): {[id: string]: unknown} const filteredTable = computed((): {[id: string]: unknown} => {
{ const t: {[id: string]: unknown} = {}
const t: {[id: string]: unknown} = {}
Object.keys(this.table).forEach(k => { Object.keys(props.table).forEach(k => {
// Ignore empty if (!props.table[k]) return
if (!this.table[k]) return
// Convert to sentence case (https://stackoverflow.com/a/7225450/7346633) let newK = k.replace(/([A-Z])/g, " $1")
let newK = k.replace(/([A-Z])/g, " $1") newK = newK.charAt(0).toUpperCase() + newK.slice(1)
newK = newK.charAt(0).toUpperCase() + newK.slice(1) t[newK] = props.table[k]
t[newK] = this.table[k] })
})
return t return t
} })
}
export default toNative(MetaTable)
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
+25 -29
View File
@@ -23,8 +23,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator' import {computed, onMounted} from 'vue'
import moment from "moment"; import moment from "moment";
import MetaTable from "@/components/MetaTable.vue"; import MetaTable from "@/components/MetaTable.vue";
import {capitalize} from "@/scripts/utils"; import {capitalize} from "@/scripts/utils";
@@ -32,36 +32,32 @@ import linkifyUrls from "linkify-urls";
import {$} from '@/scripts/constants'; import {$} from '@/scripts/constants';
import {ZoteroData, ZoteroItem} from "@/scripts/zotero"; import {ZoteroData, ZoteroItem} from "@/scripts/zotero";
@Component({components: {MetaTable}}) const props = defineProps<{ item: ZoteroItem }>()
class ZoteroPublicationView extends Vue
{
@Prop({required: true}) item!: ZoteroItem
get d(): ZoteroData { return this.item.data } const d = computed((): ZoteroData => props.item.data)
get date(): moment.Moment { return moment(this.item.meta.parsedDate) } const date = computed((): moment.Moment => moment(props.item.meta.parsedDate))
get authors(): string { return this.d.creators.map(it => it.firstName + ' ' + it.lastName).join(' & ') } const authors = computed((): string => d.value.creators.map(it => it.firstName + ' ' + it.lastName).join(' & '))
get tableData(): {[id: string]: unknown} const tableData = computed((): {[id: string]: unknown} => {
{ const t: {[id: string]: unknown} = {...d.value}
const t: {[id: string]: unknown} = {...this.d} t.creators = authors.value
t.creators = this.authors delete t.key
delete t.key delete t.version
delete t.version delete t.title
delete t.title delete t.abstractNote
delete t.abstractNote if (t.itemType) t.itemType = capitalize(t.itemType as string)
if (t.itemType) t.itemType = capitalize(t.itemType as string) if (t.url) t.url = linkifyUrls(t.url as string)
if (t.url) t.url = linkifyUrls(t.url as string) return t
return t })
}
mounted(): void onMounted((): void => {
{ $('.publication').accordion({
$('.publication').accordion({collapsible: true, header: 'div.header', active: false, collapsible: true,
heightStyle: "content"}) header: 'div.header',
} active: false,
} heightStyle: 'content'
})
export default toNative(ZoteroPublicationView) })
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
+2 -4
View File
@@ -1,5 +1,3 @@
import { Component, Vue, Prop } from 'vue-facing-decorator'
/** /**
* Same as python's range * Same as python's range
* *
@@ -41,9 +39,9 @@ export function minMax(val: number, min: number, max: number): number
export type Keybinds = {[id: string]: (e: KeyboardEvent) => unknown} export type Keybinds = {[id: string]: (e: KeyboardEvent) => unknown}
/** /**
* Key handler mixin * Key handler standalone helper class (not a Vue mixin)
*/ */
export class KeyHandler extends Vue export class KeyHandler
{ {
keybinds: Keybinds = {} keybinds: Keybinds = {}
_keybinds: Keybinds = {} _keybinds: Keybinds = {}
+18 -31
View File
@@ -9,8 +9,8 @@
<Loading v-else></Loading> <Loading v-else></Loading>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator' import {onMounted, ref} from 'vue'
import {marked} from 'marked'; import {marked} from 'marked';
import emojiRegex from 'emoji-regex'; import emojiRegex from 'emoji-regex';
import {parseExtensions} from '@/scripts/extended_markdown' import {parseExtensions} from '@/scripts/extended_markdown'
@@ -19,37 +19,24 @@ import {hosts} from "@/scripts/constants";
import Loading from "@/components/Loading.vue"; import Loading from "@/components/Loading.vue";
import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero"; import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero";
@Component({components: {Loading, ZoteroPublication}}) const html = ref("")
class About extends Vue const publications = ref<ZoteroItem[]>([])
{
html = ""
publications: ZoteroItem[] = []
mounted(): void onMounted((): void => {
{ fetch(`${hosts.content}/README.md`).then(it => it.text())
// Fetch readme .then(it => html.value = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
fetch(`${hosts.content}/README.md`).then(it => it.text()) return `<span class="emoji">${emoji}</span>`
.then(it => this.html = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => { }))))
return `<span class="emoji">${emoji}</span>`
}))))
// Fetch publications from zotero fetch(`${hosts.api}/zotero.json`)
fetch(`${hosts.api}/zotero.json`) .then(it => it.json()).then((it: ZoteroItem[]) => {
.then(it => it.json()).then(it => publications.value = it
{ let files: ZoteroAttachment[] = it as unknown as ZoteroAttachment[]
// Filter out publications and attachments files = files.filter(file => file.data.itemType === 'attachment')
this.publications = it publications.value = publications.value.filter(pub => pub.data.itemType !== 'attachment')
let files: ZoteroAttachment[] = it publications.value.forEach(pub => pub.attachments = files.filter(a => a.data.parentItem == pub.key))
files = files.filter(it => it.data.itemType === 'attachment') })
this.publications = this.publications.filter(it => it.data.itemType !== 'attachment') })
// Add attachments to
this.publications.forEach(it => it.attachments = files.filter(a => a.data.parentItem == it.key))
})
}
}
export default toNative(About)
</script> </script>
<style lang="sass"> <style lang="sass">
+72 -74
View File
@@ -1,10 +1,10 @@
<script lang="ts"> <script setup lang="ts">
import { Vue, Component, toNative } from 'vue-facing-decorator'; import {onMounted, ref} from 'vue'
import { RouteLocationNormalizedLoaded } from "vue-router"; import {useRoute} from "vue-router";
interface PhotoMetadata { interface PhotoMetadata {
id: string id: string
owner_key: string // This will be filtered out for public requests owner_key: string
upload_time: string upload_time: string
original_photo: string original_photo: string
edited_photo: string edited_photo: string
@@ -13,7 +13,6 @@ interface PhotoMetadata {
exif: {[id: string]: string} exif: {[id: string]: string}
} }
// Take in a string, use its hash to produce a number from 0 to 1
function detRandom(seed: string): number { function detRandom(seed: string): number {
return Array.from(seed).reduce((acc, char) => (acc + char.charCodeAt(0) * 65535) % 22859, 0) / 22859 return Array.from(seed).reduce((acc, char) => (acc + char.charCodeAt(0) * 65535) % 22859, 0) / 22859
} }
@@ -21,84 +20,83 @@ function detRandom(seed: string): number {
async function waitTruthy<T>(condition: () => T, interval = 100): Promise<T> { async function waitTruthy<T>(condition: () => T, interval = 100): Promise<T> {
return new Promise((resolve) => { return new Promise((resolve) => {
const check = () => { const check = () => {
let a = condition() const value = condition()
if (a) resolve(a) if (value) resolve(value)
else setTimeout(check, interval) else setTimeout(check, interval)
} }
check() check()
}) })
} }
@Component({}) const route = useRoute()
class Photos extends Vue { const photos = ref<PhotoMetadata[]>([])
photos: PhotoMetadata[] const photoRows = ref<PhotoMetadata[][]>([])
photoRows: PhotoMetadata[][]
declare $route: RouteLocationNormalizedLoaded const rowProbabilityTable: Record<number, number> = {
1: 0,
async created() { 2: 0.3,
this.photos = await (await fetch('https://p.aza.moe/photos')).json() 3: 0.5
this.photos.sort((a, b) => (a.exif.DateTime < b.exif.DateTime ? 1 : -1))
let rowProbabilityTable = {
1: 0, 2: 0.3, 3: 0.5
}
// Generate photo rows: there is a 10% chance that a photo will be the only photo in its row
this.photoRows = []
let currentRow: PhotoMetadata[] = []
this.photos.forEach((p) => {
if (currentRow.length === 0) currentRow.push(p)
else if (currentRow.length >= 3) {
this.photoRows.push(currentRow)
currentRow = [ p ]
}
else {
const singleChance = detRandom(p.original_photo)
if (singleChance < rowProbabilityTable[currentRow.length]) {
this.photoRows.push(currentRow)
currentRow = [ p ]
} else currentRow.push(p)
}
})
if (currentRow.length > 0) this.photoRows.push(currentRow)
}
async mounted() {
if (this.$route.params.id) {
const photoEl = await waitTruthy(() => document.getElementById(`photo-${this.$route.params.id}`))
photoEl.click()
}
}
url(s: string): string {
s = s.replace('data/photos', 'static').replace('./', '')
return `https://p.aza.moe/${s}`
}
randomRotation(s: string): string {
const angle = (detRandom(s) * 20) - 10 // -10 to +10 degrees
return `rotate(${angle}deg)`
}
async clickPhoto(p: PhotoMetadata, e: MouseEvent) {
console.log("Clicked photo:", p.id)
const dom = e.currentTarget as HTMLDivElement
const photoEl = dom.querySelector('.photo-wrapper') as HTMLDivElement
photoEl.style.viewTransitionName = `photo-${p.id}`
const transition = document.startViewTransition(() => {
dom.classList.toggle('active')
document.getElementsByClassName('blur')[0].toggleAttribute('hidden')
})
await transition.finished
photoEl.style.viewTransitionName = ''
}
} }
export default toNative(Photos) const initPhotos = async () => {
photos.value = await (await fetch('https://p.aza.moe/photos')).json()
photos.value.sort((a, b) => (a.exif.DateTime < b.exif.DateTime ? 1 : -1))
const rows: PhotoMetadata[][] = []
let currentRow: PhotoMetadata[] = []
photos.value.forEach((p) => {
if (currentRow.length === 0) currentRow.push(p)
else if (currentRow.length >= 3) {
rows.push(currentRow)
currentRow = [p]
} else {
const singleChance = detRandom(p.original_photo)
if (singleChance < rowProbabilityTable[currentRow.length]) {
rows.push(currentRow)
currentRow = [p]
} else currentRow.push(p)
}
})
if (currentRow.length > 0) rows.push(currentRow)
photoRows.value = rows
}
const url = (s: string): string => {
s = s.replace('data/photos', 'static').replace('./', '')
return `https://p.aza.moe/${s}`
}
const randomRotation = (s: string): string => {
const angle = (detRandom(s) * 20) - 10
return `rotate(${angle}deg)`
}
const clickPhoto = async (p: PhotoMetadata, e: MouseEvent) => {
console.log("Clicked photo:", p.id)
const dom = e.currentTarget as HTMLDivElement
const photoEl = dom.querySelector('.photo-wrapper') as HTMLDivElement
photoEl.style.viewTransitionName = `photo-${p.id}`
const transition = document.startViewTransition(() => {
dom.classList.toggle('active')
document.getElementsByClassName('blur')[0].toggleAttribute('hidden')
})
await transition.finished
photoEl.style.viewTransitionName = ''
}
onMounted(async () => {
await initPhotos()
if (route.params.id) {
const photoEl = await waitTruthy(() => document.getElementById(`photo-${route.params.id}`))
photoEl.click()
}
})
</script> </script>
<template> <template>
+28 -36
View File
@@ -23,57 +23,49 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator' import {onMounted, ref} from 'vue'
import {fab, hosts} from "@/scripts/constants"; import {fab, hosts} from "@/scripts/constants";
import {shuffle} from "@/scripts/utils"; import {shuffle} from "@/scripts/utils";
export interface Friend { interface Friend {
name: string name: string
avatar: string avatar: string
banner: string banner: string
desc?: string desc?: string
[id: string]: string | undefined
} }
const excludes = new Set(["name", "avatar", "banner", "desc"]) const excludes = new Set(["name", "avatar", "banner", "desc"])
const icons = { const icons: {[id: string]: string} = {
blog: 'fas fa-book' blog: 'fas fa-book'
} }
@Component const friends = ref<Friend[]>([])
class Friends extends Vue
{
friends: Friend[] = []
async created() const bgStyle = (f: Friend) => {
{ if (f.banner) return {'background-image': `url("${f.banner}")`}
this.friends = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json() return {}
// Fix avatar relative url
this.friends.forEach(f => {
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
})
this.friends = shuffle(this.friends)
}
bgStyle(f: Friend)
{
if (f.banner) return {'background-image': `url("${f.banner}")`}
else return {}
}
getFriendLinks(f: Friend): { link: string, icon: string }[]
{
return Object.entries(f).filter(pair => !excludes.has(pair[0].toString()))
.map(pair => {
return { link: pair[1], icon: fab.includes(pair[0]) ? `fab fa-${pair[0]}` :
pair[0] in icons ? icons[pair[0]] : pair[0] }
})
}
} }
export default toNative(Friends)
const getFriendLinks = (f: Friend): { link: string, icon: string }[] => {
return Object.entries(f)
.filter(([key, value]) => !excludes.has(key) && typeof value === 'string')
.map(([key, value]) => ({
link: value,
icon: fab.includes(key) ? `fab fa-${key}` : (key in icons ? icons[key] : key)
}))
}
onMounted(async () => {
friends.value = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
friends.value.forEach(f => {
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
})
friends.value = shuffle(friends.value)
})
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
+20 -39
View File
@@ -23,30 +23,26 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator' import {ref} from 'vue'
export interface MenuItem interface MenuItem {
{
name: string name: string
sub?: string sub?: string
img?: string img?: string
recommend?: boolean recommend?: boolean
original?: boolean original?: boolean
id?: number id?: number
} }
export interface MenuCategory interface MenuCategory {
{
cat: string cat: string
subtitle?: string subtitle?: string
items: MenuItem[] items: MenuItem[]
column?: number column?: number
} }
export const menu: MenuCategory[] = [ const menu: MenuCategory[] = [
{ {
cat: '🍖 猪肉', cat: '🍖 猪肉',
items: [ items: [
@@ -134,40 +130,25 @@ export const menu: MenuCategory[] = [
}, },
] ]
@Component const maxCols = 2
class Menu extends Vue const cols = ref<MenuCategory[][]>(Array.from({length: maxCols}, () => []))
{
max_cols = 2
cols: MenuCategory[][] = new Array(this.max_cols)
created() const tmp = Array.from(menu)
{ tmp.sort((a, b) => b.items.length - a.items.length)
// Calculate menu layout
const tmp = Array.from(menu)
tmp.sort((a, b) => b.items.length - a.items.length)
// Two columns const colCounts = new Array(maxCols).fill(0)
let col_counts = new Array(this.max_cols).fill(0) for (const cat of tmp) {
for (const cat of tmp) const col = colCounts.indexOf(Math.min(...colCounts))
{ cat.column = col
// Get column index with minimal item count colCounts[col] += cat.items.length
let col = col_counts.indexOf(Math.min(...col_counts))
cat.column = col
col_counts[col] += cat.items.length
}
// Separate arrays by column
for (let i = 0; i < this.max_cols; i++)
this.cols[i] = menu.filter(it => it.column == i)
// Assign ID to each item
let id = 0
this.cols.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
}
} }
export default toNative(Menu) for (let i = 0; i < maxCols; i++) {
cols.value[i] = menu.filter(it => it.column == i)
}
let id = 0
cols.value.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
+1 -10
View File
@@ -4,16 +4,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
@Component
class Projects extends Vue
{
}
export default toNative(Projects)
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>