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",
"tg-blog": "^1.1.7",
"vue": "^3.4.35",
"vue-facing-decorator": "^4.0.1",
"vue-i18n": "^11.1.12",
"vue-router": "^4.4.0"
},
+81 -85
View File
@@ -1,21 +1,21 @@
<template>
<div id="nav" class="fbox-v"
: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="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>
<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>
<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>
<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>
<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>
<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>
</div>
@@ -28,92 +28,88 @@
<router-view/>
</template>
<script lang="ts">
import { ComponentPublicInstance } from 'vue';
import { Component, Vue, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import {nextTick, onMounted, onUnmounted, ref, ComponentPublicInstance} from 'vue';
import router from "@/scripts/router";
import { RouteLocationNormalized, RouteLocationNormalizedLoaded, Router } from "vue-router";
import { TranslateResult } from "vue-i18n";
import {RouteLocationNormalized, RouteLocationRaw, useRoute} from "vue-router";
@Component
class App extends Vue
{
currentRoute = ''
currentLink: Element = null as never as Element
bookmarkCss = ""
bookmarkUpdateIntervalId!: number
lastTop = 0
const route = useRoute()
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
declare $route: RouteLocationNormalizedLoaded
declare $router: Router
const navRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({})
showMenu(): void
{
this.menuOpen = !this.menuOpen
const showMenu = (): void => {
menuOpen.value = !menuOpen.value
// Auto close
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;`
}
if (menuOpen.value) setTimeout(() => menuOpen.value = false, 2000)
}
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>
<style lang="sass">
+13 -21
View File
@@ -1,6 +1,6 @@
<template>
<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>
</div>
<div class="index index-categories" v-else>
@@ -9,35 +9,27 @@
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import Tag from "@/components/Tag.vue";
import {pushQuery} from "@/scripts/router";
import {BlogMeta} from "@/scripts/models";
import {globals} from "@/scripts/global";
@Component({components: {Tag}})
class BlogIndexLinks extends Vue
{
@Prop({default: 'tags'}) mode: 'tags' | 'categories' = 'tags'
withDefaults(defineProps<{ mode?: 'tags' | 'categories' }>(), {
mode: 'tags'
})
meta: BlogMeta = globals.staticMeta
const meta: BlogMeta = globals.staticMeta
clickCat(e: MouseEvent, cat: [string, number]): void
{
e.stopPropagation()
pushQuery({category: cat[0], tag: null})
}
clickTag(e: MouseEvent, tag: [string, number]): void
{
e.stopPropagation()
pushQuery({tag: tag[0], category: null})
}
const clickCat = (e: MouseEvent, cat: [string, number]): void => {
e.stopPropagation()
pushQuery({category: cat[0], tag: null})
}
export default toNative(BlogIndexLinks)
const clickTag = (e: MouseEvent, tag: [string, number]): void => {
e.stopPropagation()
pushQuery({tag: tag[0], category: null})
}
</script>
<style lang="sass" scoped>
+14 -20
View File
@@ -7,30 +7,24 @@
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import {computed, onMounted} from 'vue'
import {$} from '@/scripts/constants';
@Component
class Collapse extends Vue
{
@Prop title!: string
@Prop({default: false}) active = false
const props = withDefaults(defineProps<{ title: string, active?: boolean }>(), {
active: false
})
show = false
const displayTitle = computed((): string => decodeURIComponent(props.title))
get displayTitle(): string
{
return decodeURIComponent(this.title)
}
mounted(): void
{
$('.collapse').accordion({collapsible: true, header: 'h3', heightStyle: 'content',
active: this.active})
}
}
export default toNative(Collapse)
onMounted((): void => {
$('.collapse').accordion({
collapsible: true,
header: 'h3',
heightStyle: 'content',
active: props.active
})
})
</script>
<style lang="sass">
+13 -22
View File
@@ -7,33 +7,24 @@
</table>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import {computed} from 'vue'
@Component
class MetaTable extends Vue
{
@Prop({required: true}) table!: {[id: string]: unknown}
const props = defineProps<{ table: {[id: string]: unknown} }>()
get filteredTable(): {[id: string]: unknown}
{
const t: {[id: string]: unknown} = {}
const filteredTable = computed((): {[id: string]: unknown} => {
const t: {[id: string]: unknown} = {}
Object.keys(this.table).forEach(k => {
// Ignore empty
if (!this.table[k]) return
Object.keys(props.table).forEach(k => {
if (!props.table[k]) return
// Convert to sentence case (https://stackoverflow.com/a/7225450/7346633)
let newK = k.replace(/([A-Z])/g, " $1")
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
t[newK] = this.table[k]
})
let newK = k.replace(/([A-Z])/g, " $1")
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
t[newK] = props.table[k]
})
return t
}
}
export default toNative(MetaTable)
return t
})
</script>
<style lang="sass" scoped>
+25 -29
View File
@@ -23,8 +23,8 @@
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import {computed, onMounted} from 'vue'
import moment from "moment";
import MetaTable from "@/components/MetaTable.vue";
import {capitalize} from "@/scripts/utils";
@@ -32,36 +32,32 @@ import linkifyUrls from "linkify-urls";
import {$} from '@/scripts/constants';
import {ZoteroData, ZoteroItem} from "@/scripts/zotero";
@Component({components: {MetaTable}})
class ZoteroPublicationView extends Vue
{
@Prop({required: true}) item!: ZoteroItem
const props = defineProps<{ item: ZoteroItem }>()
get d(): ZoteroData { return this.item.data }
get date(): moment.Moment { return moment(this.item.meta.parsedDate) }
get authors(): string { return this.d.creators.map(it => it.firstName + ' ' + it.lastName).join(' & ') }
const d = computed((): ZoteroData => props.item.data)
const date = computed((): moment.Moment => moment(props.item.meta.parsedDate))
const authors = computed((): string => d.value.creators.map(it => it.firstName + ' ' + it.lastName).join(' & '))
get tableData(): {[id: string]: unknown}
{
const t: {[id: string]: unknown} = {...this.d}
t.creators = this.authors
delete t.key
delete t.version
delete t.title
delete t.abstractNote
if (t.itemType) t.itemType = capitalize(t.itemType as string)
if (t.url) t.url = linkifyUrls(t.url as string)
return t
}
const tableData = computed((): {[id: string]: unknown} => {
const t: {[id: string]: unknown} = {...d.value}
t.creators = authors.value
delete t.key
delete t.version
delete t.title
delete t.abstractNote
if (t.itemType) t.itemType = capitalize(t.itemType as string)
if (t.url) t.url = linkifyUrls(t.url as string)
return t
})
mounted(): void
{
$('.publication').accordion({collapsible: true, header: 'div.header', active: false,
heightStyle: "content"})
}
}
export default toNative(ZoteroPublicationView)
onMounted((): void => {
$('.publication').accordion({
collapsible: true,
header: 'div.header',
active: false,
heightStyle: 'content'
})
})
</script>
<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
*
@@ -41,9 +39,9 @@ export function minMax(val: number, min: number, max: number): number
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 = {}
+18 -31
View File
@@ -9,8 +9,8 @@
<Loading v-else></Loading>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {marked} from 'marked';
import emojiRegex from 'emoji-regex';
import {parseExtensions} from '@/scripts/extended_markdown'
@@ -19,37 +19,24 @@ import {hosts} from "@/scripts/constants";
import Loading from "@/components/Loading.vue";
import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero";
@Component({components: {Loading, ZoteroPublication}})
class About extends Vue
{
html = ""
publications: ZoteroItem[] = []
const html = ref("")
const publications = ref<ZoteroItem[]>([])
mounted(): void
{
// Fetch readme
fetch(`${hosts.content}/README.md`).then(it => it.text())
.then(it => this.html = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
return `<span class="emoji">${emoji}</span>`
}))))
onMounted((): void => {
fetch(`${hosts.content}/README.md`).then(it => it.text())
.then(it => html.value = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
return `<span class="emoji">${emoji}</span>`
}))))
// Fetch publications from zotero
fetch(`${hosts.api}/zotero.json`)
.then(it => it.json()).then(it =>
{
// Filter out publications and attachments
this.publications = it
let files: ZoteroAttachment[] = it
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)
fetch(`${hosts.api}/zotero.json`)
.then(it => it.json()).then((it: ZoteroItem[]) => {
publications.value = it
let files: ZoteroAttachment[] = it as unknown as ZoteroAttachment[]
files = files.filter(file => file.data.itemType === 'attachment')
publications.value = publications.value.filter(pub => pub.data.itemType !== 'attachment')
publications.value.forEach(pub => pub.attachments = files.filter(a => a.data.parentItem == pub.key))
})
})
</script>
<style lang="sass">
+73 -75
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import { Vue, Component, toNative } from 'vue-facing-decorator';
import { RouteLocationNormalizedLoaded } from "vue-router";
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {useRoute} from "vue-router";
interface PhotoMetadata {
id: string
owner_key: string // This will be filtered out for public requests
owner_key: string
upload_time: string
original_photo: string
edited_photo: string
@@ -13,7 +13,6 @@ interface PhotoMetadata {
exif: {[id: string]: string}
}
// Take in a string, use its hash to produce a number from 0 to 1
function detRandom(seed: string): number {
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> {
return new Promise((resolve) => {
const check = () => {
let a = condition()
if (a) resolve(a)
const value = condition()
if (value) resolve(value)
else setTimeout(check, interval)
}
check()
})
}
@Component({})
class Photos extends Vue {
photos: PhotoMetadata[]
photoRows: PhotoMetadata[][]
const route = useRoute()
const photos = ref<PhotoMetadata[]>([])
const photoRows = ref<PhotoMetadata[][]>([])
declare $route: RouteLocationNormalizedLoaded
async created() {
this.photos = await (await fetch('https://p.aza.moe/photos')).json()
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 = ''
}
const rowProbabilityTable: Record<number, number> = {
1: 0,
2: 0.3,
3: 0.5
}
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>
<template>
@@ -176,4 +174,4 @@ img.pin
width: 40px
height: 40px
z-index: 2000
</style>
</style>
+28 -36
View File
@@ -23,57 +23,49 @@
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {fab, hosts} from "@/scripts/constants";
import {shuffle} from "@/scripts/utils";
export interface Friend {
interface Friend {
name: string
avatar: string
banner: string
desc?: string
[id: string]: string | undefined
}
const excludes = new Set(["name", "avatar", "banner", "desc"])
const icons = {
const icons: {[id: string]: string} = {
blog: 'fas fa-book'
}
@Component
class Friends extends Vue
{
friends: Friend[] = []
const friends = ref<Friend[]>([])
async created()
{
this.friends = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
// 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] }
})
}
const bgStyle = (f: Friend) => {
if (f.banner) return {'background-image': `url("${f.banner}")`}
return {}
}
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>
<style lang="sass" scoped>
+20 -39
View File
@@ -23,30 +23,26 @@
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
<script setup lang="ts">
import {ref} from 'vue'
export interface MenuItem
{
interface MenuItem {
name: string
sub?: string
img?: string
recommend?: boolean
original?: boolean
id?: number
}
export interface MenuCategory
{
interface MenuCategory {
cat: string
subtitle?: string
items: MenuItem[]
column?: number
}
export const menu: MenuCategory[] = [
const menu: MenuCategory[] = [
{
cat: '🍖 猪肉',
items: [
@@ -134,40 +130,25 @@ export const menu: MenuCategory[] = [
},
]
@Component
class Menu extends Vue
{
max_cols = 2
cols: MenuCategory[][] = new Array(this.max_cols)
const maxCols = 2
const cols = ref<MenuCategory[][]>(Array.from({length: maxCols}, () => []))
created()
{
// Calculate menu layout
const tmp = Array.from(menu)
tmp.sort((a, b) => b.items.length - a.items.length)
const tmp = Array.from(menu)
tmp.sort((a, b) => b.items.length - a.items.length)
// Two columns
let col_counts = new Array(this.max_cols).fill(0)
for (const cat of tmp)
{
// Get column index with minimal item count
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++)))
}
const colCounts = new Array(maxCols).fill(0)
for (const cat of tmp) {
const col = colCounts.indexOf(Math.min(...colCounts))
cat.column = col
colCounts[col] += cat.items.length
}
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>
<style lang="sass" scoped>
+1 -10
View File
@@ -4,16 +4,7 @@
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
@Component
class Projects extends Vue
{
}
export default toNative(Projects)
<script setup lang="ts">
</script>
<style lang="sass" scoped>