All files / src App.vue

89.36% Statements 42/47
62.96% Branches 17/27
91.3% Functions 21/23
86.48% Lines 32/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                      3x 3x 3x 5x 4x 4x 4x 4x 3x 4x                   36x 3x 4x 4x 4x       4x     3x                               1x 1x 1x             1x 1x                   1x 1x   2x               1x 21x           21x       1x     2x                     1x   1x                
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { RouterLink, RouterView } from "vue-router";
import { useRoute, useRouter } from "vue-router";
import {
  PjButton,
  PjStatusNotice,
} from "@plain-journal/ui";
 
import { useStaffSessionStore } from "./stores/session";
 
const route = useRoute();
const router = useRouter();
const session = useStaffSessionStore();
const publicLayout = computed(() => route.meta.publicLayout === true);
const roles = computed(() => session.profile?.roles ?? []);
const canCatalog = computed(() => roles.value.some((role) => ["ADMIN", "OPERATOR"].includes(role)));
const canWarehouse = computed(() => roles.value.some((role) => ["ADMIN", "WAREHOUSE"].includes(role)));
const isAdmin = computed(() => roles.value.includes("ADMIN"));
const workspaceMenu = ref<HTMLDetailsElement | null>(null);
const workspaceLinks = computed(() => [
  { to: "/", label: "工作区总览", visible: true },
  { to: "/catalog", label: "商品经营", visible: canCatalog.value },
  { to: "/inventory", label: "库存", visible: canWarehouse.value },
  { to: "/fulfillment", label: "履约与退货", visible: canWarehouse.value },
  { to: "/after-sales", label: "售后审核", visible: isAdmin.value },
  { to: "/marketing", label: "营销权益", visible: canCatalog.value },
  { to: "/chat", label: "客服会话", visible: canCatalog.value },
  { to: "/reviews", label: "评价治理", visible: canCatalog.value },
  { to: "/governance", label: "补偿与对账", visible: isAdmin.value },
].filter((item) => item.visible));
const currentWorkspace = computed(() => {
  const matched = workspaceLinks.value.find((item) => {
    Eif (item.to === "/") {
      return route.path === "/";
    }
    return route.path === item.to || route.path.startsWith(`${item.to}/`);
  });
  return matched?.label ?? "管理工作区";
});
 
watch(() => session.reauthRequired, (required) => {
  if (required && route.name !== "staff-login") {
    void router.replace({
      name: "staff-login",
      query: { redirect: route.fullPath },
    });
  }
});
 
function closeWorkspaceMenu() {
  if (workspaceMenu.value) {
    workspaceMenu.value.open = false;
  }
}
 
async function logout() {
  try {
    await session.logout();
    await router.replace("/login");
  } catch {
    // Preserve the session until an explicit local-only choice is made.
  }
}
 
async function clearLocal() {
  await session.clearLocalOnly();
  await router.replace("/login");
}
</script>
 
<template>
  <a class="pj-skip-link" href="#admin-main">跳到主要内容</a>
  <main v-if="publicLayout" id="admin-main" class="pj-app-main" tabindex="-1">
    <RouterView />
  </main>
  <div v-else class="pj-app-shell admin-shell">
    <header class="admin-header">
      <div class="admin-header__inner">
        <RouterLink class="admin-brand" to="/">
          <span>素简记</span>
          <small>PLAIN JOURNAL</small>
        </RouterLink>
        <details ref="workspaceMenu" class="admin-workspace-switcher">
          <summary>
            <span>当前工作区</span>
            <strong>{{ currentWorkspace }}</strong>
          </summary>
          <nav aria-label="管理工作区切换">
            <RouterLink
              v-for="item in workspaceLinks"
              :key="item.to"
              :to="item.to"
              @click="closeWorkspaceMenu"
            >
              {{ item.label }}
            </RouterLink>
          </nav>
        </details>
        <div class="admin-session">
          <span>{{ session.profile?.displayName }}</span>
          <small>{{ session.profile?.roles.join(" / ") }}</small>
          <PjButton variant="text" :disabled="session.busy" @click="logout">退出</PjButton>
        </div>
      </div>
    </header>
    <PjStatusNotice
      v-if="session.logoutError"
      class="admin-session-warning"
      tone="warning"
      title="退出尚未确认"
      assertive
    >
      <span>{{ session.logoutError }} 当前会话仍保留。</span>
      <template #actions>
        <PjButton variant="text" @click="clearLocal">仅清除此设备</PjButton>
      </template>
    </PjStatusNotice>
    <main id="admin-main" class="pj-app-main admin-main" tabindex="-1">
      <RouterView />
    </main>
  </div>
</template>