278 lines
5.6 KiB
Vue
Executable File
278 lines
5.6 KiB
Vue
Executable File
<template>
|
|
<Progress
|
|
v-show="routerStatus.isLoad"
|
|
class="router-progress"
|
|
:size="6"
|
|
/>
|
|
|
|
<div class="modals-container" />
|
|
|
|
<main class="layout">
|
|
<div class="left-menu">
|
|
<RouterLink
|
|
to="/"
|
|
:class='$route.path === "/"
|
|
? "isActive"
|
|
: ""'
|
|
title="ホーム"
|
|
>
|
|
<img :src='"icon" in serverInfo
|
|
? serverInfo.icon
|
|
: "/assets/lynqchat.svg"'
|
|
/>
|
|
</RouterLink>
|
|
|
|
<RouterLink
|
|
v-for="community of communitys"
|
|
:to="`/community/${community.id}`"
|
|
:class='$route.path.startsWith(`/community/${community.id}`)
|
|
? "isActive"
|
|
: ""'
|
|
:title="community.name"
|
|
>
|
|
<img
|
|
v-if="community.icon"
|
|
:src="community.icon"
|
|
/>
|
|
<Icon
|
|
v-else
|
|
icon="material-symbols:groups-rounded"
|
|
/>
|
|
</RouterLink>
|
|
</div>
|
|
|
|
<div class="content-main">
|
|
<div class="content-header">
|
|
{{
|
|
title
|
|
?? (serverInfo.success
|
|
? serverInfo.name
|
|
: null)
|
|
?? "LynqChat"
|
|
}}
|
|
</div>
|
|
|
|
<div
|
|
class="route-main"
|
|
:class='isFullRoute
|
|
? "full-route"
|
|
: ""'
|
|
>
|
|
<RouterView />
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<style scoped>
|
|
main.layout {
|
|
display: flex;
|
|
width: 100dvw;
|
|
height: 100dvh;
|
|
padding: 0.5rem;
|
|
gap: 0.5rem;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.left-menu {
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
flex-direction: column;
|
|
padding: 0.5rem 0;
|
|
height: 100%;
|
|
overflow: scroll;
|
|
gap: 0.5rem;
|
|
box-sizing: border-box;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
|
|
.left-menu a {
|
|
display: block;
|
|
position: relative;
|
|
z-index: 0;
|
|
border-radius: 0.5rem;
|
|
width: 3rem;
|
|
height: 3rem;
|
|
padding: 0.5rem;
|
|
transition: background-color 200ms ease-out;
|
|
}
|
|
|
|
.left-menu a * {
|
|
font-size: 3rem;
|
|
padding: 0.25rem;
|
|
border-radius: 0.5rem;
|
|
width: 3rem;
|
|
height: 3rem;
|
|
color: var(--text-color);
|
|
display: block;
|
|
overflow: hidden;
|
|
object-fit: contain;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.left-menu a::before {
|
|
content: "";
|
|
display: block;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 0.3rem;
|
|
transform: translateY(-50%);
|
|
width: 0.25rem;
|
|
height: 70%;
|
|
border-radius: 0.5rem;
|
|
background-color: transparent;
|
|
transition: all 200ms ease-out;
|
|
}
|
|
|
|
.left-menu a.isActive::before {
|
|
background-color: var(--text-color);
|
|
}
|
|
|
|
.left-menu a:hover,
|
|
.left-menu a.isActive {
|
|
background-color: var(--border-color);
|
|
}
|
|
|
|
.content-main {
|
|
border: 1px solid var(--border-color);
|
|
background-color: var(--route-bg-color);
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.content-header {
|
|
padding: 1rem 1.25rem;
|
|
font-size: 1.3rem;
|
|
font-weight: bold;
|
|
border-bottom-left-radius: 0.5rem;
|
|
border-bottom-right-radius: 0.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
box-shadow: 0px 1px 10px var(--border-color);
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
|
|
.route-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 1.25rem;
|
|
padding-bottom: 0;
|
|
overflow: scroll;
|
|
height: 100%;
|
|
}
|
|
|
|
.content-main .route-main.full-route {
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.router-progress {
|
|
position: fixed;
|
|
inset: 0.5rem 0.5rem 0 auto;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.modals-container {
|
|
z-index: 9998;
|
|
position: relative;
|
|
}
|
|
|
|
@media (max-width: 40rem) {
|
|
.layout {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.left-menu {
|
|
display: none;
|
|
}
|
|
|
|
.content-main {
|
|
border: none;
|
|
border-radius: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script lang="ts" setup>
|
|
import { RouterView, RouterLink, useRouter, useRoute } from "vue-router";
|
|
import routerStatus, { title } from "@/lib/router";
|
|
import { Icon } from "@iconify/vue";
|
|
import Progress from "@/components/Progress.vue";
|
|
import { communitys, serverInfo } from "@/lib/account";
|
|
import { computed, onBeforeUnmount, onMounted, watch } from "vue";
|
|
import { createModal } from "@/lib/modal";
|
|
import ErrorModal from "@/components/Modal/Error.vue";
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
const isFullRoute = computed(() => route.meta.isFullRoute === true);
|
|
|
|
watch(route, (to, from) => {
|
|
if (
|
|
to.matched[0]?.path === from.matched[0]?.path &&
|
|
from.meta.canReloadTitle === false
|
|
) {
|
|
routerStatus.isLoad = false;
|
|
return false;
|
|
}
|
|
|
|
if (typeof route.meta.title === "string")
|
|
title.value = route.meta.title;
|
|
});
|
|
|
|
if (!serverInfo.value.success) {
|
|
throw new Error("サーバー情報の取得に失敗しました。");
|
|
}
|
|
|
|
if (!serverInfo.value.isInitialized) {
|
|
router.replace("/setup/initialization");
|
|
}
|
|
|
|
if (!serverInfo.value.isFirstAdminExists) {
|
|
router.replace("/setup/create-admin");
|
|
}
|
|
|
|
function handleError(event: ErrorEvent | PromiseRejectionEvent) {
|
|
let content = event instanceof PromiseRejectionEvent
|
|
? event.reason
|
|
: event;
|
|
|
|
if (content instanceof Error) {
|
|
content = content.message;
|
|
}
|
|
|
|
createModal({
|
|
component: ErrorModal,
|
|
props: {
|
|
error: content ?? "不明なエラー",
|
|
canClose: false,
|
|
},
|
|
});
|
|
}
|
|
|
|
onMounted(async () => {
|
|
window.addEventListener("error", handleError);
|
|
window.addEventListener("unhandledrejection", handleError);
|
|
|
|
if ("serviceWorker" in navigator) {
|
|
const swFile = import.meta.env.MODE === "production"
|
|
? "/sw.js"
|
|
: "/dev-sw.js?dev-sw";
|
|
|
|
navigator.serviceWorker.register(swFile, {
|
|
type: import.meta.env.MODE === "production"
|
|
? "classic"
|
|
: "module",
|
|
scope: "/",
|
|
});
|
|
}
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener("error", handleError);
|
|
window.removeEventListener("unhandledrejection", handleError);
|
|
});
|
|
</script> |