|
|
@@ -0,0 +1,828 @@
|
|
|
+<!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>
|
|
|
+ <style>
|
|
|
+ :root {
|
|
|
+ --brand: #0a9f6d;
|
|
|
+ --brand-dark: #087f57;
|
|
|
+ --brand-soft: #e9f9f2;
|
|
|
+ --bg: #f5f8f7;
|
|
|
+ --surface: #fff;
|
|
|
+ --text: #1f2a26;
|
|
|
+ --muted: #6b7b74;
|
|
|
+ --placeholder: #9aa8a1;
|
|
|
+ --border: #dcebe4;
|
|
|
+ --border-light: #edf5f1;
|
|
|
+ --danger: #b91c1c;
|
|
|
+ --danger-soft: #fef2f2;
|
|
|
+ --header: linear-gradient(180deg, #087f57 0%, #087f57 28%, #0a9f6d 78%, #0a9f6d 100%);
|
|
|
+ --shadow-sm: 0 4px 12px rgba(10, 159, 109, 0.06);
|
|
|
+ }
|
|
|
+ * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+ button, input { font: inherit; border: none; background: none; cursor: pointer; }
|
|
|
+ body {
|
|
|
+ background: #cfd9d4;
|
|
|
+ color: var(--text);
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.45;
|
|
|
+ min-height: 100vh;
|
|
|
+ }
|
|
|
+ .stage { max-width: 440px; margin: 0 auto; padding: 14px 12px 40px; }
|
|
|
+ .banner {
|
|
|
+ background: #1f2a26;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.55;
|
|
|
+ }
|
|
|
+ .banner strong { color: #9fe3c4; }
|
|
|
+ .banner li { margin: 4px 0 0 1.1em; }
|
|
|
+ .tabs {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr 1fr;
|
|
|
+ gap: 6px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+ .tab {
|
|
|
+ padding: 10px 6px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #b7c9c0;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--muted);
|
|
|
+ line-height: 1.3;
|
|
|
+ }
|
|
|
+ .tab small {
|
|
|
+ display: block;
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+ .tab.active {
|
|
|
+ background: var(--brand-soft);
|
|
|
+ border-color: #b7ebce;
|
|
|
+ color: var(--brand-dark);
|
|
|
+ }
|
|
|
+ .note {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--muted);
|
|
|
+ line-height: 1.55;
|
|
|
+ }
|
|
|
+ .note strong { color: var(--text); }
|
|
|
+ .note .rec {
|
|
|
+ display: inline-block;
|
|
|
+ margin-left: 6px;
|
|
|
+ padding: 1px 7px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: var(--brand-soft);
|
|
|
+ color: var(--brand-dark);
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+ .panel { display: none; }
|
|
|
+ .panel.active { display: block; }
|
|
|
+ .phone {
|
|
|
+ background: var(--bg);
|
|
|
+ border-radius: 28px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 8px solid #24312c;
|
|
|
+ height: 720px;
|
|
|
+ box-shadow: 0 18px 40px rgba(0,0,0,.16);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+ .status-bar {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ height: 28px;
|
|
|
+ background: var(--brand-dark);
|
|
|
+ color: rgba(255,255,255,.85);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 0 16px;
|
|
|
+ font-size: 11px;
|
|
|
+ }
|
|
|
+ .header {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ background: var(--header);
|
|
|
+ color: #fff;
|
|
|
+ padding: 8px 12px 14px;
|
|
|
+ border-radius: 0 0 22px 22px;
|
|
|
+ box-shadow: 0 10px 18px -10px rgba(8, 127, 87, 0.35);
|
|
|
+ }
|
|
|
+ .header-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ min-height: 44px;
|
|
|
+ }
|
|
|
+ .back {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 13px;
|
|
|
+ opacity: 0.95;
|
|
|
+ padding: 6px 4px;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .header h1 {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: 700;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .hdr-btn {
|
|
|
+ height: 32px;
|
|
|
+ padding: 0 12px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #fff;
|
|
|
+ background: rgb(255 255 255 / 15%);
|
|
|
+ border: 1px solid rgb(255 255 255 / 75%);
|
|
|
+ border-radius: 8px;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .hdr-btn:active { opacity: 0.8; }
|
|
|
+ .scroll {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+ padding-bottom: 20px;
|
|
|
+ }
|
|
|
+ .tag {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 20px;
|
|
|
+ padding: 1px 7px;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ }
|
|
|
+ .tag--ok {
|
|
|
+ color: var(--brand-dark);
|
|
|
+ background: var(--brand-soft);
|
|
|
+ border-color: #b7ebce;
|
|
|
+ }
|
|
|
+ .tag--off {
|
|
|
+ color: var(--danger);
|
|
|
+ background: var(--danger-soft);
|
|
|
+ border-color: #fecaca;
|
|
|
+ }
|
|
|
+ .tag--lv {
|
|
|
+ color: var(--muted);
|
|
|
+ background: #f1f5f3;
|
|
|
+ border-color: var(--border);
|
|
|
+ }
|
|
|
+ .icon-more {
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: var(--brand-soft);
|
|
|
+ color: var(--brand-dark);
|
|
|
+ font-size: 18px;
|
|
|
+ line-height: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .icon-add {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: var(--brand-soft);
|
|
|
+ color: var(--brand-dark);
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .toast {
|
|
|
+ position: fixed;
|
|
|
+ left: 50%;
|
|
|
+ bottom: 36px;
|
|
|
+ transform: translateX(-50%) translateY(16px);
|
|
|
+ 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: 50;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .toast.show {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateX(-50%) translateY(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ── A:缩进展开树(整卡一体) ── */
|
|
|
+ .tree-box {
|
|
|
+ margin: 12px 10px 0;
|
|
|
+ background: var(--surface);
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 16px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ .tree-tools {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 10px 12px 0;
|
|
|
+ }
|
|
|
+ .ghost-btn {
|
|
|
+ height: 30px;
|
|
|
+ padding: 0 10px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ background: #fff;
|
|
|
+ color: var(--muted);
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ .ghost-btn:active { background: #f7fbf9; }
|
|
|
+ .tree-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ min-height: 54px;
|
|
|
+ padding: 10px 10px 10px 0;
|
|
|
+ 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; }
|
|
|
+ .chevron {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ border-radius: 6px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: var(--brand);
|
|
|
+ font-size: 11px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ transition: transform 0.15s ease;
|
|
|
+ }
|
|
|
+ .chevron.is-open { transform: rotate(90deg); }
|
|
|
+ .chevron.is-leaf { visibility: hidden; }
|
|
|
+ .tree-main { flex: 1; min-width: 0; }
|
|
|
+ .tree-name {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+ .tree-meta {
|
|
|
+ margin-top: 3px;
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
+ .guide {
|
|
|
+ width: 12px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ align-self: stretch;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+ .kids { display: none; }
|
|
|
+ .kids.open { display: block; }
|
|
|
+
|
|
|
+ /* ── B:逐级下钻 ── */
|
|
|
+ .crumb {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 12px 12px 0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
+ .crumb a { color: var(--brand-dark); font-weight: 600; }
|
|
|
+ .crumb .sep { opacity: 0.45; }
|
|
|
+ .crumb .cur { color: var(--text); font-weight: 700; }
|
|
|
+ .drill-list {
|
|
|
+ margin: 12px 10px 0;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 16px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ .drill-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ min-height: 58px;
|
|
|
+ padding: 12px 12px;
|
|
|
+ border-bottom: 1px solid var(--border-light);
|
|
|
+ }
|
|
|
+ .drill-row:last-child { border-bottom: none; }
|
|
|
+ .drill-row:active { background: #f7fbf9; }
|
|
|
+ .drill-icon {
|
|
|
+ width: 38px;
|
|
|
+ height: 38px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: var(--brand-soft);
|
|
|
+ color: var(--brand-dark);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 700;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .drill-main { flex: 1; min-width: 0; }
|
|
|
+ .drill-name { font-size: 15px; font-weight: 600; }
|
|
|
+ .drill-sub {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
+ .drill-arrow { color: var(--placeholder); font-size: 18px; }
|
|
|
+
|
|
|
+ /* ── C:层级卡片 + 全路径 ── */
|
|
|
+ .card-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 12px 10px 0;
|
|
|
+ }
|
|
|
+ .cat-card {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 14px;
|
|
|
+ padding: 12px 12px 10px;
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
+ }
|
|
|
+ .cat-card:active { background: #f9fcfa; }
|
|
|
+ .cat-card__top {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ .cat-card__body { flex: 1; min-width: 0; }
|
|
|
+ .cat-card__title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ .cat-card__title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+ .cat-card__path {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ line-height: 1.4;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+ .cat-card__foot {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid var(--border-light);
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ .cat-card__meta {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
+ .cat-card__actions { display: flex; gap: 6px; }
|
|
|
+ .mini-btn {
|
|
|
+ height: 30px;
|
|
|
+ padding: 0 10px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ background: #fff;
|
|
|
+ color: var(--text);
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ .mini-btn.primary {
|
|
|
+ background: var(--brand);
|
|
|
+ border-color: var(--brand);
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ .indent-mark {
|
|
|
+ width: 3px;
|
|
|
+ align-self: stretch;
|
|
|
+ border-radius: 999px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-top: 2px;
|
|
|
+ margin-bottom: 2px;
|
|
|
+ }
|
|
|
+ .indent-mark.l1 { background: var(--brand); }
|
|
|
+ .indent-mark.l2 { background: #7ec9ad; }
|
|
|
+ .indent-mark.l3 { background: #c5e6d7; }
|
|
|
+
|
|
|
+ .choice-hint {
|
|
|
+ margin-top: 14px;
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px dashed #b7c9c0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--muted);
|
|
|
+ line-height: 1.55;
|
|
|
+ }
|
|
|
+ .choice-hint strong { color: var(--text); }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="stage">
|
|
|
+ <div class="banner">
|
|
|
+ <strong>产品分类 · 三方案对比</strong>(对齐小程序产品管理 webview)
|
|
|
+ <ul>
|
|
|
+ <li>数据为父子孙三级示例:化工原料 → 有机溶剂 → 醇类</li>
|
|
|
+ <li>顶部 Tab 切换方案;手机框内可点展开 / 下钻 / 操作按钮</li>
|
|
|
+ <li>选好后在对话里回复 A / B / C 即可</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tabs" role="tablist">
|
|
|
+ <button class="tab active" type="button" data-panel="a">
|
|
|
+ A 缩进展开
|
|
|
+ <small>推荐 · 总览强</small>
|
|
|
+ </button>
|
|
|
+ <button class="tab" type="button" data-panel="b">
|
|
|
+ B 逐级下钻
|
|
|
+ <small>目录式</small>
|
|
|
+ </button>
|
|
|
+ <button class="tab" type="button" data-panel="c">
|
|
|
+ C 层级卡片
|
|
|
+ <small>对齐产品卡</small>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 方案 A -->
|
|
|
+ <section class="panel active" id="panel-a">
|
|
|
+ <p class="note">
|
|
|
+ <strong>A · 缩进展开树</strong><span class="rec">推荐</span><br />
|
|
|
+ 整卡一体列表:箭头展开/折叠,左侧缩进表达父子孙;「+」加子类、「⋯」编辑/删除。一屏看清整棵树,适合管理 CRUD。当前线上页接近本方案,但行是分散小卡;本 Demo 收成一体树表。
|
|
|
+ </p>
|
|
|
+ <div class="phone">
|
|
|
+ <div class="status-bar"><span>9:41</span><span>▍</span></div>
|
|
|
+ <header class="header">
|
|
|
+ <div class="header-row">
|
|
|
+ <button class="back" type="button" data-toast="返回产品列表">← 返回产品列表</button>
|
|
|
+ <h1>产品分类</h1>
|
|
|
+ <button class="hdr-btn" type="button" data-toast="新增顶级分类">新增</button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+ <div class="scroll">
|
|
|
+ <div class="tree-tools">
|
|
|
+ <button class="ghost-btn" type="button" id="btn-expand-all">全部展开</button>
|
|
|
+ </div>
|
|
|
+ <div class="tree-box" id="tree-a"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- 方案 B -->
|
|
|
+ <section class="panel" id="panel-b">
|
|
|
+ <p class="note">
|
|
|
+ <strong>B · 逐级下钻</strong><br />
|
|
|
+ 类似文件管理器:每次只显示当前层;面包屑回退。深层分类更清晰,但无法一屏总览整树,新增子类需先进入父级。
|
|
|
+ </p>
|
|
|
+ <div class="phone">
|
|
|
+ <div class="status-bar"><span>9:41</span><span>📶</span></div>
|
|
|
+ <header class="header">
|
|
|
+ <div class="header-row">
|
|
|
+ <button class="back" type="button" data-toast="返回产品列表">← 返回产品列表</button>
|
|
|
+ <h1>产品分类</h1>
|
|
|
+ <button class="hdr-btn" type="button" data-toast="在当前层新增">新增</button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+ <div class="scroll">
|
|
|
+ <nav class="crumb" id="drill-crumb"></nav>
|
|
|
+ <div class="drill-list" id="drill-list"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- 方案 C -->
|
|
|
+ <section class="panel" id="panel-c">
|
|
|
+ <p class="note">
|
|
|
+ <strong>C · 层级卡片 + 全路径</strong><br />
|
|
|
+ 与近期产品/客户列表卡视觉对齐:每条独立卡片,左侧色条表示层级,副文案用「父 / 子 / 孙」全路径。搜索友好、风格统一,但树感弱、列表更长。
|
|
|
+ </p>
|
|
|
+ <div class="phone">
|
|
|
+ <div class="status-bar"><span>9:41</span><span>📶</span></div>
|
|
|
+ <header class="header">
|
|
|
+ <div class="header-row">
|
|
|
+ <button class="back" type="button" data-toast="返回产品列表">← 返回产品列表</button>
|
|
|
+ <h1>产品分类</h1>
|
|
|
+ <button class="hdr-btn" type="button" data-toast="新增顶级分类">新增</button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+ <div class="scroll">
|
|
|
+ <div class="card-list" id="flat-list"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="choice-hint">
|
|
|
+ <strong>怎么选?</strong><br />
|
|
|
+ · 分类数量不多、要一眼看层级 → <strong>A</strong><br />
|
|
|
+ · 层级很深、只关心当前层 → <strong>B</strong><br />
|
|
|
+ · 希望和产品列表卡同一套视觉语言 → <strong>C</strong>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="toast" id="toast"></div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ 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 toastEl = document.getElementById('toast')
|
|
|
+ function showToast(msg) {
|
|
|
+ toastEl.textContent = msg
|
|
|
+ toastEl.classList.add('show')
|
|
|
+ clearTimeout(showToast._t)
|
|
|
+ showToast._t = setTimeout(() => toastEl.classList.remove('show'), 1500)
|
|
|
+ }
|
|
|
+
|
|
|
+ document.querySelectorAll('[data-toast]').forEach((el) => {
|
|
|
+ el.addEventListener('click', () => showToast(el.getAttribute('data-toast')))
|
|
|
+ })
|
|
|
+
|
|
|
+ document.querySelectorAll('.tab').forEach((tab) => {
|
|
|
+ tab.addEventListener('click', () => {
|
|
|
+ document.querySelectorAll('.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.panel).classList.add('active')
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ const statusTag = (s) =>
|
|
|
+ String(s) === '0'
|
|
|
+ ? '<span class="tag tag--ok">正常</span>'
|
|
|
+ : '<span class="tag tag--off">停用</span>'
|
|
|
+
|
|
|
+ function childCount(node) {
|
|
|
+ return (node.children || []).length
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ── A ── */
|
|
|
+ const expanded = new Set(['1', '11', '2'])
|
|
|
+
|
|
|
+ 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 = 10 + level * 16
|
|
|
+
|
|
|
+ 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="chevron ${hasKids ? '' : 'is-leaf'} ${isOpen ? 'is-open' : ''}" type="button" data-toggle="${node.id}">▶</button>
|
|
|
+ <div class="tree-main" data-more="${node.categoryName}">
|
|
|
+ <div class="tree-name">${node.categoryName}</div>
|
|
|
+ <div class="tree-meta">
|
|
|
+ ${statusTag(node.status)}
|
|
|
+ <span>排序 ${node.orderNum}</span>
|
|
|
+ ${hasKids ? `<span>${childCount(node)} 个子类</span>` : '<span>末级</span>'}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button class="icon-add" type="button" data-toast="新增子分类:${node.categoryName}">+</button>
|
|
|
+ <button class="icon-more" type="button" data-toast="操作:${node.categoryName}">⋯</button>
|
|
|
+ `
|
|
|
+ container.appendChild(row)
|
|
|
+
|
|
|
+ if (hasKids) {
|
|
|
+ const wrap = document.createElement('div')
|
|
|
+ wrap.className = 'kids' + (isOpen ? ' open' : '')
|
|
|
+ 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((el) => {
|
|
|
+ el.addEventListener('click', () => showToast('操作:' + el.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 renderDrill() {
|
|
|
+ const node = drillStack[drillStack.length - 1]
|
|
|
+ const crumb = document.getElementById('drill-crumb')
|
|
|
+ crumb.innerHTML = drillStack
|
|
|
+ .map((n, i) => {
|
|
|
+ if (i === drillStack.length - 1) return `<span class="cur">${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()
|
|
|
+ drillStack = drillStack.slice(0, Number(a.getAttribute('data-crumb')) + 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">${String(n.status) === '0' ? '正常' : '停用'} · 排序 ${n.orderNum}${hasKids ? ' · ' + childCount(n) + ' 个子类' : ' · 末级'}</div>
|
|
|
+ </div>
|
|
|
+ <button class="icon-more" type="button" data-toast="操作:${n.categoryName}">⋯</button>
|
|
|
+ <span class="drill-arrow" style="${hasKids ? '' : 'opacity:0'}">›</span>
|
|
|
+ </div>
|
|
|
+ `
|
|
|
+ })
|
|
|
+ .join('')
|
|
|
+
|
|
|
+ list.querySelectorAll('[data-enter]').forEach((row) => {
|
|
|
+ row.addEventListener('click', (e) => {
|
|
|
+ if (e.target.closest('[data-toast]')) 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) {
|
|
|
+ showToast('末级:' + next.categoryName)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ list.querySelectorAll('[data-toast]').forEach((btn) => {
|
|
|
+ btn.addEventListener('click', (e) => {
|
|
|
+ e.stopPropagation()
|
|
|
+ showToast(btn.getAttribute('data-toast'))
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ── C ── */
|
|
|
+ function flattenWithPath(nodes, pathParts, depth, out) {
|
|
|
+ nodes.forEach((n) => {
|
|
|
+ const path = [...pathParts, n.categoryName]
|
|
|
+ out.push({ ...n, pathLabel: path.join(' / '), depth })
|
|
|
+ if (n.children?.length) flattenWithPath(n.children, path, depth + 1, out)
|
|
|
+ })
|
|
|
+ return out
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderFlat() {
|
|
|
+ const items = flattenWithPath(TREE, [], 1, [])
|
|
|
+ const list = document.getElementById('flat-list')
|
|
|
+ list.innerHTML = items
|
|
|
+ .map((n) => {
|
|
|
+ const lv = Math.min(n.depth, 3)
|
|
|
+ return `
|
|
|
+ <article class="cat-card">
|
|
|
+ <div class="cat-card__top">
|
|
|
+ <span class="indent-mark l${lv}"></span>
|
|
|
+ <div class="cat-card__body">
|
|
|
+ <div class="cat-card__title-row">
|
|
|
+ <span class="cat-card__title">${n.categoryName}</span>
|
|
|
+ <span class="tag tag--lv">L${lv}</span>
|
|
|
+ ${statusTag(n.status)}
|
|
|
+ </div>
|
|
|
+ <div class="cat-card__path">${n.pathLabel}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="cat-card__foot">
|
|
|
+ <span class="cat-card__meta">排序 ${n.orderNum}${childCount(n) ? ' · ' + childCount(n) + ' 个子类' : ' · 末级'}</span>
|
|
|
+ <div class="cat-card__actions">
|
|
|
+ <button class="mini-btn" type="button" data-toast="新增子分类:${n.categoryName}">子分类</button>
|
|
|
+ <button class="mini-btn primary" type="button" data-toast="编辑:${n.categoryName}">编辑</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ `
|
|
|
+ })
|
|
|
+ .join('')
|
|
|
+
|
|
|
+ list.querySelectorAll('[data-toast]').forEach((btn) => {
|
|
|
+ btn.addEventListener('click', () => showToast(btn.getAttribute('data-toast')))
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ renderTreeA()
|
|
|
+ renderDrill()
|
|
|
+ renderFlat()
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|