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:
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user