feat(系统设置): 按原型重做菜单资源设置并支持折叠控制
This commit is contained in:
@@ -1,4 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 菜单设置(原型 V10.2 `#setMenuCard`)。
|
||||
*
|
||||
* 版式照原型:一张 card(标题 + 右侧说明)里放 `.menuset` 两栏 ——
|
||||
* 左 340px `.ms-tree`(`.ms-ops` 四个按钮 + 可滚动的 `.ms-node` 行列表),
|
||||
* 右 `.ms-detail`(`.ch` 标题 + `.tbl` 事实表 + 底部按钮行)。
|
||||
*
|
||||
* 与原型的两处**有意差异**(都是沿用仓库既有口径,没跟着原型改):
|
||||
* 1. 树默认**全部展开**(原型默认展开目录、折叠菜单)。演示时更直观,且与改版前一致。
|
||||
* 2. 资源树的层级是「目录 / 菜单 / 按钮」三层,但**按钮层不允许再新增下级**
|
||||
* (原型用类型推导限制,这里同样只对 DIR / MENU 开放「新增下级」)。
|
||||
*
|
||||
* 功能逻辑(接口、权限闸、保护节点、校验)全部沿用改版前实现,本次只动样式与布局。
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
@@ -22,8 +36,8 @@ const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const tree = ref<PermissionResourceNodeDto[]>([])
|
||||
const selectedId = ref('')
|
||||
const expanded = ref(true)
|
||||
const treeKey = ref(0)
|
||||
/** 折叠的节点 id;空数组 = 全部展开(默认) */
|
||||
const collapsedIds = ref<string[]>([])
|
||||
const editorVisible = ref(false)
|
||||
const editingId = ref('')
|
||||
const form = ref({
|
||||
@@ -56,6 +70,40 @@ const formParentName = computed(() => {
|
||||
const editorTitle = computed(
|
||||
() => `${editingId.value ? '编辑' : '新增'}${typeName(form.value.resourceType)}`,
|
||||
)
|
||||
/** 详情标题右侧的面包屑:目录:X → 菜单:Y → 按钮:Z */
|
||||
const currentPath = computed(() => {
|
||||
const node = currentNode.value
|
||||
if (!node) return ''
|
||||
const all = flatten(tree.value)
|
||||
const parts: string[] = []
|
||||
let cursor: PermissionResourceNodeDto | null = node
|
||||
while (cursor) {
|
||||
parts.unshift(`${typeName(cursor.resourceType)}:${cursor.name}`)
|
||||
const parentId: string | null = cursor.parentId
|
||||
cursor = parentId ? (all.find((item) => item.id === parentId) ?? null) : null
|
||||
}
|
||||
return parts.join(' → ')
|
||||
})
|
||||
|
||||
interface TreeRow {
|
||||
node: PermissionResourceNodeDto
|
||||
level: number
|
||||
hasChildren: boolean
|
||||
}
|
||||
|
||||
/** 按折叠状态把树拍平成可见行(保持接口返回的原始顺序,不重排) */
|
||||
const treeRows = computed<TreeRow[]>(() => {
|
||||
const rows: TreeRow[] = []
|
||||
const walk = (nodes: PermissionResourceNodeDto[], level: number) => {
|
||||
nodes.forEach((node) => {
|
||||
const children = node.children ?? []
|
||||
rows.push({ node, level, hasChildren: children.length > 0 })
|
||||
if (children.length && isOpen(node.id)) walk(children, level + 1)
|
||||
})
|
||||
}
|
||||
walk(tree.value, 0)
|
||||
return rows
|
||||
})
|
||||
|
||||
function flatten(nodes: PermissionResourceNodeDto[]): PermissionResourceNodeDto[] {
|
||||
return nodes.flatMap((node) => [node, ...flatten(node.children ?? [])])
|
||||
@@ -72,6 +120,33 @@ function typeName(type: PermissionResourceNodeDto['resourceType']) {
|
||||
return type === 'DIR' ? '目录' : type === 'MENU' ? '菜单' : '按钮'
|
||||
}
|
||||
|
||||
/** 原型徽标类名是 ty-dir / ty-menu / ty-btn(注意 BUTTON → btn,不是 button) */
|
||||
function typeClass(type: PermissionResourceNodeDto['resourceType']) {
|
||||
return type === 'DIR' ? 'ty-dir' : type === 'MENU' ? 'ty-menu' : 'ty-btn'
|
||||
}
|
||||
|
||||
/* ---------------- 展开 / 折叠 ---------------- */
|
||||
|
||||
function isOpen(id: string) {
|
||||
return !collapsedIds.value.includes(id)
|
||||
}
|
||||
|
||||
function toggleOpen(id: string) {
|
||||
collapsedIds.value = isOpen(id)
|
||||
? [...collapsedIds.value, id]
|
||||
: collapsedIds.value.filter((item) => item !== id)
|
||||
}
|
||||
|
||||
function expandAll() {
|
||||
collapsedIds.value = []
|
||||
}
|
||||
|
||||
function collapseAll() {
|
||||
collapsedIds.value = flatten(tree.value)
|
||||
.filter((node) => (node.children ?? []).length > 0)
|
||||
.map((node) => node.id)
|
||||
}
|
||||
|
||||
function selectNode(node: PermissionResourceNodeDto) {
|
||||
selectedId.value = node.id
|
||||
}
|
||||
@@ -84,7 +159,6 @@ async function loadTree() {
|
||||
if (!flatten(tree.value).some((node) => node.id === selectedId.value)) {
|
||||
selectedId.value = tree.value[0]?.id ?? ''
|
||||
}
|
||||
treeKey.value += 1
|
||||
} catch {
|
||||
ElMessage.error('菜单资源加载失败,请检查菜单配置权限')
|
||||
} finally {
|
||||
@@ -282,173 +356,213 @@ async function moveNode(node: PermissionResourceNodeDto, direction: -1 | 1) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpansion() {
|
||||
expanded.value = !expanded.value
|
||||
treeKey.value += 1
|
||||
}
|
||||
|
||||
onMounted(loadTree)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="menu-settings">
|
||||
<header class="menu-toolbar">
|
||||
<div>
|
||||
<strong>菜单资源</strong>
|
||||
<p>菜单资源同时作为导航、路由和角色授权的唯一来源。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button :disabled="!canManage" @click="toggleExpansion">{{
|
||||
expanded ? '全部收起' : '全部展开'
|
||||
}}</el-button>
|
||||
<el-button type="primary" :disabled="!canManage" @click="openCreate('DIR', null)"
|
||||
>新增目录</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="
|
||||
!canManage ||
|
||||
!currentNode ||
|
||||
currentNode.resourceType === 'BUTTON' ||
|
||||
isEffectivelyDisabled(currentNode)
|
||||
"
|
||||
@click="
|
||||
currentNode &&
|
||||
openCreate(currentNode.resourceType === 'DIR' ? 'MENU' : 'BUTTON', currentNode.id)
|
||||
"
|
||||
>新增下级</el-button
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
<section class="menu-settings card">
|
||||
<div class="ch">
|
||||
<b>菜单设置</b>
|
||||
<span class="sp"></span>
|
||||
<span class="sm mut"
|
||||
>定义系统有哪些目录 / 菜单 / 按钮权限;「角色权限」负责把这些权限分配给角色</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="resource-workspace">
|
||||
<aside class="resource-tree-card">
|
||||
<el-tree
|
||||
:key="treeKey"
|
||||
:data="tree"
|
||||
:props="{ children: 'children', label: 'name' }"
|
||||
node-key="id"
|
||||
:default-expand-all="expanded"
|
||||
:expand-on-click-node="false"
|
||||
highlight-current
|
||||
:current-node-key="selectedId"
|
||||
@node-click="selectNode"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span class="tree-node">
|
||||
<span class="type-icon" :class="`type-${data.resourceType.toLowerCase()}`">{{
|
||||
data.resourceType === 'DIR' ? '目' : data.resourceType === 'MENU' ? '页' : '钮'
|
||||
}}</span>
|
||||
<span class="tree-name">{{ data.name }}</span>
|
||||
<small>#{{ data.sortNo }}</small>
|
||||
<em :class="data.status === 'ENABLED' ? 'enabled' : 'disabled'">{{
|
||||
data.status === 'ENABLED' ? '启用' : '停用'
|
||||
}}</em>
|
||||
<div v-loading="loading" class="menuset">
|
||||
<div class="ms-tree">
|
||||
<div class="ms-ops">
|
||||
<button
|
||||
type="button"
|
||||
class="btn gh sm"
|
||||
:disabled="!canManage"
|
||||
@click="openCreate('DIR', null)"
|
||||
>
|
||||
+ 新增目录
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn gh sm"
|
||||
:disabled="
|
||||
!canManage ||
|
||||
!currentNode ||
|
||||
currentNode.resourceType === 'BUTTON' ||
|
||||
isEffectivelyDisabled(currentNode)
|
||||
"
|
||||
@click="
|
||||
currentNode &&
|
||||
openCreate(currentNode.resourceType === 'DIR' ? 'MENU' : 'BUTTON', currentNode.id)
|
||||
"
|
||||
>
|
||||
+ 新增下级
|
||||
</button>
|
||||
<button type="button" class="btn gh sm" @click="expandAll">展开全部</button>
|
||||
<button type="button" class="btn gh sm" @click="collapseAll">收起全部</button>
|
||||
</div>
|
||||
|
||||
<div class="ms-tree-box">
|
||||
<div
|
||||
v-for="row in treeRows"
|
||||
:key="row.node.id"
|
||||
class="ms-node"
|
||||
:class="[
|
||||
`lv${row.level}`,
|
||||
{ on: selectedId === row.node.id, off: isEffectivelyDisabled(row.node) },
|
||||
]"
|
||||
@click="selectNode(row.node)"
|
||||
>
|
||||
<span class="ar" @click.stop="row.hasChildren && toggleOpen(row.node.id)">{{
|
||||
row.hasChildren ? (isOpen(row.node.id) ? '▾' : '▸') : ''
|
||||
}}</span>
|
||||
<span class="ty" :class="typeClass(row.node.resourceType)">
|
||||
{{ typeName(row.node.resourceType) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<div v-if="!loading && !tree.length" class="tree-empty">暂无菜单资源</div>
|
||||
</aside>
|
||||
|
||||
<section v-if="currentNode" class="resource-detail-card">
|
||||
<header class="detail-heading">
|
||||
<div>
|
||||
<span class="detail-type">{{ typeName(currentNode.resourceType) }}</span>
|
||||
<h2>{{ currentNode.name }}</h2>
|
||||
<span class="nm"
|
||||
>{{ row.node.icon ? `${row.node.icon} ` : '' }}{{ row.node.name }}</span
|
||||
>
|
||||
<span class="sp"></span>
|
||||
<span class="sm mut">{{ row.node.sortNo }}</span>
|
||||
<span v-if="row.node.status === 'DISABLED'" class="ms-off-tag">停用</span>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button
|
||||
<div v-if="!treeRows.length && !loading" class="sm mut ms-empty">
|
||||
暂无菜单资源,请先新增目录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-detail">
|
||||
<template v-if="currentNode">
|
||||
<div class="ch">
|
||||
<b>{{ currentNode.icon ? `${currentNode.icon} ` : '' }}{{ currentNode.name }}</b>
|
||||
<span class="sp"></span>
|
||||
<span class="sm mut">{{ currentPath }}</span>
|
||||
</div>
|
||||
|
||||
<table class="tbl">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="k">资源类型</td>
|
||||
<td>{{ typeName(currentNode.resourceType) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="k">名称</td>
|
||||
<td>{{ currentNode.name }}</td>
|
||||
</tr>
|
||||
<tr v-if="currentNode.resourceType !== 'DIR'">
|
||||
<td class="k">权限标识</td>
|
||||
<td>
|
||||
<span class="mono sm">{{ currentNode.code ?? '—' }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="k">上级资源</td>
|
||||
<td>{{ currentParentName }}</td>
|
||||
</tr>
|
||||
<tr v-if="currentNode.resourceType === 'MENU'">
|
||||
<td class="k">路由</td>
|
||||
<td>{{ currentNode.routePath || '—' }}</td>
|
||||
</tr>
|
||||
<tr v-if="currentNode.resourceType === 'MENU'">
|
||||
<td class="k">组件</td>
|
||||
<td>{{ currentNode.componentPath || '—' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="k">图标</td>
|
||||
<td>{{ currentNode.icon || '—' }}</td>
|
||||
</tr>
|
||||
<tr v-if="currentNode.resourceType === 'MENU'">
|
||||
<td class="k">是否显示</td>
|
||||
<td>{{ currentNode.visible ? '显示' : '隐藏(仍可通过授权路由访问)' }}</td>
|
||||
</tr>
|
||||
<tr v-if="currentNode.resourceType === 'MENU'">
|
||||
<td class="k">是否缓存</td>
|
||||
<td>{{ currentNode.cacheable ? '缓存' : '不缓存' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="k">排序</td>
|
||||
<td>{{ currentNode.sortNo }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="k">状态</td>
|
||||
<td>{{ currentNode.status === 'ENABLED' ? '启用' : '停用' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="k">核心资源</td>
|
||||
<td>{{ currentNode.protectedNode ? '是(不可删除或停用)' : '否' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="ms-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn gh sm"
|
||||
:disabled="!canManage || currentNode.protectedNode"
|
||||
@click="openEdit(currentNode)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
v-if="currentNode.resourceType !== 'BUTTON'"
|
||||
type="button"
|
||||
class="btn gh sm"
|
||||
:disabled="
|
||||
!canManage || currentNode.protectedNode || isEffectivelyDisabled(currentNode)
|
||||
"
|
||||
@click="
|
||||
openCreate(currentNode.resourceType === 'DIR' ? 'MENU' : 'BUTTON', currentNode.id)
|
||||
"
|
||||
>
|
||||
新增下级
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn gh sm"
|
||||
:disabled="!canManage"
|
||||
@click="moveNode(currentNode, -1)"
|
||||
>
|
||||
上移
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn gh sm"
|
||||
:disabled="!canManage"
|
||||
@click="moveNode(currentNode, 1)"
|
||||
>
|
||||
下移
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn gh sm"
|
||||
:disabled="
|
||||
!canManage ||
|
||||
currentNode.protectedNode ||
|
||||
(currentNode.status === 'ENABLED' && hasProtectedDescendant(currentNode))
|
||||
"
|
||||
@click="changeStatus(currentNode)"
|
||||
>{{ currentNode.status === 'ENABLED' ? '停用' : '启用' }}</el-button
|
||||
>
|
||||
<el-button
|
||||
{{ currentNode.status === 'ENABLED' ? '停用' : '启用' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn dg sm"
|
||||
:disabled="
|
||||
!canManage ||
|
||||
currentNode.protectedNode ||
|
||||
(currentNode.resourceType === 'DIR' && currentNode.children.length > 0) ||
|
||||
hasProtectedDescendant(currentNode)
|
||||
"
|
||||
type="danger"
|
||||
plain
|
||||
@click="removeResource(currentNode)"
|
||||
>删除</el-button
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<dl class="resource-facts">
|
||||
<div>
|
||||
<dt>权限标识</dt>
|
||||
<dd>{{ currentNode.code ?? '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>上级资源</dt>
|
||||
<dd>
|
||||
{{ currentParentName }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>排序</dt>
|
||||
<dd>
|
||||
{{ currentNode.sortNo }}
|
||||
<span class="sort-actions"
|
||||
><button
|
||||
type="button"
|
||||
:disabled="!canManage"
|
||||
aria-label="上移"
|
||||
@click="moveNode(currentNode, -1)"
|
||||
>
|
||||
↑</button
|
||||
><button
|
||||
type="button"
|
||||
:disabled="!canManage"
|
||||
aria-label="下移"
|
||||
@click="moveNode(currentNode, 1)"
|
||||
>
|
||||
↓
|
||||
</button></span
|
||||
>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="currentNode.resourceType === 'MENU'">
|
||||
<dt>路由</dt>
|
||||
<dd>{{ currentNode.routePath || '—' }}</dd>
|
||||
</div>
|
||||
<div v-if="currentNode.resourceType === 'MENU'">
|
||||
<dt>组件</dt>
|
||||
<dd>{{ currentNode.componentPath || '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>图标</dt>
|
||||
<dd>{{ currentNode.icon || '—' }}</dd>
|
||||
</div>
|
||||
<div v-if="currentNode.resourceType === 'MENU'">
|
||||
<dt>导航显示</dt>
|
||||
<dd>{{ currentNode.visible ? '显示' : '隐藏(仍可通过授权路由访问)' }}</dd>
|
||||
</div>
|
||||
<div v-if="currentNode.resourceType === 'MENU'">
|
||||
<dt>缓存</dt>
|
||||
<dd>{{ currentNode.cacheable ? '开启' : '关闭' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>核心资源</dt>
|
||||
<dd>{{ currentNode.protectedNode ? '是(不可删除或停用)' : '否' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
<section v-else class="resource-detail-card empty-detail">
|
||||
从左侧选择资源,查看或维护配置。
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div v-else class="sm mut ms-empty-detail">
|
||||
← 请在左侧选择 目录 / 菜单 / 按钮,查看并编辑配置
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="editorVisible" :title="editorTitle" width="560px" destroy-on-close>
|
||||
@@ -503,167 +617,283 @@ onMounted(loadTree)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.menu-settings {
|
||||
min-height: 480px;
|
||||
/* 原型 .card:本分区在原型里就是一张 card(标题 + 两栏工作区) */
|
||||
.card {
|
||||
padding: 16px 18px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
.menu-toolbar,
|
||||
.detail-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.menu-toolbar {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.menu-toolbar strong {
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
}
|
||||
.menu-toolbar p {
|
||||
margin: 5px 0 0;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
}
|
||||
.toolbar-actions {
|
||||
|
||||
/* 原型 .card .ch */
|
||||
.ch {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.resource-workspace {
|
||||
|
||||
.ch b {
|
||||
color: var(--ink);
|
||||
font-size: 14.5px;
|
||||
}
|
||||
|
||||
.ch .sp {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mut {
|
||||
color: var(--mut);
|
||||
}
|
||||
|
||||
.sm {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 原型 .menuset */
|
||||
.menuset {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.85fr) minmax(380px, 1.5fr);
|
||||
grid-template-columns: 340px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
min-height: 420px;
|
||||
min-height: 470px;
|
||||
}
|
||||
.resource-tree-card,
|
||||
.resource-detail-card {
|
||||
min-height: 420px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
|
||||
/* 原型 .ms-tree */
|
||||
.ms-tree {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.tree-node {
|
||||
|
||||
/* 原型 .ms-ops */
|
||||
.ms-ops {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
/* 原型 .ms-tree #menuTreeBox */
|
||||
.ms-tree-box {
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 原型 .ms-node 系列 */
|
||||
.ms-node {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.type-icon {
|
||||
display: inline-grid;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--brand);
|
||||
font-size: 10px;
|
||||
place-items: center;
|
||||
|
||||
.ms-node:hover {
|
||||
background: var(--brand-l);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.type-button {
|
||||
color: #a06c1c;
|
||||
background: #fff4dd;
|
||||
|
||||
.ms-node.on {
|
||||
color: #fff;
|
||||
background: var(--brand);
|
||||
}
|
||||
.tree-name {
|
||||
|
||||
.ms-node.on .sm,
|
||||
.ms-node.on .ar {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ms-node.on .ty-dir {
|
||||
color: #0e6e8c;
|
||||
}
|
||||
|
||||
.ms-node.on .ty-menu {
|
||||
color: #3a7bd5;
|
||||
}
|
||||
|
||||
.ms-node.on .ty-btn {
|
||||
color: #d9821f;
|
||||
}
|
||||
|
||||
.ms-node.off {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.ms-node .ar {
|
||||
flex: none;
|
||||
width: 12px;
|
||||
color: var(--mut);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.ms-node .nm {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tree-node small {
|
||||
margin-left: auto;
|
||||
color: #98a8ae;
|
||||
|
||||
.ms-node .sp {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ms-node.lv1 {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.ms-node.lv2 {
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
/* 原型 .ty 类型徽标 */
|
||||
.ty {
|
||||
flex: none;
|
||||
padding: 1px 6px;
|
||||
color: #687f8b;
|
||||
font-size: 10px;
|
||||
}
|
||||
.tree-node em {
|
||||
padding: 1px 5px;
|
||||
font-size: 9px;
|
||||
font-style: normal;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.tree-node .enabled {
|
||||
color: #18875d;
|
||||
background: #e7f6ef;
|
||||
}
|
||||
.tree-node .disabled {
|
||||
color: #849198;
|
||||
background: #eef1f2;
|
||||
}
|
||||
.detail-heading {
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.detail-heading h2 {
|
||||
margin: 4px 0 0;
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
}
|
||||
.detail-type {
|
||||
color: var(--brand);
|
||||
font-size: 11px;
|
||||
}
|
||||
.resource-facts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 18px;
|
||||
margin: 0;
|
||||
}
|
||||
.resource-facts div {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #eef3f5;
|
||||
}
|
||||
.resource-facts dt {
|
||||
margin-bottom: 5px;
|
||||
color: var(--mut);
|
||||
font-size: 11px;
|
||||
}
|
||||
.resource-facts dd {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sort-actions {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.sort-actions button {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: var(--brand);
|
||||
background: #f2f9fb;
|
||||
border: 1px solid var(--line);
|
||||
background: #eef1f3;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.sort-actions button:disabled {
|
||||
opacity: 0.45;
|
||||
|
||||
.ty-dir {
|
||||
color: #0e6e8c;
|
||||
background: #e3f0f4;
|
||||
}
|
||||
.tree-empty,
|
||||
.empty-detail {
|
||||
display: grid;
|
||||
|
||||
.ty-menu {
|
||||
color: #3a7bd5;
|
||||
background: #e7effb;
|
||||
}
|
||||
|
||||
.ty-btn {
|
||||
color: #d9821f;
|
||||
background: #fbf0dd;
|
||||
}
|
||||
|
||||
/* 原型树节点上的「停用」小标 */
|
||||
.ms-off-tag {
|
||||
flex: none;
|
||||
padding: 2px 10px;
|
||||
color: #8a9aa3;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: #eef1f3;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
/* 原型 .ms-detail */
|
||||
.ms-detail {
|
||||
min-height: 470px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 原型 .tbl */
|
||||
.tbl {
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
font-size: 13px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.tbl td {
|
||||
padding: 9px 12px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #eef4f6;
|
||||
}
|
||||
|
||||
.tbl tr:hover td {
|
||||
background: #f7fbfc;
|
||||
}
|
||||
|
||||
.tbl td.k {
|
||||
width: 110px;
|
||||
color: var(--mut);
|
||||
place-items: center;
|
||||
}
|
||||
.tree-empty {
|
||||
padding: 50px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.resource-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: Consolas, 'Microsoft YaHei', monospace;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.menu-toolbar,
|
||||
.detail-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.resource-facts {
|
||||
|
||||
.ms-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 原型 .btn / .btn.gh / .btn.dg / .btn.sm */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
background: var(--brand);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--brand-d);
|
||||
}
|
||||
|
||||
.btn.gh {
|
||||
color: var(--brand);
|
||||
background: #fff;
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.btn.gh:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
|
||||
.btn.dg {
|
||||
color: var(--err);
|
||||
background: #fff;
|
||||
border-color: var(--err);
|
||||
}
|
||||
|
||||
.btn.dg:hover {
|
||||
background: var(--err-l);
|
||||
}
|
||||
|
||||
.btn.sm {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.ms-empty {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.ms-empty-detail {
|
||||
display: grid;
|
||||
min-height: 430px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.menuset {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ms-detail {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ms-empty-detail {
|
||||
min-height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user