liu
2 天以前 4a45d6c79b2157a149afcb3b49ba504ff7c5075f
src/layouts/basic.vue
@@ -1,34 +1,33 @@
<script lang="ts" setup>
import type { NotificationItem } from '../packages/effects/layouts/src';
import type { NotificationItem } from '@vben/layouts';
import type { SystemTenantApi } from '#/api/system/tenant';
import { computed, onMounted, ref, watch } from 'vue';
import { useAccess } from '../packages/effects/access/src';
import { AuthenticationLoginExpiredModal, useVbenModal } from '../packages/effects/common-ui/src';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '../packages/constants/src';
import { isTenantEnable, useTabs, useWatermark } from '../packages/effects/hooks/src';
import { useDebounceFn } from '@vueuse/core';
import { message } from 'ant-design-vue';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
import {
  AntdProfileOutlined,
  BookOpenText,
  CircleHelp,
  IconifyIcon,
  SvgGithubIcon,
} from '../packages/icons/src';
} from '@vben/icons';
import {
  BasicLayout,
  Help,
  LockScreen,
  Notification,
  TenantDropdown,
  UserDropdown,
} from '../packages/effects/layouts/src';
import { preferences, usePreferences } from '../packages/preferences/src';
import { useAccessStore, useUserStore } from '../packages/stores/src';
import { formatDateTime, openWindow } from '../packages/utils/src';
} from '@vben/layouts';
import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { formatDateTime, openWindow } from '@vben/utils';
import { message, Tooltip } from 'ant-design-vue';
import {
  getUnreadNotifyMessageCount,
@@ -36,18 +35,20 @@
  updateAllNotifyMessageRead,
  updateNotifyMessageRead,
} from '#/api/system/notify/message';
import { getSimpleTenantList } from '#/api/system/tenant';
import { $t } from '#/locales';
import { router } from '#/router';
import { useAuthStore } from '#/store';
import {
  loadPortalUiConfig,
  savePortalUiConfigToServer,
} from '#/utils/portal-ui-config';
import LoginForm from '#/views/_core/authentication/login.vue';
import PortalBrandText from '#/components/portal/portal-brand-text.vue';
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { hasAccessByCodes } = useAccess();
const { destroyWatermark, updateWatermark } = useWatermark();
const { closeOtherTabs, refreshTab } = useTabs();
const notifications = ref<NotificationItem[]>([]);
const unreadCount = ref(0);
@@ -67,30 +68,35 @@
    text: $t('ui.widgets.profile'),
  },
  {
    handler: () => {
      openWindow(VBEN_DOC_URL, {
        target: '_blank',
      });
    },
    icon: BookOpenText,
    text: $t('ui.widgets.document'),
    handler: () => router.push({ name: 'Profile', query: { section: 'resetPwd' } }),
    icon: 'lucide:shield-check',
    text: '密码设置',
  },
  {
    handler: () => {
      openWindow(VBEN_GITHUB_URL, {
        target: '_blank',
      });
    },
    icon: SvgGithubIcon,
    text: 'GitHub',
  },
  {
    handler: () => {
      helpModalApi.open();
    },
    icon: CircleHelp,
    text: $t('ui.widgets.qa'),
  },
  // {
  //   handler: () => {
  //     openWindow(VBEN_DOC_URL, {
  //       target: '_blank',
  //     });
  //   },
  //   icon: BookOpenText,
  //   text: $t('ui.widgets.document'),
  // },
  // {
  //   handler: () => {
  //     openWindow(VBEN_GITHUB_URL, {
  //       target: '_blank',
  //     });
  //   },
  //   icon: SvgGithubIcon,
  //   text: 'GitHub',
  // },
  // {
  //   handler: () => {
  //     helpModalApi.open();
  //   },
  //   icon: CircleHelp,
  //   text: $t('ui.widgets.qa'),
  // },
]);
const avatar = computed(() => {
@@ -157,47 +163,45 @@
  handleNotificationGetUnreadCount();
}
/** 打开 IM 聊天 */
function handleOpenImHome() {
  const { href } = router.resolve({ name: 'ImHome' });
  window.open(href, '_blank');
}
// ========== 门户字号全局配置(存数据库,所有用户共用) ==========
/** 已同步到数据库的字号,用于避免读取后回写 */
let portalFontBaseline: null | { home: number; menu: number } = null;
/** 保存失败只提示一次,避免连续调节时反复弹窗 */
let portalFontSaveWarned = false;
// 租户列表
const tenants = ref<SystemTenantApi.Tenant[]>([]);
const tenantEnable = computed(
  () => hasAccessByCodes(['system:tenant:visit']) && isTenantEnable(),
);
/** 获取租户列表 */
async function handleGetTenantList() {
  if (tenantEnable.value) {
    tenants.value = await getSimpleTenantList();
  }
}
/** 处理租户切换 */
async function handleTenantChange(tenant: SystemTenantApi.Tenant) {
  if (!tenant || !tenant.id) {
    message.error('切换租户失败');
const persistPortalFontConfig = useDebounceFn(async () => {
  const home = preferences.app.homeFontSize;
  const menu = preferences.navigation.fontSize;
  if (portalFontBaseline && portalFontBaseline.home === home && portalFontBaseline.menu === menu) {
    return;
  }
  // 设置访问租户 ID
  accessStore.setVisitTenantId(tenant.id as number);
  // 关闭其他标签页,只保留当前页
  await closeOtherTabs();
  // 刷新当前页面
  await refreshTab();
  // 提示切换成功
  message.success(`切换当前租户为: ${tenant.name}`);
}
  try {
    await savePortalUiConfigToServer({ homeFontSize: home, menuFontSize: menu });
    portalFontBaseline = { home, menu };
    portalFontSaveWarned = false;
  } catch {
    if (!portalFontSaveWarned) {
      portalFontSaveWarned = true;
      message.warning('字号已在本机生效,但保存到全局失败,请确认是否有参数配置修改权限');
    }
  }
}, 800);
watch(
  () => [preferences.app.homeFontSize, preferences.navigation.fontSize],
  () => persistPortalFontConfig(),
);
// ========== 初始化 ==========
onMounted(() => {
onMounted(async () => {
  // 读取数据库中的全局字号配置并应用
  await loadPortalUiConfig();
  portalFontBaseline = {
    home: preferences.app.homeFontSize,
    menu: preferences.navigation.fontSize,
  };
  // 首次加载未读数量
  handleNotificationGetUnreadCount();
  // 获取租户列表
  handleGetTenantList();
  // 轮询刷新未读数量
  setInterval(
    () => {
@@ -275,9 +279,13 @@
</script>
<template>
  <BasicLayout @clear-preferences-and-logout="handleLogout">
  <BasicLayout class="supply-portal-shell" @clear-preferences-and-logout="handleLogout" @click-logo="router.push('/analytics')">
    <template #logo-text>
      <PortalBrandText />
    </template>
    <template #user-dropdown>
      <UserDropdown
        show-text
        :avatar
        :menus
        :text="userStore.userInfo?.nickname"
@@ -286,6 +294,9 @@
        @logout="handleLogout"
        @clear-preferences-and-logout="handleLogout"
      />
    </template>
    <template #header-right-165>
      <button type="button" class="portal-personal-entry" aria-label="个人中心" title="个人中心" :class="{ 'is-current': router.currentRoute.value.name === 'Profile' }" @click="router.push({ name: 'Profile' })"><IconifyIcon icon="lucide:user-round" /><span>个人中心</span></button>
    </template>
    <template #notification>
      <Notification
@@ -299,26 +310,15 @@
        @on-click="handleClick"
      />
    </template>
    <template #header-right-1>
      <div v-if="tenantEnable">
        <TenantDropdown
          class="mr-2"
          :tenant-list="tenants"
          :visit-tenant-id="accessStore.visitTenantId"
          @success="handleTenantChange"
        />
      </div>
    </template>
    <template #header-right-900>
      <Tooltip title="IM 聊天">
        <button
          class="hover:bg-accent hover:text-accent-foreground mr-1 inline-flex size-8 items-center justify-center rounded-md transition-colors"
          type="button"
          @click="handleOpenImHome"
        >
          <IconifyIcon class="size-4" icon="lucide:message-circle" />
        </button>
      </Tooltip>
    <template v-if="router.hasRoute('MesWorkbench') && accessStore.accessMenus.some((menu) => /生产|MES/i.test(menu.name))" #header-right-170>
      <button
        type="button"
        class="flex cursor-pointer items-center gap-1.5 rounded-md border-none bg-primary/15 px-3 py-1.5 text-sm font-medium text-primary transition-colors hover:bg-primary/25"
        @click="router.push({ name: 'MesWorkbench' })"
      >
        <IconifyIcon icon="lucide:factory" class="size-4" />
        <span>工作台</span>
      </button>
    </template>
    <template #extra>
      <AuthenticationLoginExpiredModal