20260806173205
This commit is contained in:
1 parent
74cdb7f306
commit
6baa339efa
14 files changed
+804
-324
No files matched your search
@@ -2,34 +2,38 @@
|
||||
import zhCN from "antdv-next/locale/zh_CN";
|
||||
|
||||
const themeConfig = {
|
||||
// token: {
|
||||
// controlHeight: 26,
|
||||
// },
|
||||
// components: {
|
||||
// Splitter: {
|
||||
// splitBarSize: 1,
|
||||
// },
|
||||
// Button: {
|
||||
// contentFontSize: 12,
|
||||
// paddingInline: 10,
|
||||
// },
|
||||
// Form: {
|
||||
// labelFontSize: 12,
|
||||
// verticalLabelMargin: "0 0 2px",
|
||||
// itemMarginBottom: 8,
|
||||
// },
|
||||
// Collapse: {
|
||||
// headerBg: "#ffffff",
|
||||
// contentBg: "#ffffff",
|
||||
// borderlessContentBg: "#ffffff",
|
||||
// },
|
||||
// },
|
||||
token: {
|
||||
controlHeight: 32,
|
||||
borderRadius: 6,
|
||||
colorPrimary: '#0a0a0a',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgLayout: '#fafafa',
|
||||
colorBorder: '#e4e4e7',
|
||||
colorText: '#0a0a0a',
|
||||
colorTextSecondary: '#71717a',
|
||||
fontSize: 14,
|
||||
},
|
||||
components: {
|
||||
Splitter: {
|
||||
splitBarSize: 1,
|
||||
},
|
||||
Button: {
|
||||
contentFontSize: 14,
|
||||
},
|
||||
Tree: {
|
||||
nodeSelectedBg: '#0a0a0a',
|
||||
nodeSelectedColor: '#fafafa',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const timePickerLocale = {
|
||||
...zhCN.TimePicker,
|
||||
placeholder: "",
|
||||
rangePlaceholder: ["", ""],
|
||||
};
|
||||
|
||||
const datePickerLocale = {
|
||||
...zhCN.DatePicker,
|
||||
lang: {
|
||||
@@ -70,7 +74,7 @@ const locale = {
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 全局:按钮内图标跟随 contentFontSize(13px),覆盖 Lucide 的固定 size */
|
||||
/* 全局:按钮内图标跟随 contentFontSize,覆盖 Lucide 的固定 size */
|
||||
.ant-btn svg {
|
||||
width: 1em !important;
|
||||
height: 1em !important;
|
||||
|
||||
@@ -63,17 +63,21 @@ const routeInstanceKey = computed(
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--fms-bg);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
height: calc(100% - 16px);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 8px;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--fms-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 0 1px rgb(0 0 0 / 4%), 0 1px 3px rgb(0 0 0 / 4%), 0 4px 12px rgb(0 0 0 / 3%);
|
||||
background: #ffffff;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
@@ -83,10 +87,12 @@ const routeInstanceKey = computed(
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.route-content-wrap {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Database, House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
|
||||
import { ChevronDown, Database, House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import SidebarMenuItem from "./SidebarMenuItem.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
|
||||
const openKeys = ref(["base", "system"]);
|
||||
const openKeys = ref(["base", "system", "settings-dept"]);
|
||||
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
|
||||
|
||||
function moduleLabel(moduleId, path, fallback) {
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_code === moduleId || item.b_route === path,
|
||||
);
|
||||
return appStore.t(module?.b_i18n, fallback);
|
||||
}
|
||||
|
||||
function directoryLabel(fallback) {
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_module_type === "directory" && item.b_name === fallback,
|
||||
);
|
||||
return appStore.t(module?.b_i18n, fallback);
|
||||
}
|
||||
|
||||
function canAccessMenu(item) {
|
||||
if (item.moduleId) return permissionStore.canAccess(item.moduleId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const menuItems = computed(() => {
|
||||
const basePages = [
|
||||
{
|
||||
@@ -71,40 +75,118 @@ const menuItems = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
key: "/dashboard",
|
||||
icon: menuIcon(House),
|
||||
icon: House,
|
||||
label: appStore.t("common.dashboard", "工作台"),
|
||||
},
|
||||
];
|
||||
if (basePages.length)
|
||||
items.push({
|
||||
key: "base",
|
||||
icon: menuIcon(Database),
|
||||
icon: Database,
|
||||
label: directoryLabel("基础数据"),
|
||||
children: basePages,
|
||||
});
|
||||
if (systemPages.length)
|
||||
items.push({
|
||||
key: "system",
|
||||
icon: menuIcon(Settings),
|
||||
icon: Settings,
|
||||
label: directoryLabel("系统管理"),
|
||||
children: systemPages,
|
||||
});
|
||||
|
||||
// --- 模拟三级菜单(爷爷 → 爸爸 → 儿子)---
|
||||
items.push({
|
||||
key: "settings",
|
||||
icon: Settings,
|
||||
label: "系统设置",
|
||||
children: [
|
||||
{
|
||||
key: "settings-dept",
|
||||
label: "部门管理",
|
||||
children: [
|
||||
{ key: "/settings/dept/list", label: "部门列表" },
|
||||
{ key: "/settings/dept/position", label: "职位管理" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "settings-role",
|
||||
label: "角色权限",
|
||||
children: [
|
||||
{ key: "/settings/role/list", label: "角色列表" },
|
||||
{ key: "/settings/role/assign", label: "权限分配" },
|
||||
],
|
||||
},
|
||||
{ key: "/settings/audit", label: "操作日志" },
|
||||
],
|
||||
});
|
||||
|
||||
return items;
|
||||
});
|
||||
const selectedKeys = computed(() => [route.meta.menuKey || route.path]);
|
||||
|
||||
// 递归收集所有后代 key(用于路由变化时自动展开父级链)
|
||||
function collectKeys(item) {
|
||||
const keys = [item.key];
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
keys.push(...collectKeys(child));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
const isActive = (key) => {
|
||||
const current = route.meta.menuKey || route.path;
|
||||
return current === key;
|
||||
};
|
||||
|
||||
const isGroupOpen = (key) => openKeys.value.includes(key);
|
||||
|
||||
function toggleGroup(key) {
|
||||
if (openKeys.value.includes(key)) {
|
||||
openKeys.value = openKeys.value.filter((k) => k !== key);
|
||||
} else {
|
||||
openKeys.value = [...openKeys.value, key];
|
||||
}
|
||||
}
|
||||
|
||||
function navigate(key) {
|
||||
if (typeof key === "string" && key.startsWith("/")) router.push(key);
|
||||
}
|
||||
|
||||
function handleItemClick(item) {
|
||||
if (item.children) {
|
||||
if (!effectiveCollapsed.value) {
|
||||
toggleGroup(item.key);
|
||||
}
|
||||
} else {
|
||||
navigate(item.key);
|
||||
}
|
||||
}
|
||||
|
||||
// 展开某节点及其所有祖先
|
||||
function expandAncestors(items, targetKey) {
|
||||
for (const item of items) {
|
||||
if (!item.children) continue;
|
||||
const keys = collectKeys(item);
|
||||
if (keys.includes(targetKey)) {
|
||||
if (!openKeys.value.includes(item.key)) {
|
||||
openKeys.value = [...openKeys.value, item.key];
|
||||
}
|
||||
expandAncestors(item.children, targetKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 路由变化时自动展开父级链
|
||||
watch(
|
||||
() => route.path,
|
||||
(path) => {
|
||||
if (path.startsWith("/base") && !openKeys.value.includes("base"))
|
||||
openKeys.value = [...openKeys.value, "base"];
|
||||
if (path.startsWith("/system") && !openKeys.value.includes("system"))
|
||||
openKeys.value = [...openKeys.value, "system"];
|
||||
const current = route.meta.menuKey || path;
|
||||
expandAncestors(menuItems.value, current);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
function navigate({ key }) {
|
||||
if (typeof key === "string" && key.startsWith("/")) router.push(key);
|
||||
}
|
||||
|
||||
onMounted(() => permissionStore.load().catch(() => {}));
|
||||
</script>
|
||||
|
||||
@@ -115,24 +197,93 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
:trigger="null"
|
||||
:collapsed-width="56"
|
||||
collapsible
|
||||
resizable
|
||||
theme="light"
|
||||
width="208"
|
||||
width="220"
|
||||
>
|
||||
<div class="brand" :class="{ 'brand--collapsed': effectiveCollapsed }">
|
||||
<div class="brand__mark">F</div>
|
||||
<strong>{{ appStore.t("common.appName", "FMS 管理系统") }}</strong>
|
||||
<!-- 品牌区 -->
|
||||
<div class="sidebar-header" :class="{ collapsed: effectiveCollapsed }">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand__mark">F</div>
|
||||
<span class="sidebar-brand__name">{{ appStore.t("common.appName", "FMS 管理系统") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sider-menu">
|
||||
<a-menu
|
||||
mode="inline"
|
||||
:items="menuItems"
|
||||
:selected-keys="selectedKeys"
|
||||
:open-keys="openKeys"
|
||||
@click="navigate"
|
||||
@open-change="openKeys = $event"
|
||||
/>
|
||||
</div>
|
||||
<div class="sider-footer">
|
||||
|
||||
<!-- 菜单区 -->
|
||||
<nav class="sidebar-nav">
|
||||
<template v-for="group in menuItems" :key="group.key">
|
||||
<!-- ===== 有子菜单:可展开 ===== -->
|
||||
<template v-if="group.children">
|
||||
<!-- 展开态 -->
|
||||
<template v-if="!effectiveCollapsed">
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-menu-button"
|
||||
@click="toggleGroup(group.key)"
|
||||
>
|
||||
<component :is="group.icon" v-if="group.icon" class="sidebar-menu-icon" />
|
||||
<span class="sidebar-menu-label">{{ group.label }}</span>
|
||||
<ChevronDown
|
||||
class="sidebar-menu-chevron"
|
||||
:class="{ rotated: isGroupOpen(group.key) }"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="isGroupOpen(group.key)" class="sidebar-submenu">
|
||||
<SidebarMenuItem
|
||||
v-for="child in group.children"
|
||||
:key="child.key"
|
||||
:item="child"
|
||||
:open-keys="openKeys"
|
||||
:is-active="isActive"
|
||||
:is-group-open="isGroupOpen"
|
||||
@toggle="toggleGroup"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 折叠态 -->
|
||||
<template v-else>
|
||||
<a-tooltip :title="group.label" placement="right">
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-menu-button collapsed"
|
||||
@click="handleItemClick(group)"
|
||||
>
|
||||
<component :is="group.icon" v-if="group.icon" class="sidebar-menu-icon" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- ===== 无子菜单:普通按钮 ===== -->
|
||||
<template v-else>
|
||||
<a-tooltip v-if="effectiveCollapsed" :title="group.label" placement="right">
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-menu-button collapsed"
|
||||
:class="{ active: isActive(group.key) }"
|
||||
@click="navigate(group.key)"
|
||||
>
|
||||
<component :is="group.icon" v-if="group.icon" class="sidebar-menu-icon" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="sidebar-menu-button"
|
||||
:class="{ active: isActive(group.key) }"
|
||||
@click="navigate(group.key)"
|
||||
>
|
||||
<component :is="group.icon" v-if="group.icon" class="sidebar-menu-icon" />
|
||||
<span class="sidebar-menu-label">{{ group.label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- 底部折叠按钮 -->
|
||||
<div class="sidebar-footer">
|
||||
<a-tooltip
|
||||
:title="
|
||||
effectiveCollapsed
|
||||
@@ -140,84 +291,223 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
: appStore.t('common.collapseSidebar', '收起侧栏')
|
||||
"
|
||||
placement="right"
|
||||
><a-button type="text" class="sider-collapse" @click="appStore.toggleSidebar"
|
||||
><PanelLeftOpen v-if="effectiveCollapsed" /><PanelLeftClose v-else /></a-button
|
||||
></a-tooltip>
|
||||
>
|
||||
<button type="button" class="sidebar-collapse-btn" @click="appStore.toggleSidebar">
|
||||
<PanelLeftOpen v-if="effectiveCollapsed" class="lucide" />
|
||||
<PanelLeftClose v-else class="lucide" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* ============================================================
|
||||
* Sidebar 容器
|
||||
* ============================================================ */
|
||||
.app-sider {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--fms-border);
|
||||
border-right: 1px solid transparent;
|
||||
background: #ffffff;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-right-color: var(--border, #e4e4e7);
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-children) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
:deep(.ant-menu) {
|
||||
border-inline-end: 0;
|
||||
}
|
||||
:deep(.ant-menu-item),
|
||||
:deep(.ant-menu-submenu-title) {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
line-height: 36px;
|
||||
transition: padding-inline-start 0.2s ease;
|
||||
}
|
||||
:deep(.ant-menu-inline-collapsed) {
|
||||
.ant-menu-title-content {
|
||||
display: none !important;
|
||||
}
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-title {
|
||||
padding-inline-start: 20px !important;
|
||||
}
|
||||
.ant-menu-item-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Header / 品牌区
|
||||
* ============================================================ */
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
flex: none;
|
||||
height: 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px 0;
|
||||
|
||||
&.collapsed {
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
}
|
||||
.brand {
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px 6px 15px;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.collapsed & {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
&__mark {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
place-items: center;
|
||||
border-radius: 6px;
|
||||
background: var(--primary, #0a0a0a);
|
||||
color: var(--primary-foreground, #fafafa);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--foreground, #0a0a0a);
|
||||
|
||||
.collapsed & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brand__mark {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
place-items: center;
|
||||
border-radius: 6px;
|
||||
background: #1677ff;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.brand strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
.brand--collapsed strong {
|
||||
display: none;
|
||||
}
|
||||
.sider-menu {
|
||||
|
||||
/* ============================================================
|
||||
* 导航区域
|
||||
* ============================================================ */
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
gap: 2px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
.sider-footer {
|
||||
padding: 4px;
|
||||
border-top: 1px solid var(--fms-border);
|
||||
}
|
||||
.sider-collapse {
|
||||
|
||||
/* ---- 菜单按钮(一级 & 分组标题)---- */
|
||||
.sidebar-menu-button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #3f3f46;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent, #f4f4f5);
|
||||
color: #3f3f46;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
background: #f4f4f5;
|
||||
color: #3f3f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: none;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.sidebar-menu-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-menu-chevron {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: none;
|
||||
stroke-width: 2;
|
||||
color: var(--muted-foreground, #71717a);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 子菜单容器 ---- */
|
||||
.sidebar-submenu {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
padding: 2px 0 2px 24px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
left: 16px;
|
||||
width: 1px;
|
||||
background: var(--border, #e4e4e7);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Footer / 底部操作区
|
||||
* ============================================================ */
|
||||
.sidebar-footer {
|
||||
display: flex;
|
||||
flex: none;
|
||||
padding: 0 6px 6px;
|
||||
}
|
||||
|
||||
.sidebar-collapse-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted-foreground, #71717a);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent, #f4f4f5);
|
||||
color: var(--foreground, #0a0a0a);
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
House,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
RotateCw,
|
||||
X,
|
||||
} from "@lucide/vue";
|
||||
import { theme } from "antdv-next";
|
||||
import { ChevronLeft, ChevronRight, X } from "@lucide/vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
|
||||
@@ -19,7 +9,6 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const { token } = theme.useToken();
|
||||
const headerRef = ref(null);
|
||||
const trackRef = ref(null);
|
||||
const overflowed = ref(false);
|
||||
@@ -161,62 +150,15 @@ function handleTabKeydown(event, index) {
|
||||
focusAdjacentTab(index, event.key === "ArrowLeft" ? -1 : 1);
|
||||
}
|
||||
|
||||
function goConsole() {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
function refreshCurrentRoute() {
|
||||
appStore.refreshRoute(route.fullPath);
|
||||
}
|
||||
|
||||
function toggleMaximize() {
|
||||
appStore.toggleContentMaximized();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
ref="headerRef"
|
||||
class="tab-header"
|
||||
:style="{
|
||||
'--tabs-active-bg': token.colorPrimaryBg,
|
||||
'--tabs-active-color': token.colorPrimary,
|
||||
'--tabs-idle-bg': token.colorFillQuaternary,
|
||||
}"
|
||||
>
|
||||
<div class="tab-actions">
|
||||
<a-tooltip title="返回上一页" placement="bottom">
|
||||
<button type="button" class="tab-action" aria-label="返回上一页" @click="goBack">
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip title="刷新当前页" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="tab-action"
|
||||
aria-label="刷新当前页"
|
||||
@click="refreshCurrentRoute"
|
||||
>
|
||||
<RotateCw />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip title="返回控制台" placement="bottom">
|
||||
<button type="button" class="tab-action" aria-label="返回控制台" @click="goConsole">
|
||||
<House />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<header ref="headerRef" class="tab-header">
|
||||
<!-- 滚动按钮 -->
|
||||
<button
|
||||
v-if="overflowed"
|
||||
type="button"
|
||||
class="tab-nav-button"
|
||||
class="tab-btn"
|
||||
aria-label="向左滚动页签"
|
||||
:disabled="!canScrollLeft"
|
||||
@click="scrollTabs(-1)"
|
||||
@@ -224,6 +166,7 @@ function toggleMaximize() {
|
||||
<ChevronLeft />
|
||||
</button>
|
||||
|
||||
<!-- 标签轨道 -->
|
||||
<div ref="trackRef" class="tab-track" role="tablist" @scroll.passive="updateScrollState">
|
||||
<div
|
||||
v-for="(item, index) in tabItems"
|
||||
@@ -258,28 +201,13 @@ function toggleMaximize() {
|
||||
<button
|
||||
v-if="overflowed"
|
||||
type="button"
|
||||
class="tab-nav-button"
|
||||
class="tab-btn"
|
||||
aria-label="向右滚动页签"
|
||||
:disabled="!canScrollRight"
|
||||
@click="scrollTabs(1)"
|
||||
>
|
||||
<ChevronRight />
|
||||
</button>
|
||||
|
||||
<a-tooltip
|
||||
:title="appStore.contentMaximized ? '退出最大化' : '应用内最大化'"
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="tab-nav-button tab-maximize"
|
||||
aria-label="应用内最大化"
|
||||
@click="toggleMaximize"
|
||||
>
|
||||
<Minimize2 v-if="appStore.contentMaximized" />
|
||||
<Maximize2 v-else />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -292,16 +220,53 @@ function toggleMaximize() {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
background: var(--fms-surface);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ---- 通用小按钮 ---- */
|
||||
.tab-btn {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: #71717a;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #3f3f46;
|
||||
background: #f4f4f5;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 标签轨道 ---- */
|
||||
.tab-track {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
padding: 2px 0;
|
||||
margin: -2px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow-y: visible;
|
||||
overscroll-behavior-inline: contain;
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -310,6 +275,7 @@ function toggleMaximize() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 标签项 ---- */
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex: none;
|
||||
@@ -317,17 +283,28 @@ function toggleMaximize() {
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: var(--tabs-idle-bg);
|
||||
color: var(--fms-text-secondary);
|
||||
border-radius: 6px;
|
||||
background: #f4f4f5;
|
||||
color: #71717a;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--tabs-active-color);
|
||||
background: #f4f4f5;
|
||||
color: #3f3f46;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: var(--tabs-active-bg);
|
||||
color: var(--tabs-active-color);
|
||||
background: #f4f4f5;
|
||||
color: #3f3f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 6%), 0 1px 2px rgb(0 0 0 / 4%);
|
||||
|
||||
&:hover {
|
||||
background: #f4f4f5;
|
||||
color: #3f3f46;
|
||||
}
|
||||
}
|
||||
|
||||
&__select,
|
||||
@@ -345,8 +322,8 @@ function toggleMaximize() {
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 7px;
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
@@ -359,6 +336,7 @@ function toggleMaximize() {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgb(0 0 0 / 6%);
|
||||
@@ -367,70 +345,8 @@ function toggleMaximize() {
|
||||
.lucide {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-nav-button {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--fms-text-secondary);
|
||||
background: var(--tabs-idle-bg);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--tabs-active-color);
|
||||
background: var(--tabs-active-bg);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tab-action {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--fms-text-secondary);
|
||||
background: var(--tabs-idle-bg);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--tabs-active-color);
|
||||
background: var(--tabs-active-bg);
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { message } from "antdv-next";
|
||||
import { Bell, ChevronDown, Languages, LogOut, Settings, UserRound } from "@lucide/vue";
|
||||
import { Bell, ChevronDown, Languages, LogOut, UserRound } from "@lucide/vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
@@ -14,7 +14,6 @@ const authStore = useAuthStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const profileOpen = ref(false);
|
||||
const logoutOpen = ref(false);
|
||||
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
const items = [];
|
||||
@@ -58,18 +57,18 @@ const userMenu = computed(() => ({
|
||||
items: [
|
||||
{
|
||||
key: "profile",
|
||||
icon: menuIcon(UserRound),
|
||||
icon: () => h(UserRound, { size: 16, strokeWidth: 1.8 }),
|
||||
label: appStore.t("common.profile", "个人资料"),
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
icon: menuIcon(Settings),
|
||||
icon: () => h(Settings, { size: 16, strokeWidth: 1.8 }),
|
||||
label: appStore.t("common.accountSettings", "账户设置"),
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "logout",
|
||||
icon: menuIcon(LogOut),
|
||||
icon: () => h(LogOut, { size: 16, strokeWidth: 1.8 }),
|
||||
label: appStore.t("common.logout", "退出登录"),
|
||||
danger: true,
|
||||
},
|
||||
@@ -107,12 +106,17 @@ function confirmLogout() {
|
||||
|
||||
<template>
|
||||
<header class="top-toolbar" :class="{ 'top-toolbar--hidden': appStore.contentMaximized }">
|
||||
<a-breadcrumb class="toolbar-breadcrumb" separator=">">
|
||||
<a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.label">
|
||||
{{ item.label }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<!-- 面包屑 -->
|
||||
<nav class="toolbar-breadcrumb" aria-label="面包屑导航">
|
||||
<template v-for="(item, index) in breadcrumbItems" :key="item.label">
|
||||
<span v-if="index > 0" class="toolbar-breadcrumb__sep">/</span>
|
||||
<span class="toolbar-breadcrumb__item" :class="{ current: index === breadcrumbItems.length - 1 }">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧操作 -->
|
||||
<div class="toolbar-actions">
|
||||
<a-tooltip :title="appStore.t('common.switchLanguage', '切换语言')" placement="bottom">
|
||||
<a-dropdown
|
||||
@@ -123,27 +127,29 @@ function confirmLogout() {
|
||||
:trigger="['click']"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<a-button
|
||||
type="text"
|
||||
class="toolbar-action"
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn"
|
||||
:aria-label="appStore.t('common.switchLanguage', '切换语言')"
|
||||
>
|
||||
<template #icon><Languages /></template>
|
||||
</a-button>
|
||||
<Languages />
|
||||
</button>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip :title="appStore.t('common.notifications', '通知提醒')" placement="bottom">
|
||||
<a-button
|
||||
type="text"
|
||||
class="toolbar-action"
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn"
|
||||
:aria-label="appStore.t('common.notifications', '通知提醒')"
|
||||
>
|
||||
<a-badge dot><Bell /></a-badge>
|
||||
</a-button>
|
||||
</button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-dropdown :menu="userMenu" :trigger="['click']" placement="bottomRight">
|
||||
<button type="button" class="toolbar-user" aria-label="用户菜单">
|
||||
<a-avatar :size="24"><UserRound /></a-avatar>
|
||||
<a-avatar :size="26"><UserRound /></a-avatar>
|
||||
<span>{{ displayName }}</span>
|
||||
<ChevronDown class="toolbar-user__arrow" />
|
||||
</button>
|
||||
@@ -179,23 +185,40 @@ function confirmLogout() {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 8px 0 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
background: var(--fms-surface);
|
||||
padding: 0 8px 0 14px;
|
||||
background: #ffffff;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 面包屑 ---- */
|
||||
.toolbar-breadcrumb {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&__sep {
|
||||
color: #d4d4d8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
color: #71717a;
|
||||
|
||||
&.current {
|
||||
color: #3f3f46;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 右侧操作区 ---- */
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
@@ -204,38 +227,63 @@ function confirmLogout() {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toolbar-action {
|
||||
/* ---- 通用操作按钮 ---- */
|
||||
.toolbar-btn {
|
||||
display: grid;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding-inline: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--fms-text-secondary);
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: #71717a;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 17px;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #3f3f46;
|
||||
background: #f4f4f5;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 用户按钮 ---- */
|
||||
.toolbar-user {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 4px;
|
||||
padding: 0 6px 0 4px;
|
||||
padding: 0 8px 0 4px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--fms-text);
|
||||
border-radius: 6px;
|
||||
color: #3f3f46;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--fms-hover);
|
||||
background: #f4f4f5;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--fms-text-secondary);
|
||||
color: #71717a;
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup>
|
||||
import { ChevronDown } from "@lucide/vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
openKeys: { type: Array, required: true },
|
||||
isActive: { type: Function, required: true },
|
||||
isGroupOpen: { type: Function, required: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["toggle", "navigate"]);
|
||||
|
||||
function handleClick() {
|
||||
if (props.item.children) {
|
||||
emit("toggle", props.item.key);
|
||||
} else {
|
||||
emit("navigate", props.item.key);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 有子节点:可展开 -->
|
||||
<template v-if="item.children">
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-submenu-button"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="sidebar-submenu-label">{{ item.label }}</span>
|
||||
<ChevronDown
|
||||
class="sidebar-menu-chevron"
|
||||
:class="{ rotated: isGroupOpen(item.key) }"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="isGroupOpen(item.key)" class="sidebar-submenu-nested">
|
||||
<SidebarMenuItem
|
||||
v-for="child in item.children"
|
||||
:key="child.key"
|
||||
:item="child"
|
||||
:open-keys="openKeys"
|
||||
:is-active="isActive"
|
||||
:is-group-open="isGroupOpen"
|
||||
@toggle="(key) => emit('toggle', key)"
|
||||
@navigate="(key) => emit('navigate', key)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 叶子节点 -->
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="sidebar-submenu-item"
|
||||
:class="{ active: isActive(item.key) }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="sidebar-submenu-label">{{ item.label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ---- 叶子节点 ---- */
|
||||
.sidebar-submenu-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #3f3f46;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar-submenu-item:hover {
|
||||
background: var(--accent, #f4f4f5);
|
||||
color: #3f3f46;
|
||||
}
|
||||
|
||||
.sidebar-submenu-item.active {
|
||||
background: #f4f4f5;
|
||||
color: #3f3f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
/* ---- 可展开的父级按钮 ---- */
|
||||
.sidebar-submenu-button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #3f3f46;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar-submenu-button:hover {
|
||||
background: var(--accent, #f4f4f5);
|
||||
color: #3f3f46;
|
||||
}
|
||||
|
||||
/* ---- 嵌套容器 ---- */
|
||||
.sidebar-submenu-nested {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
padding: 2px 0 2px 16px;
|
||||
}
|
||||
|
||||
.sidebar-submenu-nested::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
left: 8px;
|
||||
width: 1px;
|
||||
background: var(--border, #e4e4e7);
|
||||
}
|
||||
|
||||
/* ---- 文字 ---- */
|
||||
.sidebar-submenu-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-menu-chevron {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex: none;
|
||||
stroke-width: 2;
|
||||
color: var(--muted-foreground, #71717a);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-menu-chevron.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -2,26 +2,27 @@
|
||||
* 基础重置 / Base Reset
|
||||
* ============================================================ */
|
||||
:root {
|
||||
color: #1f2329;
|
||||
background: #f5f7fa;
|
||||
color: #0a0a0a;
|
||||
background: #fafafa;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* 设计令牌:提升到 :root,确保 teleport 到 <body> 的模态框(如附件管理)也能继承,
|
||||
否则模态框内 var(--fms-border) 等全部未定义,导致报错且边框/颜色失效。 */
|
||||
--fms-bg: #f5f7fa;
|
||||
否则模态框内 var(--fms-border) 等全部未定义,导致报错且边框/颜色失效。
|
||||
shadcn 黑白极简风,对齐 antdv themeConfig */
|
||||
--fms-bg: #fafafa;
|
||||
--fms-surface: #ffffff;
|
||||
--fms-border: #e5e8eb;
|
||||
--fms-hover: #f2f3f5;
|
||||
--fms-text: #1f2329;
|
||||
--fms-text-secondary: #646a73;
|
||||
--fms-disabled: #c9cdd4;
|
||||
--fms-shadow: 0 4px 12px rgb(31 35 41 / 6%);
|
||||
--fms-primary: #1677ff;
|
||||
--fms-danger: #ff4d4f;
|
||||
--fms-warning: #fa8c16;
|
||||
--fms-border: #e4e4e7;
|
||||
--fms-hover: #f4f4f5;
|
||||
--fms-text: #0a0a0a;
|
||||
--fms-text-secondary: #71717a;
|
||||
--fms-disabled: #d4d4d8;
|
||||
--fms-shadow: 0 4px 12px rgb(10 10 10 / 6%);
|
||||
--fms-primary: #0a0a0a;
|
||||
--fms-danger: #ef4444;
|
||||
--fms-warning: #f59e0b;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -1,2 +1,71 @@
|
||||
@import "tailwindcss";
|
||||
@import "@antdv-next/tailwind/compat.css";
|
||||
|
||||
/* ============================================================
|
||||
* shadcn 风格主题令牌(仅亮色模式)
|
||||
* 对齐 antdv themeConfig:colorPrimary=#0a0a0a, borderRadius=6
|
||||
* ============================================================ */
|
||||
|
||||
/* 圆角比例体系:--radius 为基准 (≈6px) */
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: calc(var(--radius) - 1px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 2px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #fafafa;
|
||||
--foreground: #0a0a0a;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #0a0a0a;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #0a0a0a;
|
||||
--primary: #0a0a0a;
|
||||
--primary-foreground: #fafafa;
|
||||
--secondary: #f4f4f5;
|
||||
--secondary-foreground: #0a0a0a;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #71717a;
|
||||
--accent: #f4f4f5;
|
||||
--accent-foreground: #0a0a0a;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #fafafa;
|
||||
--border: #e4e4e7;
|
||||
--input: #e4e4e7;
|
||||
--ring: #0a0a0a;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: var(--border);
|
||||
}
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,6 @@ function goBack() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--fms-surface);
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
|
||||
@@ -85,9 +85,6 @@ function confirmBatchDelete() {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.contact-page :deep(.fms-module-list-page) {
|
||||
|
||||
@@ -503,9 +503,6 @@ onMounted(() => {
|
||||
|
||||
.othercompany-page {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.category-panel,
|
||||
|
||||
@@ -520,9 +520,6 @@ onMounted(loadCategories);
|
||||
}
|
||||
.otherdata-page {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
.category-panel,
|
||||
.data-panel {
|
||||
|
||||
@@ -240,7 +240,6 @@ function onLocaleChange(record) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
color: var(--fms-text);
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.table-demo__toolbar {
|
||||
|
||||
@@ -432,9 +432,6 @@ async function handleSave() {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.user-page :deep(.fms-module-list-page) {
|
||||
|
||||
Reference in new issue
Block a user