UNPKG

@koishijs/client

Version:

Koishi Console Client

185 lines (160 loc) 5.09 kB
import { createRouter, createWebHistory, START_LOCATION } from 'vue-router' import Overlay from '../components/chat/overlay.vue' import { Context } from '../context' import { insert, Service } from '../utils' import { Component, MaybeRefOrGetter, reactive, ref, toValue } from 'vue' import { global, Store, store } from '../data' import { Dict, omit, remove } from 'cosmokit' import { Disposable } from 'cordis' import { SlotOptions } from '../components' declare module 'vue-router' { interface RouteMeta { activity?: Activity } } declare module '../context' { interface Context { $router: RouterService slot(options: SlotOptions): () => void page(options: Activity.Options): () => void } interface Events { 'activity'(activity: Activity): boolean } } export namespace Activity { export interface Options { id?: string path: string strict?: boolean component: Component name: MaybeRefOrGetter<string> desc?: MaybeRefOrGetter<string> icon?: MaybeRefOrGetter<string> order?: number authority?: number position?: 'top' | 'bottom' fields?: (keyof Store)[] /** @deprecated */ when?: () => boolean disabled?: () => boolean } } export interface Activity extends Activity.Options {} function getActivityId(path: string) { return path.split('/').find(Boolean) ?? '' } export const redirectTo = ref<string>() export class Activity { id: string _disposables: Disposable[] = [] constructor(public ctx: Context, public options: Activity.Options) { options.order ??= 0 options.position ??= 'top' Object.assign(this, omit(options, ['icon', 'name', 'desc', 'disabled'])) const { path, id = getActivityId(path), component } = options this._disposables.push(ctx.$router.router.addRoute({ path, name: id, component, meta: { activity: this } })) this.id ??= id this.handleUpdate() this.authority ??= 0 this.fields ??= [] ctx.$router.pages[this.id] = this } handleUpdate() { if (redirectTo.value) { const location = this.ctx.$router.router.resolve(redirectTo.value) if (location.matched.length) { redirectTo.value = null this.ctx.$router.router.replace(location) } } } get icon() { return toValue(this.options.icon ?? 'activity:default') } get name() { return toValue(this.options.name ?? this.id) } get desc() { return toValue(this.options.desc) } disabled() { if (this.ctx.bail('activity', this)) return true if (!this.fields.every(key => store[key])) return true if (this.when && !this.when()) return true if (this.options.disabled?.()) return true } dispose() { this._disposables.forEach(dispose => dispose()) const current = this.ctx.$router.router.currentRoute.value if (current?.meta?.activity === this) { redirectTo.value = current.fullPath this.ctx.$router.router.push(this.ctx.$router.cache['home'] || '/') } return delete this.ctx.$router.pages[this.id] } } export default class RouterService extends Service { public views = reactive<Dict<SlotOptions[]>>({}) public cache = reactive<Record<keyof any, string>>({}) public pages = reactive<Dict<Activity>>({}) public router = createRouter({ history: createWebHistory(global.uiPath), linkActiveClass: 'active', routes: [], }) constructor(ctx: Context) { super(ctx, '$router', true) ctx.mixin('$router', ['slot', 'page']) const initialTitle = document.title ctx.effect(() => this.router.afterEach((route) => { const { name, fullPath } = this.router.currentRoute.value this.cache[name] = fullPath if (route.meta.activity) { document.title = `${route.meta.activity.name}` if (initialTitle) document.title += ` | ${initialTitle}` } })) this.router.beforeEach(async (to, from) => { if (to.matched.length) { if (to.matched[0].path !== '/') { redirectTo.value = null } return } if (from === START_LOCATION) { await ctx.$loader.initTask to = this.router.resolve(to) if (to.matched.length) return to } redirectTo.value = to.fullPath const result = this.cache['home'] || '/' if (result === to.fullPath) return return result }) this.slot({ type: 'global', component: Overlay, }) } slot(options: SlotOptions) { options.order ??= 0 options.component = this.ctx.wrapComponent(options.component) if (options.when) options.disabled = () => !options.when() return this.ctx.effect(() => { const list = this.views[options.type] ||= [] insert(list, options) return () => { remove(list, options) if (!list.length) delete this.views[options.type] } }) } page(options: Activity.Options) { options.component = this.ctx.wrapComponent(options.component) return this.ctx.effect(() => { const activity = new Activity(this.ctx, options) return () => activity.dispose() }) } }