833 lines
27 KiB
HTML
833 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||
<title>山峡化工 · 产品分类树形展示 Demo</title>
|
||
<link rel="stylesheet" href="shanxia-mp-base.css" />
|
||
<style>
|
||
/* 方案切换条(Demo 专用,非业务 UI) */
|
||
.approach-bar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
background: #fff;
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 10px 12px 8px;
|
||
}
|
||
.approach-bar__title {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 8px;
|
||
}
|
||
.approach-tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
.approach-tab {
|
||
flex: 1;
|
||
padding: 8px 4px;
|
||
border-radius: 10px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: #f3f7f5;
|
||
border: 1px solid transparent;
|
||
line-height: 1.3;
|
||
text-align: center;
|
||
}
|
||
.approach-tab.active {
|
||
color: var(--brand-green-dark);
|
||
background: var(--brand-green-soft);
|
||
border-color: #b7e6d2;
|
||
}
|
||
.approach-tab small {
|
||
display: block;
|
||
font-weight: 400;
|
||
font-size: 10px;
|
||
opacity: 0.85;
|
||
margin-top: 2px;
|
||
}
|
||
.approach-note {
|
||
margin: 10px 12px 0;
|
||
padding: 10px 12px;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
line-height: 1.55;
|
||
}
|
||
.approach-note strong {
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
}
|
||
.approach-note .rec {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 1px 6px;
|
||
border-radius: 999px;
|
||
background: var(--brand-green-soft);
|
||
color: var(--brand-green-dark);
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
}
|
||
.panel { display: none; }
|
||
.panel.active { display: block; }
|
||
|
||
.mp-subpage-back { color: #fff; font-size: 13px; opacity: 0.95; }
|
||
.header-tools {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.header-tools .mp-header__action {
|
||
padding: 0 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* ── 方案 A:缩进展开树 ── */
|
||
.tree-list {
|
||
margin: 12px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
.tree-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-height: 52px;
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--border-light);
|
||
background: #fff;
|
||
}
|
||
.tree-row:last-child { border-bottom: none; }
|
||
.tree-row:active { background: #f7fbf9; }
|
||
.tree-row.is-l2 { background: #fbfdff; }
|
||
.tree-row.is-l3 { background: #fafcfb; }
|
||
.tree-chevron {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
font-size: 10px;
|
||
flex-shrink: 0;
|
||
transition: transform 0.15s ease;
|
||
}
|
||
.tree-chevron.is-leaf {
|
||
visibility: hidden;
|
||
}
|
||
.tree-chevron.is-open {
|
||
transform: rotate(90deg);
|
||
color: var(--brand-green);
|
||
}
|
||
.tree-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.tree-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.tree-meta {
|
||
margin-top: 2px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.tree-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
.icon-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
color: var(--brand-green-dark);
|
||
background: var(--brand-green-soft);
|
||
}
|
||
.icon-btn.more {
|
||
background: #f1f5f3;
|
||
color: var(--text-muted);
|
||
}
|
||
.children-wrap { display: none; }
|
||
.children-wrap.open { display: block; }
|
||
|
||
/* ── 方案 B:逐级下钻 ── */
|
||
.crumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 10px 12px 0;
|
||
flex-wrap: wrap;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
.crumb a {
|
||
color: var(--brand-green-dark);
|
||
font-weight: 500;
|
||
}
|
||
.crumb .sep { opacity: 0.5; }
|
||
.crumb .current { color: var(--text-primary); font-weight: 600; }
|
||
.drill-list {
|
||
margin: 12px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
.drill-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-height: 56px;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--border-light);
|
||
}
|
||
.drill-row:last-child { border-bottom: none; }
|
||
.drill-row:active { background: #f7fbf9; }
|
||
.drill-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 10px;
|
||
background: var(--brand-green-soft);
|
||
color: var(--brand-green-dark);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
.drill-main { flex: 1; min-width: 0; }
|
||
.drill-name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
}
|
||
.drill-sub {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 2px;
|
||
}
|
||
.drill-arrow {
|
||
color: var(--text-placeholder);
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* ── 方案 C:扁平 + 全路径 ── */
|
||
.flat-list {
|
||
margin: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.flat-card {
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 12px 14px;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
.flat-card__head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
.flat-card__title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
}
|
||
.flat-card__path {
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
line-height: 1.4;
|
||
}
|
||
.flat-card__foot {
|
||
margin-top: 10px;
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.filter-chips {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 0 12px;
|
||
margin-top: 10px;
|
||
overflow-x: auto;
|
||
}
|
||
.chip {
|
||
flex-shrink: 0;
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
color: var(--text-muted);
|
||
}
|
||
.chip.active {
|
||
background: var(--brand-green-soft);
|
||
border-color: #b7e6d2;
|
||
color: var(--brand-green-dark);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 操作菜单 Action Sheet */
|
||
.cat-as {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 0;
|
||
transform: translateX(-50%) translateY(100%);
|
||
width: 100%;
|
||
max-width: 420px;
|
||
z-index: 210;
|
||
padding: 0 12px calc(10px + env(safe-area-inset-bottom, 0px));
|
||
transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
|
||
pointer-events: none;
|
||
}
|
||
.cat-as.is-open {
|
||
transform: translateX(-50%) translateY(0);
|
||
pointer-events: auto;
|
||
}
|
||
.cat-as__handle {
|
||
width: 36px; height: 4px; border-radius: 999px;
|
||
background: rgba(255,255,255,0.55); margin: 0 auto 8px;
|
||
}
|
||
.cat-as__head {
|
||
background: #fff; border-radius: 16px 16px 0 0;
|
||
padding: 14px 16px 10px; border-bottom: 1px solid var(--border-light);
|
||
}
|
||
.cat-as__eyebrow { display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
|
||
.cat-as__title { display: block; font-size: 16px; font-weight: 700; color: var(--text-primary); }
|
||
.cat-as__group {
|
||
background: #fff; border-radius: 0 0 16px 16px;
|
||
overflow: hidden; margin-bottom: 8px;
|
||
}
|
||
.cat-as__item {
|
||
display: flex; align-items: center; gap: 12px;
|
||
min-height: 56px; padding: 12px 14px;
|
||
border-bottom: 1px solid var(--border-light);
|
||
width: 100%; text-align: left;
|
||
}
|
||
.cat-as__item:last-child { border-bottom: none; }
|
||
.cat-as__item:active { background: #f7fbf9; }
|
||
.cat-as__icon {
|
||
width: 36px; height: 36px; border-radius: 10px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 13px; font-weight: 700; flex-shrink: 0;
|
||
}
|
||
.cat-as__icon--edit { color: var(--brand-green-dark); background: var(--brand-green-soft); }
|
||
.cat-as__icon--add { color: #087f57; background: #e9f9f2; }
|
||
.cat-as__icon--del { color: var(--danger); background: var(--danger-soft); }
|
||
.cat-as__meta { flex: 1; min-width: 0; }
|
||
.cat-as__name { display: block; font-size: 15px; font-weight: 600; color: var(--text-primary); }
|
||
.cat-as__item--danger .cat-as__name { color: var(--danger); }
|
||
.cat-as__desc { display: block; margin-top: 2px; font-size: 11px; color: var(--text-muted); }
|
||
.cat-as__arrow { color: #c5d5cd; font-size: 18px; }
|
||
.cat-as__cancel {
|
||
width: 100%; min-height: 50px; background: #fff; border-radius: 16px;
|
||
font-size: 15px; font-weight: 600; color: var(--text-muted);
|
||
}
|
||
.cat-as__cancel:active { background: #f3f7f5; }
|
||
|
||
.toast {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 40px;
|
||
transform: translateX(-50%) translateY(20px);
|
||
background: rgba(31, 42, 38, 0.92);
|
||
color: #fff;
|
||
padding: 10px 16px;
|
||
border-radius: 999px;
|
||
font-size: 13px;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: all 0.2s ease;
|
||
z-index: 100;
|
||
white-space: nowrap;
|
||
}
|
||
.toast.show {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav class="proto-nav">
|
||
<strong>Demo 导航</strong>
|
||
<a href="shanxia-mp-index.html">索引</a>
|
||
<a href="shanxia-mp-workbench-demo.html">工作台</a>
|
||
<a href="shanxia-mp-crm-demo.html">CRM列表</a>
|
||
<a class="active" href="shanxia-mp-crm-product-category-demo.html">产品分类树</a>
|
||
</nav>
|
||
|
||
<main class="phone no-tabbar">
|
||
<!-- 方案切换 -->
|
||
<div class="approach-bar">
|
||
<div class="approach-bar__title">移动端树形展示方案对比 · 已采用方案 A(点击切换预览)</div>
|
||
<div class="approach-tabs">
|
||
<button class="approach-tab active" type="button" data-approach="a">
|
||
A 缩进展开
|
||
<small>推荐</small>
|
||
</button>
|
||
<button class="approach-tab" type="button" data-approach="b">
|
||
B 逐级下钻
|
||
<small>目录式</small>
|
||
</button>
|
||
<button class="approach-tab" type="button" data-approach="c">
|
||
C 扁平路径
|
||
<small>搜索友好</small>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 方案 A ── -->
|
||
<section class="panel active" id="panel-a">
|
||
<p class="approach-note">
|
||
<strong>方案 A · 缩进展开树</strong>
|
||
<span class="rec">推荐</span><br />
|
||
对齐 Web 端树形表格:一屏看全层级,点击箭头展开/折叠;右侧「+」新增子分类、「⋯」编辑/删除。适合管理端 CRUD。
|
||
</p>
|
||
|
||
<header class="mp-header">
|
||
<div class="mp-header__row">
|
||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||
<div class="header-tools">
|
||
<button class="mp-header__action" type="button" id="btn-expand-all">全部展开</button>
|
||
<button class="mp-header__action" type="button" data-toast="打开新增分类表单">+ 新增</button>
|
||
</div>
|
||
</div>
|
||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||
<div class="mp-header__search" style="margin-top:10px">
|
||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||
</div>
|
||
</header>
|
||
|
||
<div class="filter-chips">
|
||
<button class="chip active" type="button">全部</button>
|
||
<button class="chip" type="button">启用</button>
|
||
<button class="chip" type="button">停用</button>
|
||
</div>
|
||
|
||
<div class="tree-list" id="tree-a"></div>
|
||
</section>
|
||
|
||
<!-- ── 方案 B ── -->
|
||
<section class="panel" id="panel-b">
|
||
<p class="approach-note">
|
||
<strong>方案 B · 逐级下钻</strong><br />
|
||
类似文件管理器:每次只看当前层;面包屑回退。交互简单、层级深时更清晰,但无法一屏总览整棵树,新增子分类需先进入父级。
|
||
</p>
|
||
|
||
<header class="mp-header">
|
||
<div class="mp-header__row">
|
||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||
<button class="mp-header__action" type="button" data-toast="在当前层新增分类">+ 新增</button>
|
||
</div>
|
||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||
<div class="mp-header__search" style="margin-top:10px">
|
||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||
</div>
|
||
</header>
|
||
|
||
<nav class="crumb" id="drill-crumb"></nav>
|
||
<div class="drill-list" id="drill-list"></div>
|
||
</section>
|
||
|
||
<!-- ── 方案 C ── -->
|
||
<section class="panel" id="panel-c">
|
||
<p class="approach-note">
|
||
<strong>方案 C · 扁平列表 + 全路径</strong><br />
|
||
把树拍平为卡片列表,用「父 / 子 / 孙」全路径展示归属(分隔符 <code> / </code>)。搜索命中直观,但丢失展开折叠的树感,深层级可读性一般。
|
||
</p>
|
||
|
||
<header class="mp-header">
|
||
<div class="mp-header__row">
|
||
<a class="mp-subpage-back" href="shanxia-mp-workbench-demo.html">← 工作台</a>
|
||
<button class="mp-header__action" type="button" data-toast="打开新增分类表单">+ 新增</button>
|
||
</div>
|
||
<h1 class="mp-header__title" style="margin-top:8px">产品分类</h1>
|
||
<div class="mp-header__search" style="margin-top:10px">
|
||
<span class="mp-header__search-dot"></span>搜索分类名称
|
||
</div>
|
||
</header>
|
||
|
||
<div class="filter-chips">
|
||
<button class="chip active" type="button">全部</button>
|
||
<button class="chip" type="button">启用</button>
|
||
<button class="chip" type="button">停用</button>
|
||
</div>
|
||
|
||
<div class="flat-list" id="flat-list"></div>
|
||
</section>
|
||
</main>
|
||
|
||
<!-- 遮罩 + 操作 Action Sheet -->
|
||
<div class="mp-overlay" id="mask"></div>
|
||
<div class="cat-as" id="action-sheet">
|
||
<div class="cat-as__handle"></div>
|
||
<div class="cat-as__head">
|
||
<span class="cat-as__eyebrow">操作分类</span>
|
||
<span class="cat-as__title" id="sheet-title">分类名称</span>
|
||
</div>
|
||
<div class="cat-as__group">
|
||
<button class="cat-as__item" type="button" data-toast="编辑分类">
|
||
<span class="cat-as__icon cat-as__icon--edit">编</span>
|
||
<span class="cat-as__meta">
|
||
<span class="cat-as__name">编辑</span>
|
||
<span class="cat-as__desc">修改名称、上级、状态等</span>
|
||
</span>
|
||
<span class="cat-as__arrow">›</span>
|
||
</button>
|
||
<button class="cat-as__item" type="button" data-toast="新增子分类">
|
||
<span class="cat-as__icon cat-as__icon--add">子</span>
|
||
<span class="cat-as__meta">
|
||
<span class="cat-as__name">新增子分类</span>
|
||
<span class="cat-as__desc">在此分类下创建下级</span>
|
||
</span>
|
||
<span class="cat-as__arrow">›</span>
|
||
</button>
|
||
<button class="cat-as__item cat-as__item--danger" type="button" data-toast="删除(需二次确认)">
|
||
<span class="cat-as__icon cat-as__icon--del">删</span>
|
||
<span class="cat-as__meta">
|
||
<span class="cat-as__name">删除</span>
|
||
<span class="cat-as__desc">有子分类或关联产品时不可删</span>
|
||
</span>
|
||
<span class="cat-as__arrow">›</span>
|
||
</button>
|
||
</div>
|
||
<button class="cat-as__cancel" type="button" id="sheet-close">取消</button>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
/**
|
||
* 模拟数据:对齐 Web CRM 产品分类树结构
|
||
* 字段:id / parentId / categoryName / status / orderNum / children
|
||
*/
|
||
const TREE = [
|
||
{
|
||
id: '1', parentId: '0', categoryName: '化工原料', status: '0', orderNum: 1,
|
||
children: [
|
||
{
|
||
id: '11', parentId: '1', categoryName: '有机溶剂', status: '0', orderNum: 1,
|
||
children: [
|
||
{ id: '111', parentId: '11', categoryName: '醇类', status: '0', orderNum: 1, children: [] },
|
||
{ id: '112', parentId: '11', categoryName: '酮类', status: '0', orderNum: 2, children: [] }
|
||
]
|
||
},
|
||
{
|
||
id: '12', parentId: '1', categoryName: '无机盐', status: '0', orderNum: 2,
|
||
children: [
|
||
{ id: '121', parentId: '12', categoryName: '硫酸盐', status: '1', orderNum: 1, children: [] }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: '2', parentId: '0', categoryName: '精细化学品', status: '0', orderNum: 2,
|
||
children: [
|
||
{
|
||
id: '21', parentId: '2', categoryName: '催化剂', status: '0', orderNum: 1,
|
||
children: []
|
||
},
|
||
{
|
||
id: '22', parentId: '2', categoryName: '助剂', status: '0', orderNum: 2,
|
||
children: [
|
||
{ id: '221', parentId: '22', categoryName: '分散剂', status: '0', orderNum: 1, children: [] },
|
||
{ id: '222', parentId: '22', categoryName: '消泡剂', status: '0', orderNum: 2, children: [] }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: '3', parentId: '0', categoryName: '包装耗材', status: '1', orderNum: 3,
|
||
children: []
|
||
}
|
||
]
|
||
|
||
const statusLabel = (s) => (String(s) === '0' ? '正常' : '停用')
|
||
const statusTag = (s) =>
|
||
String(s) === '0'
|
||
? '<span class="mp-tag mp-tag--success">正常</span>'
|
||
: '<span class="mp-tag mp-tag--muted">停用</span>'
|
||
|
||
/* ── Toast / Sheet ── */
|
||
const toastEl = document.getElementById('toast')
|
||
function showToast(msg) {
|
||
toastEl.textContent = msg
|
||
toastEl.classList.add('show')
|
||
clearTimeout(showToast._t)
|
||
showToast._t = setTimeout(() => toastEl.classList.remove('show'), 1600)
|
||
}
|
||
|
||
const mask = document.getElementById('mask')
|
||
const sheet = document.getElementById('action-sheet')
|
||
function openSheet(name) {
|
||
document.getElementById('sheet-title').textContent = name
|
||
mask.classList.add('is-open')
|
||
sheet.classList.add('is-open')
|
||
}
|
||
function closeSheet() {
|
||
mask.classList.remove('is-open')
|
||
sheet.classList.remove('is-open')
|
||
}
|
||
document.getElementById('sheet-close').onclick = closeSheet
|
||
mask.onclick = closeSheet
|
||
document.querySelectorAll('[data-toast]').forEach((el) => {
|
||
el.addEventListener('click', () => {
|
||
showToast(el.getAttribute('data-toast'))
|
||
if (el.closest('.cat-as')) closeSheet()
|
||
})
|
||
})
|
||
|
||
/* ── 方案切换 ── */
|
||
document.querySelectorAll('.approach-tab').forEach((tab) => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.approach-tab').forEach((t) => t.classList.remove('active'))
|
||
document.querySelectorAll('.panel').forEach((p) => p.classList.remove('active'))
|
||
tab.classList.add('active')
|
||
document.getElementById('panel-' + tab.dataset.approach).classList.add('active')
|
||
})
|
||
})
|
||
|
||
document.querySelectorAll('.filter-chips .chip').forEach((chip) => {
|
||
chip.addEventListener('click', () => {
|
||
const group = chip.parentElement
|
||
group.querySelectorAll('.chip').forEach((c) => c.classList.remove('active'))
|
||
chip.classList.add('active')
|
||
showToast('筛选:' + chip.textContent.trim() + '(Demo 仅示意)')
|
||
})
|
||
})
|
||
|
||
/* ── 方案 A:缩进展开树 ── */
|
||
const expanded = new Set(['1', '11', '2']) // 默认展开部分节点
|
||
|
||
function childCount(node) {
|
||
return (node.children || []).length
|
||
}
|
||
|
||
function renderTreeA() {
|
||
const root = document.getElementById('tree-a')
|
||
root.innerHTML = ''
|
||
|
||
function renderNodes(nodes, level, container) {
|
||
nodes.forEach((node) => {
|
||
const hasKids = childCount(node) > 0
|
||
const isOpen = expanded.has(node.id)
|
||
const pad = 12 + level * 18
|
||
|
||
const row = document.createElement('div')
|
||
row.className = 'tree-row' + (level === 1 ? ' is-l2' : level >= 2 ? ' is-l3' : '')
|
||
row.style.paddingLeft = pad + 'px'
|
||
row.innerHTML = `
|
||
<button class="tree-chevron ${hasKids ? '' : 'is-leaf'} ${isOpen ? 'is-open' : ''}" type="button" data-toggle="${node.id}" aria-label="展开折叠">▶</button>
|
||
<div class="tree-main">
|
||
<div class="tree-name">${node.categoryName}</div>
|
||
<div class="tree-meta">
|
||
${statusTag(node.status)}
|
||
<span>排序 ${node.orderNum}</span>
|
||
${hasKids ? `<span>${childCount(node)} 个子类</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="tree-actions">
|
||
<button class="icon-btn" type="button" data-toast="新增子分类:${node.categoryName}">+</button>
|
||
<button class="icon-btn more" type="button" data-more="${node.categoryName}">⋯</button>
|
||
</div>
|
||
`
|
||
container.appendChild(row)
|
||
|
||
if (hasKids) {
|
||
const wrap = document.createElement('div')
|
||
wrap.className = 'children-wrap' + (isOpen ? ' open' : '')
|
||
wrap.dataset.wrap = node.id
|
||
renderNodes(node.children, level + 1, wrap)
|
||
container.appendChild(wrap)
|
||
}
|
||
})
|
||
}
|
||
renderNodes(TREE, 0, root)
|
||
|
||
root.querySelectorAll('[data-toggle]').forEach((btn) => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation()
|
||
const id = btn.getAttribute('data-toggle')
|
||
if (expanded.has(id)) expanded.delete(id)
|
||
else expanded.add(id)
|
||
renderTreeA()
|
||
})
|
||
})
|
||
root.querySelectorAll('[data-toast]').forEach((btn) => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation()
|
||
showToast(btn.getAttribute('data-toast'))
|
||
})
|
||
})
|
||
root.querySelectorAll('[data-more]').forEach((btn) => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation()
|
||
openSheet(btn.getAttribute('data-more'))
|
||
})
|
||
})
|
||
}
|
||
|
||
function collectIds(nodes, set) {
|
||
nodes.forEach((n) => {
|
||
if (childCount(n)) {
|
||
set.add(n.id)
|
||
collectIds(n.children, set)
|
||
}
|
||
})
|
||
}
|
||
|
||
let allExpanded = false
|
||
document.getElementById('btn-expand-all').addEventListener('click', () => {
|
||
allExpanded = !allExpanded
|
||
expanded.clear()
|
||
if (allExpanded) collectIds(TREE, expanded)
|
||
document.getElementById('btn-expand-all').textContent = allExpanded ? '全部折叠' : '全部展开'
|
||
renderTreeA()
|
||
})
|
||
|
||
/* ── 方案 B:逐级下钻 ── */
|
||
let drillStack = [{ id: '0', categoryName: '全部', children: TREE }]
|
||
|
||
function currentDrillNode() {
|
||
return drillStack[drillStack.length - 1]
|
||
}
|
||
|
||
function renderDrill() {
|
||
const node = currentDrillNode()
|
||
const crumb = document.getElementById('drill-crumb')
|
||
crumb.innerHTML = drillStack
|
||
.map((n, i) => {
|
||
if (i === drillStack.length - 1) {
|
||
return `<span class="current">${n.categoryName}</span>`
|
||
}
|
||
return `<a href="#" data-crumb="${i}">${n.categoryName}</a><span class="sep">/</span>`
|
||
})
|
||
.join('')
|
||
|
||
crumb.querySelectorAll('[data-crumb]').forEach((a) => {
|
||
a.addEventListener('click', (e) => {
|
||
e.preventDefault()
|
||
const idx = Number(a.getAttribute('data-crumb'))
|
||
drillStack = drillStack.slice(0, idx + 1)
|
||
renderDrill()
|
||
})
|
||
})
|
||
|
||
const list = document.getElementById('drill-list')
|
||
const kids = node.children || []
|
||
if (!kids.length) {
|
||
list.innerHTML = `<div class="drill-row"><div class="drill-main"><div class="drill-name">暂无子分类</div><div class="drill-sub">可点击右上角新增</div></div></div>`
|
||
return
|
||
}
|
||
|
||
list.innerHTML = kids
|
||
.map((n) => {
|
||
const hasKids = childCount(n) > 0
|
||
return `
|
||
<div class="drill-row" data-enter="${n.id}">
|
||
<div class="drill-icon">${n.categoryName.slice(0, 1)}</div>
|
||
<div class="drill-main">
|
||
<div class="drill-name">${n.categoryName}</div>
|
||
<div class="drill-sub">${statusLabel(n.status)} · 排序 ${n.orderNum}${hasKids ? ' · ' + childCount(n) + ' 个子类' : ' · 末级'}</div>
|
||
</div>
|
||
<button class="icon-btn more" type="button" data-more="${n.categoryName}">⋯</button>
|
||
${hasKids ? '<span class="drill-arrow">›</span>' : '<span class="drill-arrow" style="opacity:0">›</span>'}
|
||
</div>
|
||
`
|
||
})
|
||
.join('')
|
||
|
||
list.querySelectorAll('[data-enter]').forEach((row) => {
|
||
row.addEventListener('click', (e) => {
|
||
if (e.target.closest('[data-more]')) return
|
||
const id = row.getAttribute('data-enter')
|
||
const next = kids.find((k) => k.id === id)
|
||
if (next && childCount(next)) {
|
||
drillStack.push(next)
|
||
renderDrill()
|
||
} else if (next) {
|
||
openSheet(next.categoryName)
|
||
}
|
||
})
|
||
})
|
||
list.querySelectorAll('[data-more]').forEach((btn) => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation()
|
||
openSheet(btn.getAttribute('data-more'))
|
||
})
|
||
})
|
||
}
|
||
|
||
/* ── 方案 C:扁平 + 全路径 ── */
|
||
function flattenWithPath(nodes, pathParts, out) {
|
||
nodes.forEach((n) => {
|
||
const path = [...pathParts, n.categoryName]
|
||
out.push({ ...n, pathLabel: path.join(' / ') })
|
||
if (n.children?.length) flattenWithPath(n.children, path, out)
|
||
})
|
||
return out
|
||
}
|
||
|
||
function renderFlat() {
|
||
const items = flattenWithPath(TREE, [], [])
|
||
const list = document.getElementById('flat-list')
|
||
list.innerHTML = items
|
||
.map(
|
||
(n) => `
|
||
<article class="flat-card">
|
||
<div class="flat-card__head">
|
||
<div>
|
||
<div class="flat-card__title">${n.categoryName}</div>
|
||
<div class="flat-card__path">${n.pathLabel}</div>
|
||
</div>
|
||
${statusTag(n.status)}
|
||
</div>
|
||
<div class="flat-card__foot">
|
||
<button class="mp-btn mp-btn--ghost" type="button" data-toast="新增子分类:${n.categoryName}">子分类</button>
|
||
<button class="mp-btn mp-btn--ghost" type="button" data-more="${n.categoryName}">更多</button>
|
||
<button class="mp-btn mp-btn--primary" type="button" data-toast="编辑:${n.categoryName}">编辑</button>
|
||
</div>
|
||
</article>
|
||
`
|
||
)
|
||
.join('')
|
||
|
||
list.querySelectorAll('[data-toast]').forEach((btn) => {
|
||
btn.addEventListener('click', () => showToast(btn.getAttribute('data-toast')))
|
||
})
|
||
list.querySelectorAll('[data-more]').forEach((btn) => {
|
||
btn.addEventListener('click', () => openSheet(btn.getAttribute('data-more')))
|
||
})
|
||
}
|
||
|
||
renderTreeA()
|
||
renderDrill()
|
||
renderFlat()
|
||
</script>
|
||
</body>
|
||
</html>
|