feat(系统设置): 按原型重做菜单资源设置并支持折叠控制
This commit is contained in:
@@ -1,4 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<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 { computed, onMounted, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
@@ -22,8 +36,8 @@ const loading = ref(false)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const tree = ref<PermissionResourceNodeDto[]>([])
|
const tree = ref<PermissionResourceNodeDto[]>([])
|
||||||
const selectedId = ref('')
|
const selectedId = ref('')
|
||||||
const expanded = ref(true)
|
/** 折叠的节点 id;空数组 = 全部展开(默认) */
|
||||||
const treeKey = ref(0)
|
const collapsedIds = ref<string[]>([])
|
||||||
const editorVisible = ref(false)
|
const editorVisible = ref(false)
|
||||||
const editingId = ref('')
|
const editingId = ref('')
|
||||||
const form = ref({
|
const form = ref({
|
||||||
@@ -56,6 +70,40 @@ const formParentName = computed(() => {
|
|||||||
const editorTitle = computed(
|
const editorTitle = computed(
|
||||||
() => `${editingId.value ? '编辑' : '新增'}${typeName(form.value.resourceType)}`,
|
() => `${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[] {
|
function flatten(nodes: PermissionResourceNodeDto[]): PermissionResourceNodeDto[] {
|
||||||
return nodes.flatMap((node) => [node, ...flatten(node.children ?? [])])
|
return nodes.flatMap((node) => [node, ...flatten(node.children ?? [])])
|
||||||
@@ -72,6 +120,33 @@ function typeName(type: PermissionResourceNodeDto['resourceType']) {
|
|||||||
return type === 'DIR' ? '目录' : type === 'MENU' ? '菜单' : '按钮'
|
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) {
|
function selectNode(node: PermissionResourceNodeDto) {
|
||||||
selectedId.value = node.id
|
selectedId.value = node.id
|
||||||
}
|
}
|
||||||
@@ -84,7 +159,6 @@ async function loadTree() {
|
|||||||
if (!flatten(tree.value).some((node) => node.id === selectedId.value)) {
|
if (!flatten(tree.value).some((node) => node.id === selectedId.value)) {
|
||||||
selectedId.value = tree.value[0]?.id ?? ''
|
selectedId.value = tree.value[0]?.id ?? ''
|
||||||
}
|
}
|
||||||
treeKey.value += 1
|
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('菜单资源加载失败,请检查菜单配置权限')
|
ElMessage.error('菜单资源加载失败,请检查菜单配置权限')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -282,173 +356,213 @@ async function moveNode(node: PermissionResourceNodeDto, direction: -1 | 1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleExpansion() {
|
|
||||||
expanded.value = !expanded.value
|
|
||||||
treeKey.value += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadTree)
|
onMounted(loadTree)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="menu-settings">
|
<section class="menu-settings card">
|
||||||
<header class="menu-toolbar">
|
<div class="ch">
|
||||||
<div>
|
<b>菜单设置</b>
|
||||||
<strong>菜单资源</strong>
|
<span class="sp"></span>
|
||||||
<p>菜单资源同时作为导航、路由和角色授权的唯一来源。</p>
|
<span class="sm mut"
|
||||||
</div>
|
>定义系统有哪些目录 / 菜单 / 按钮权限;「角色权限」负责把这些权限分配给角色</span
|
||||||
<div class="toolbar-actions">
|
>
|
||||||
<el-button :disabled="!canManage" @click="toggleExpansion">{{
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
<div v-loading="loading" class="resource-workspace">
|
<div v-loading="loading" class="menuset">
|
||||||
<aside class="resource-tree-card">
|
<div class="ms-tree">
|
||||||
<el-tree
|
<div class="ms-ops">
|
||||||
:key="treeKey"
|
<button
|
||||||
:data="tree"
|
type="button"
|
||||||
:props="{ children: 'children', label: 'name' }"
|
class="btn gh sm"
|
||||||
node-key="id"
|
:disabled="!canManage"
|
||||||
:default-expand-all="expanded"
|
@click="openCreate('DIR', null)"
|
||||||
:expand-on-click-node="false"
|
>
|
||||||
highlight-current
|
+ 新增目录
|
||||||
:current-node-key="selectedId"
|
</button>
|
||||||
@node-click="selectNode"
|
<button
|
||||||
>
|
type="button"
|
||||||
<template #default="{ data }">
|
class="btn gh sm"
|
||||||
<span class="tree-node">
|
:disabled="
|
||||||
<span class="type-icon" :class="`type-${data.resourceType.toLowerCase()}`">{{
|
!canManage ||
|
||||||
data.resourceType === 'DIR' ? '目' : data.resourceType === 'MENU' ? '页' : '钮'
|
!currentNode ||
|
||||||
}}</span>
|
currentNode.resourceType === 'BUTTON' ||
|
||||||
<span class="tree-name">{{ data.name }}</span>
|
isEffectivelyDisabled(currentNode)
|
||||||
<small>#{{ data.sortNo }}</small>
|
"
|
||||||
<em :class="data.status === 'ENABLED' ? 'enabled' : 'disabled'">{{
|
@click="
|
||||||
data.status === 'ENABLED' ? '启用' : '停用'
|
currentNode &&
|
||||||
}}</em>
|
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>
|
</span>
|
||||||
</template>
|
<span class="nm"
|
||||||
</el-tree>
|
>{{ row.node.icon ? `${row.node.icon} ` : '' }}{{ row.node.name }}</span
|
||||||
<div v-if="!loading && !tree.length" class="tree-empty">暂无菜单资源</div>
|
>
|
||||||
</aside>
|
<span class="sp"></span>
|
||||||
|
<span class="sm mut">{{ row.node.sortNo }}</span>
|
||||||
<section v-if="currentNode" class="resource-detail-card">
|
<span v-if="row.node.status === 'DISABLED'" class="ms-off-tag">停用</span>
|
||||||
<header class="detail-heading">
|
|
||||||
<div>
|
|
||||||
<span class="detail-type">{{ typeName(currentNode.resourceType) }}</span>
|
|
||||||
<h2>{{ currentNode.name }}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div v-if="!treeRows.length && !loading" class="sm mut ms-empty">
|
||||||
<el-button
|
暂无菜单资源,请先新增目录
|
||||||
|
</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"
|
:disabled="!canManage || currentNode.protectedNode"
|
||||||
@click="openEdit(currentNode)"
|
@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="
|
:disabled="
|
||||||
!canManage ||
|
!canManage ||
|
||||||
currentNode.protectedNode ||
|
currentNode.protectedNode ||
|
||||||
(currentNode.status === 'ENABLED' && hasProtectedDescendant(currentNode))
|
(currentNode.status === 'ENABLED' && hasProtectedDescendant(currentNode))
|
||||||
"
|
"
|
||||||
@click="changeStatus(currentNode)"
|
@click="changeStatus(currentNode)"
|
||||||
>{{ currentNode.status === 'ENABLED' ? '停用' : '启用' }}</el-button
|
|
||||||
>
|
>
|
||||||
<el-button
|
{{ currentNode.status === 'ENABLED' ? '停用' : '启用' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn dg sm"
|
||||||
:disabled="
|
:disabled="
|
||||||
!canManage ||
|
!canManage ||
|
||||||
currentNode.protectedNode ||
|
currentNode.protectedNode ||
|
||||||
(currentNode.resourceType === 'DIR' && currentNode.children.length > 0) ||
|
(currentNode.resourceType === 'DIR' && currentNode.children.length > 0) ||
|
||||||
hasProtectedDescendant(currentNode)
|
hasProtectedDescendant(currentNode)
|
||||||
"
|
"
|
||||||
type="danger"
|
|
||||||
plain
|
|
||||||
@click="removeResource(currentNode)"
|
@click="removeResource(currentNode)"
|
||||||
>删除</el-button
|
|
||||||
>
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</template>
|
||||||
<dl class="resource-facts">
|
|
||||||
<div>
|
<div v-else class="sm mut ms-empty-detail">
|
||||||
<dt>权限标识</dt>
|
← 请在左侧选择 目录 / 菜单 / 按钮,查看并编辑配置
|
||||||
<dd>{{ currentNode.code ?? '—' }}</dd>
|
</div>
|
||||||
</div>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="editorVisible" :title="editorTitle" width="560px" destroy-on-close>
|
<el-dialog v-model="editorVisible" :title="editorTitle" width="560px" destroy-on-close>
|
||||||
@@ -503,167 +617,283 @@ onMounted(loadTree)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.menu-settings {
|
/* 原型 .card:本分区在原型里就是一张 card(标题 + 两栏工作区) */
|
||||||
min-height: 480px;
|
.card {
|
||||||
|
padding: 16px 18px;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: var(--r);
|
||||||
|
box-shadow: var(--sh);
|
||||||
}
|
}
|
||||||
.menu-toolbar,
|
|
||||||
.detail-heading {
|
/* 原型 .card .ch */
|
||||||
display: flex;
|
.ch {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
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;
|
display: grid;
|
||||||
grid-template-columns: minmax(280px, 0.85fr) minmax(380px, 1.5fr);
|
grid-template-columns: 340px 1fr;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
min-height: 420px;
|
min-height: 470px;
|
||||||
}
|
}
|
||||||
.resource-tree-card,
|
|
||||||
.resource-detail-card {
|
/* 原型 .ms-tree */
|
||||||
min-height: 420px;
|
.ms-tree {
|
||||||
padding: 16px;
|
padding: 10px;
|
||||||
background: #fff;
|
|
||||||
border: 1px solid var(--line);
|
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;
|
display: flex;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
padding: 6px 8px;
|
||||||
min-width: 0;
|
font-size: 13px;
|
||||||
font-size: 12px;
|
border-radius: 7px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.type-icon {
|
|
||||||
display: inline-grid;
|
.ms-node:hover {
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
color: var(--brand);
|
|
||||||
font-size: 10px;
|
|
||||||
place-items: center;
|
|
||||||
background: var(--brand-l);
|
background: var(--brand-l);
|
||||||
border-radius: 5px;
|
|
||||||
}
|
}
|
||||||
.type-button {
|
|
||||||
color: #a06c1c;
|
.ms-node.on {
|
||||||
background: #fff4dd;
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.tree-node small {
|
|
||||||
margin-left: auto;
|
.ms-node .sp {
|
||||||
color: #98a8ae;
|
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;
|
font-size: 10px;
|
||||||
}
|
background: #eef1f3;
|
||||||
.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);
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.sort-actions button:disabled {
|
|
||||||
opacity: 0.45;
|
.ty-dir {
|
||||||
|
color: #0e6e8c;
|
||||||
|
background: #e3f0f4;
|
||||||
}
|
}
|
||||||
.tree-empty,
|
|
||||||
.empty-detail {
|
.ty-menu {
|
||||||
display: grid;
|
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);
|
color: var(--mut);
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
.tree-empty {
|
|
||||||
padding: 50px 0;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@media (max-width: 980px) {
|
|
||||||
.resource-workspace {
|
.mono {
|
||||||
grid-template-columns: 1fr;
|
font-family: Consolas, 'Microsoft YaHei', monospace;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
|
||||||
.menu-toolbar,
|
.ms-actions {
|
||||||
.detail-heading {
|
display: flex;
|
||||||
align-items: flex-start;
|
flex-wrap: wrap;
|
||||||
flex-direction: column;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.resource-facts {
|
|
||||||
|
/* 原型 .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;
|
grid-template-columns: 1fr;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-detail {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-empty-detail {
|
||||||
|
min-height: 180px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user