Files
shanxia2.0/docs/demos/simple-doclib-demo.html
T

116 lines
35 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>文档库 · 山峡化工</title>
<!-- UI pattern: P04, reference: web-admin/src/views/system/user/index.vue. Standalone, in-memory demo. -->
<style>
:root{--primary:#10b981;--dark:#059669;--mint:#ecfdf5;--text:#1e293b;--muted:#64748b;--line:#d9e8e2;--radius:10px}
*{box-sizing:border-box}body{margin:0;color:var(--text);font:14px/1.5 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#f7faf8}button,input,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.4}button,input,select{outline:none}button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--primary);outline-offset:3px}button{color:inherit}svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.app{min-height:100vh;display:flex}.sidebar{width:220px;flex-shrink:0;background:white;border-right:1px solid var(--line);display:flex;flex-direction:column;padding:26px 16px;position:fixed;inset:0 auto 0 0}.brand{display:flex;align-items:center;gap:11px;padding:0 9px 30px}.brand-mark{width:36px;height:36px;display:grid;place-items:center;background:var(--primary);color:white;border-radius:10px}.brand-mark svg{width:25px;height:25px}.brand b{font-size:17px;letter-spacing:1px}.brand small{display:block;color:var(--muted);font-size:10px;letter-spacing:2px;margin-top:2px}.nav-label{font-size:11px;color:#94a3b8;padding:12px;letter-spacing:1px}.nav-item{display:flex;gap:12px;align-items:center;padding:12px;margin:3px 0;color:var(--muted);border-radius:8px}.nav-item.active{color:var(--dark);background:var(--mint);font-weight:600}.nav-item.active:after{content:'';width:6px;height:6px;background:var(--primary);border-radius:50%;margin-left:auto}.sidebar-foot{margin-top:auto;padding:18px 10px 0;border-top:1px solid var(--line);color:var(--muted);font-size:12px;display:flex;align-items:center;gap:8px}.main{margin-left:220px;min-width:0;flex:1}.topbar{height:64px;background:white;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 28px;gap:16px}.crumb{display:flex;gap:12px;align-items:center;color:var(--muted);font-size:13px}.crumb strong{font-weight:500;color:var(--text)}.identity{display:flex;gap:10px;align-items:center}.identity select{border:0;background:white;color:var(--text);max-width:180px;font-size:13px}.avatar{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#e3f4eb;color:#34745c;font-size:12px;flex-shrink:0}.avatar.alt{background:#eaf0f8;color:#53719b}.avatar.warm{background:#f8eee3;color:#9c7547}.content{padding:28px;max-width:1720px;margin:auto}.page-heading{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:25px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--dark);font-weight:700;margin:0 0 5px}.page-heading h1{font-size:26px;letter-spacing:.5px;margin:0 0 7px;font-weight:650}.page-heading p{margin:0;color:var(--muted);font-size:13px}.demo-label{font-size:11px;border:1px solid var(--line);padding:5px 10px;border-radius:20px;color:var(--muted);background:white;white-space:nowrap}.workspace{background:white;border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 2px 8px #203e2e03;overflow:hidden}.library{display:flex;min-height:570px}.folder-pane{width:210px;flex-shrink:0;border-right:1px solid var(--line);padding:22px 12px;display:flex;flex-direction:column}.folder-caption{display:flex;justify-content:space-between;align-items:center;padding:0 10px 17px;color:var(--muted);font-size:12px}.icon-btn{background:transparent;border:0;padding:4px;display:inline-flex;align-items:center;justify-content:center;border-radius:5px}.icon-btn:hover{background:var(--mint);color:var(--dark)}.folder-link{border:0;background:transparent;width:100%;display:flex;align-items:center;gap:9px;text-align:left;padding:11px 10px;border-radius:7px;font-size:13px;margin-bottom:3px}.folder-link svg{color:#83a896}.folder-link.current{background:var(--mint);color:var(--dark);font-weight:600}.folder-link.current svg{color:var(--dark)}.folder-link .folder-count{margin-left:auto;color:#94a3b8;font-size:11px}.folder-note{margin-top:auto;padding:22px 10px 0;color:#94a3b8;font-size:11px;line-height:1.9}.folder-note svg{width:14px;height:14px;vertical-align:-3px;margin-right:4px}.file-pane{flex:1;min-width:0;display:flex;flex-direction:column}.panel-top{padding:23px 24px 18px;display:flex;align-items:center;justify-content:space-between;gap:12px}.title-bar-heading{font-size:16px;font-weight:600;display:flex;align-items:center;gap:9px;margin:0}.title-bar-heading:before{content:'';width:3px;height:17px;border-radius:3px;background:var(--primary)}.subpath{color:var(--muted);font-size:11px;margin-top:6px}.search{display:flex;align-items:center;border:1px solid var(--line);padding:0 10px;height:35px;border-radius:8px;gap:8px;width:225px;color:#94a3b8}.search input{width:100%;border:0;background:transparent;min-width:0;font-size:12px}.toolbar{display:flex;align-items:center;gap:9px;padding:0 24px 19px;flex-wrap:wrap}.selection-count{font-size:12px;color:var(--muted);margin-right:5px}.selection-count b{color:var(--dark);font-weight:600}.btn{border:1px solid var(--line);border-radius:8px;background:white;padding:7px 12px;display:inline-flex;align-items:center;justify-content:center;gap:6px;font-size:12px;white-space:nowrap;min-height:34px}.btn svg{width:16px;height:16px}.btn:hover:not(:disabled){color:var(--dark);border-color:#6ee7b7;background:#f6fefa}.btn.primary{background:var(--primary);border-color:var(--primary);color:white}.btn.primary:hover:not(:disabled){background:var(--dark);color:white}.btn.danger{color:#dc2626}.table-scroll{overflow:auto;flex:1}table{border-collapse:collapse;width:100%;text-align:left;white-space:nowrap;font-size:12px}thead{background:#f7faf8;color:var(--muted)}th{font-weight:500;height:42px;border-block:1px solid var(--line);padding:0 14px}td{height:74px;padding:10px 14px;border-bottom:1px solid #eef3f0}tbody tr:nth-child(even){background:#fcfdfd}tbody tr:hover{background:#f3fbf7}tr.selected{background:#ecfdf5!important}.checkbox-cell{width:48px;padding-left:23px;padding-right:8px}input[type=checkbox]{width:14px;height:14px;accent-color:var(--primary);cursor:pointer}.file-name-cell{min-width:235px}.file-content{display:flex;align-items:center;gap:12px}.file-icon{width:36px;height:43px;position:relative;display:flex;align-items:center;justify-content:center;border:1px solid #d4dfec;color:#4874ad;background:#f3f7fc;border-radius:5px;flex-shrink:0;font-size:10px;font-weight:750;letter-spacing:.3px}.file-icon.pdf{border-color:#eddbd5;background:#fcf6f3;color:#b87561}.file-icon.xlsx{border-color:#cce6d8;background:#f0f9f4;color:#42956a}.file-icon.pptx{border-color:#eddfcb;background:#fcf8f0;color:#af884f}.file-icon:after{content:'';position:absolute;right:-1px;top:-1px;width:9px;height:9px;background:white;border-bottom:1px solid currentColor;border-left:1px solid currentColor;border-radius:0 4px 0 3px;opacity:.5}.file-name{padding:0;border:0;background:none;color:var(--dark);font-size:13px;font-weight:500;display:block;text-align:left;max-width:285px;overflow:hidden;text-overflow:ellipsis}.file-name:hover{text-decoration:underline}.file-path{font-size:11px;color:#94a3b8;margin-top:4px}.owner{display:flex;align-items:center;gap:7px}.owner .avatar{width:24px;height:24px;font-size:10px}.people{display:flex;align-items:center;gap:5px}.people .avatar{width:23px;height:23px;font-size:10px}.people-note{font-size:11px;color:var(--muted);margin-left:3px}.badge{border-radius:4px;padding:2px 6px;font-size:10px;color:white;background:var(--dark);display:inline-block}.badge.neutral{background:transparent;color:var(--text);padding-left:0}.table-footer{padding:18px 24px;display:flex;justify-content:space-between;gap:16px;font-size:11px;color:#94a3b8;border-top:1px solid var(--line)}.foot-hint{display:flex;gap:8px;align-items:center;margin:18px 2px;color:var(--muted);font-size:12px}.foot-hint svg{width:15px;height:15px;color:#88a898}.empty{text-align:center;padding:70px 20px;color:var(--muted)}.empty svg{width:36px;height:36px;color:#a7cdbb}.empty strong{display:block;color:var(--text);margin:12px 0 5px;font-size:14px}.empty p{font-size:12px}.demo-foot{font-size:11px;color:#94a3b8;text-align:center;margin-top:30px}.modal{border:0;padding:0;border-radius:12px;width:560px;max-width:calc(100vw - 32px);max-height:calc(100vh - 100px);box-shadow:0 20px 70px #132f2825;color:var(--text)}.modal::backdrop{background:rgba(15,23,42,.35);backdrop-filter:none}.dialog-form-layout{display:flex;flex-direction:column;max-height:calc(100vh - 100px)}.modal-head{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--line)}.modal-head h2{font-size:17px;margin:0}.modal-body{padding:24px;overflow:auto}.modal-footer{padding:15px 24px;background:#f7faf8;border-top:1px solid var(--line);display:flex;justify-content:flex-end;gap:10px}.field{margin-bottom:20px}.field:last-child{margin-bottom:0}.field>label:not(.person-choice){display:block;margin-bottom:8px;font-size:13px;font-weight:500}.form-label-tip{font-size:12px;color:var(--muted);font-weight:400;margin-left:8px}.field input[type=text],.field select{height:38px;width:100%;border:1px solid var(--line);border-radius:8px;padding:0 10px;background:white}.form-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:18px}.form-col-span-2{grid-column:1/-1}.muted{color:var(--muted);font-size:12px}.file-summary{display:flex;align-items:center;gap:13px;padding:16px;border:1px solid var(--line);background:#fafcfb;border-radius:8px;margin-bottom:22px}.file-summary strong{font-size:13px;overflow-wrap:anywhere}.file-summary small{display:block;color:var(--muted);margin-top:4px}.person-choice{display:flex;align-items:center;gap:10px;padding:12px 0;border-bottom:1px solid #eef3f0;cursor:pointer}.person-choice input{margin-left:auto}.person-choice small{display:block;color:var(--muted);font-size:11px}.share-explain{background:var(--mint);color:#3e8068;font-size:12px;padding:11px 13px;border-radius:7px;margin-top:17px}.upload-row{display:flex;gap:10px;flex-wrap:wrap}.upload-card{width:140px;height:96px;border:1px dashed #a7f3d0;border-radius:8px;background:#f7fefa;color:var(--dark);display:flex;flex-direction:column;gap:8px;justify-content:center;align-items:center;font-size:12px}.upload-item{width:140px;min-height:96px;border:1px solid var(--line);border-radius:8px;padding:12px;font-size:11px;overflow-wrap:anywhere}.upload-item b{display:block;margin:6px 0;font-weight:500}.upload-item .icon-btn{float:right}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;font-size:13px}.detail-grid dt{color:var(--muted);font-size:12px;margin-bottom:6px}.detail-grid dd{margin:0;overflow-wrap:anywhere}.toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%) translateY(20px);background:#1e3e31;color:white;padding:12px 22px;border-radius:9px;box-shadow:0 8px 30px #15312320;font-size:13px;opacity:0;pointer-events:none;transition:.2s;z-index:20;max-width:90vw}.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}[hidden]{display:none!important}
@media(min-width:1600px){td{height:80px}.library{min-height:640px}}@media(max-width:1150px){.sidebar{width:185px}.main{margin-left:185px}.content{padding:20px}.folder-pane{width:175px}.panel-top,.toolbar{padding-left:18px;padding-right:18px}.search{width:185px}.file-name{max-width:230px}.topbar{padding:0 20px}}@media(max-width:800px){.sidebar{display:none}.main{margin-left:0}.content{padding:16px}.topbar{padding:0 16px}.identity .avatar,.identity>span{display:none}.folder-pane{width:155px}.panel-top{align-items:flex-start;flex-direction:column}.search{width:100%}.page-heading h1{font-size:23px}.library{min-height:520px}.table-footer{padding:14px}.table-footer span:last-child{display:none}}@media(max-width:520px){.library{display:block}.folder-pane{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:12px}.folder-caption,.folder-note{display:none}#folderList{max-height:260px;overflow:auto}.folder-link{white-space:nowrap;padding:8px}.toolbar{gap:7px}.btn{padding:7px 9px}.form-grid-2,.detail-grid{grid-template-columns:1fr}.page-heading p{font-size:12px}.demo-label{display:none}.crumb{font-size:12px;gap:6px}.modal-body{padding:18px}}
.tree-group + .tree-group{margin-top:18px}.tree-root{display:flex;align-items:center;gap:2px}.tree-root .folder-link{font-weight:600;flex:1;min-width:0}.tree-toggle{width:20px;height:32px;flex-shrink:0;color:#94a3b8}.tree-toggle svg{width:13px;height:13px;transition:transform .15s}.tree-toggle[aria-expanded=true] svg{transform:rotate(90deg)}.tree-children{margin:3px 0 0 12px;border-left:1px solid var(--line);padding-left:9px}.tree-children .folder-link{font-size:12px}.tree-children .folder-link>span:not(.folder-count){overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{font-size:11px;color:#94a3b8;padding:10px}.folder-pane{width:230px}@media(max-width:1150px){.folder-pane{width:210px}}@media(max-width:800px){.folder-pane{width:190px}}@media(max-width:520px){.folder-pane{width:100%}}
</style>
</head>
<body>
<svg style="display:none" aria-hidden="true"><defs>
<symbol id="i-folder" viewBox="0 0 24 24"><path d="M3 7V5a1 1 0 0 1 1-1h5l2 3h9a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z"/></symbol>
<symbol id="i-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
<symbol id="i-file" viewBox="0 0 24 24"><path d="M14 3H5v18h14V8Zm0 0v5h5M8 12h8M8 16h6"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><path d="M3 21v-3a6 6 0 0 1 12 0v3M16 5a3 3 0 0 1 0 6M18 14a5 5 0 0 1 3 4v3"/></symbol>
<symbol id="i-upload" viewBox="0 0 24 24"><path d="M12 16V3m-5 5 5-5 5 5M4 15v5h16v-5"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 3v13m-5-5 5 5 5-5M4 16v5h16v-5"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/></symbol>
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="11" rx="2"/><path d="M8 10V6a4 4 0 0 1 8 0v4M12 14v3"/></symbol>
<symbol id="i-chevron" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></symbol>
<symbol id="i-close" viewBox="0 0 24 24"><path d="m6 6 12 12M6 18 18 6"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></symbol>
<symbol id="i-mountain" viewBox="0 0 24 24"><path d="m2 19 7-14 7 14M12 19l5-10 5 10M6 19h12"/></symbol>
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v6m0-10v.1"/></symbol>
</defs></svg>
<div class="app">
<aside class="sidebar"><div class="brand"><div class="brand-mark"><svg><use href="#i-mountain"/></svg></div><div><b>山峡化工</b><small>SHANXIA CHEMICAL</small></div></div><div class="nav-label">工作空间</div><div class="nav-item"><svg><use href="#i-grid"/></svg>工作台</div><div class="nav-item"><svg><use href="#i-users"/></svg>客户管理</div><div class="nav-item"><svg><use href="#i-file"/></svg>业务管理</div><div class="nav-item active"><svg><use href="#i-folder"/></svg>文档库</div><div class="sidebar-foot"><svg><use href="#i-lock"/></svg>企业内部资料 · 安全共享</div></aside>
<main class="main"><header class="topbar"><div class="crumb">山峡一体化门户<svg style="width:12px"><use href="#i-chevron"/></svg><strong>文档库</strong></div><div class="identity"><span class="muted" style="font-size:11px">演示身份</span><span class="avatar" id="userAvatar">林</span><select id="identitySelect" aria-label="切换演示身份"><option value="lin">林晓 · 上传人</option><option value="chen">陈晨 · 分享接收人</option><option value="admin">超级管理员</option></select></div></header>
<div class="content"><div class="page-heading"><div><div class="eyebrow">TEAM DOCUMENTS</div><h1>文档库</h1><p>把资料整理好,分享给需要的人。</p></div><span class="demo-label">交互演示 / DEMO</span></div>
<section class="workspace"><div class="library"><aside class="folder-pane"><div class="folder-caption"><span>文档目录</span><button class="icon-btn" id="folderPlus" title="新建文件夹" aria-label="新建文件夹"><svg><use href="#i-plus"/></svg></button></div><nav id="folderList" aria-label="文档目录"></nav><div class="folder-note"><svg><use href="#i-lock"/></svg>文档默认仅自己可见<br>分享后,指定人员可查看和下载。</div></aside>
<div class="file-pane"><div class="panel-top"><div><h2 class="title-bar-heading" id="listTitle">全部文件</h2><div class="subpath" id="currentPath">我的文档 / 全部文件</div></div><label class="search"><svg><use href="#i-search"/></svg><input id="search" placeholder="搜索文档名称" aria-label="搜索文档名称"></label></div>
<div class="toolbar"><span class="selection-count">已选 <b id="selectedCount">0</b> 项</span><button class="btn primary" id="uploadBtn"><svg><use href="#i-upload"/></svg>上传文档</button><button class="btn" id="newFolderBtn"><svg><use href="#i-plus"/></svg>新建文件夹</button><button class="btn" id="shareBtn" disabled><svg><use href="#i-users"/></svg>设置分享</button><button class="btn" id="downloadBtn" disabled><svg><use href="#i-download"/></svg>下载</button><button class="btn" id="deleteBtn" disabled><svg><use href="#i-trash"/></svg>删除</button><button class="btn" id="deleteFolderBtn" hidden><svg><use href="#i-trash"/></svg>删除空文件夹</button></div>
<div class="table-scroll"><table><thead><tr><th class="checkbox-cell"><input type="checkbox" id="selectAll" aria-label="全选文档"></th><th style="min-width:260px">文档名称</th><th style="min-width:85px">大小</th><th style="min-width:100px">上传人</th><th style="min-width:145px">分享人员</th><th style="min-width:130px">上传时间</th></tr></thead><tbody id="fileRows"></tbody></table><div class="empty" id="empty" hidden><svg><use href="#i-folder"/></svg><strong>这里还没有文档</strong><p id="emptyHint">上传第一份资料,开始整理你的文档。</p></div></div><footer class="table-footer"><span id="totalText"></span><span>按上传时间从新到旧排列</span></footer></div></div></section>
<div class="foot-hint"><svg><use href="#i-info"/></svg><span id="permissionHint">分享人员可查看、下载;只有上传人和超级管理员可以删除文档。</span></div><div class="demo-foot">演示数据仅在当前页面有效 · 刷新后恢复初始状态 · 不会上传到服务器</div></div></main></div>
<dialog class="modal" id="modal" aria-labelledby="modalTitle"><div class="dialog-form-layout"><header class="modal-head"><h2 id="modalTitle"></h2><button class="icon-btn" id="closeModal" aria-label="关闭弹窗"><svg><use href="#i-close"/></svg></button></header><div class="modal-body" id="modalBody"></div><footer class="modal-footer" id="modalFooter"></footer></div></dialog><div class="toast" id="toast" role="status"></div><input type="file" id="fileInput" multiple hidden>
<script>
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)]
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))
const icon=n=>`<svg aria-hidden="true"><use href="#i-${n}"/></svg>`
const users={lin:{name:'林晓',dept:'综合管理部'},chen:{name:'陈晨',dept:'采购部'},wang:{name:'王敏',dept:'生产部'},zhou:{name:'周远',dept:'销售部'},admin:{name:'超级管理员',dept:'系统管理'}}
let folders=[{id:'f1',name:'公司制度',owner:'lin',parent:''},{id:'f2',name:'产品资料',owner:'lin',parent:''},{id:'f3',name:'合同模板',owner:'lin',parent:''},{id:'f4',name:'项目资料',owner:'lin',parent:''},{id:'f5',name:'采购资料',owner:'chen',parent:''},{id:'f6',name:'安全生产',owner:'wang',parent:''}]
let files=[
{id:'d1',name:'员工日常管理制度.pdf',size:'1.8 MB',folder:'f1',owner:'lin',shares:['chen','wang','zhou'],time:'2026-10-26 10:32'},
{id:'d2',name:'产品规格与技术参数.xlsx',size:'426 KB',folder:'f2',owner:'lin',shares:['chen','zhou'],time:'2026-10-26 09:15'},
{id:'d3',name:'采购合同标准模板.docx',size:'286 KB',folder:'f3',owner:'lin',shares:['chen'],time:'2026-10-25 16:40'},
{id:'d4',name:'新员工入职指引.pdf',size:'2.4 MB',folder:'f1',owner:'lin',shares:['chen','wang'],time:'2026-10-25 14:20'},
{id:'d5',name:'项目沟通纪要.docx',size:'128 KB',folder:'f4',owner:'lin',shares:[],time:'2026-10-24 11:08'},
{id:'d6',name:'山峡化工产品介绍.pptx',size:'6.2 MB',folder:'f2',owner:'lin',shares:['zhou'],time:'2026-10-23 15:36'},
{id:'d7',name:'供应商资料清单.xlsx',size:'315 KB',folder:'f5',owner:'chen',shares:['lin'],time:'2026-10-26 08:45'},
{id:'d8',name:'安全生产操作规程.pdf',size:'3.1 MB',folder:'f6',owner:'wang',shares:['lin','chen'],time:'2026-10-25 09:30'}]
let user='lin',tab='mine',folder='',selected=new Set(),pending=[],toastTimer
const expandedScopes={mine:true,shared:true,all:true}
const isAdmin=()=>user==='admin'
const canRead=f=>isAdmin()||f.owner===user||f.shares.includes(user)
const canManage=f=>isAdmin()||f.owner===user
const scopeFiles=(scope=tab)=>files.filter(f=>canRead(f)&&(scope==='mine'?f.owner===user:scope==='shared'?f.owner!==user&&f.shares.includes(user):isAdmin()))
const visibleFiles=()=>scopeFiles().filter(f=>(!folder||f.folder===folder)&&f.name.toLowerCase().includes($('#search').value.trim().toLowerCase())).sort((a,b)=>b.time.localeCompare(a.time))
function folderPath(id){const f=folders.find(x=>x.id===id);return f?(f.parent?folderPath(f.parent)+' / ':'')+f.name:'未分类'}
function avatar(id){return `<span class="avatar ${id==='chen'?'alt':id==='zhou'?'warm':''}">${esc(users[id].name[0])}</span>`}
function fileIcon(f){const ext=f.name.split('.').pop().toLowerCase();return `<div class="file-icon ${['pdf','xlsx','pptx'].includes(ext)?ext:''}">${esc(ext.length<6?ext.toUpperCase():'FILE')}</div>`}
function sharePeople(f){return f.shares.length?`<div class="people">${f.shares.slice(0,3).map(avatar).join('')}<span class="people-note">${f.shares.length} 人</span></div>`:'<span class="muted" style="font-size:11px">仅自己可见</span>'}
function notify(message){$('#toast').textContent=message;$('#toast').classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('#toast').classList.remove('show'),2800)}
function render(){
const visible=visibleFiles();selected=new Set([...selected].filter(id=>visible.some(f=>f.id===id)))
const scopes=[{id:'mine',label:'我的文档',icon:'folder'},{id:'shared',label:'分享给我',icon:'users'},...(isAdmin()?[{id:'all',label:'全部文档',icon:'grid'}]:[])]
$('#folderList').innerHTML=scopes.map(scope=>{
const docs=scopeFiles(scope.id)
const relevant=new Set(scope.id==='mine'?folders.filter(f=>f.owner===user).map(f=>f.id):scope.id==='all'?folders.map(f=>f.id):docs.map(f=>f.folder))
for(const id of [...relevant]){let parent=folders.find(f=>f.id===id)?.parent;while(parent){relevant.add(parent);parent=folders.find(f=>f.id===parent)?.parent}}
const branch=(parent='',depth=0)=>folders.filter(f=>f.parent===parent&&relevant.has(f.id)).map(f=>`<button class="folder-link ${tab===scope.id&&folder===f.id?'current':''}" data-scope="${scope.id}" data-folder="${f.id}" style="padding-left:${10+depth*14}px" title="${esc(folderPath(f.id))}">${icon('folder')}<span>${esc(f.name)}</span><span class="folder-count">${docs.filter(d=>d.folder===f.id).length}</span></button>${branch(f.id,depth+1)}`).join('')
return `<div class="tree-group"><div class="tree-root"><button class="icon-btn tree-toggle" data-toggle-scope="${scope.id}" aria-label="展开或收起${scope.label}" aria-expanded="${expandedScopes[scope.id]}" aria-controls="children-${scope.id}">${icon('chevron')}</button><button class="folder-link ${tab===scope.id&&!folder?'current':''}" data-scope="${scope.id}" data-folder="">${icon(scope.icon)}<span>${scope.label}</span><span class="folder-count">${docs.length}</span></button></div><div class="tree-children" id="children-${scope.id}" ${expandedScopes[scope.id]?'':'hidden'}>${branch()||'<div class="tree-empty">暂无文件夹</div>'}</div></div>`
}).join('')
$$('[data-scope]').forEach(b=>b.onclick=()=>{tab=b.dataset.scope;folder=b.dataset.folder;expandedScopes[tab]=true;selected.clear();$('#search').value='';render()})
$$('[data-toggle-scope]').forEach(b=>b.onclick=()=>{const scope=b.dataset.toggleScope;expandedScopes[scope]=!expandedScopes[scope];render()})
const current=folders.find(f=>f.id===folder), scopeName=tab==='mine'?'我的文档':tab==='shared'?'分享给我':'全部文档'
$('#listTitle').textContent=current?current.name:scopeName;$('#currentPath').textContent=scopeName+' / '+(current?folderPath(current.id):'全部文件')
$('#fileRows').innerHTML=visible.map(f=>`<tr class="${selected.has(f.id)?'selected':''}" data-row="${f.id}"><td class="checkbox-cell"><input type="checkbox" data-select="${f.id}" aria-label="选择 ${esc(f.name)}" ${selected.has(f.id)?'checked':''}></td><td class="file-name-cell"><div class="file-content">${fileIcon(f)}<div><button class="file-name" data-detail="${f.id}">${esc(f.name)}</button><div class="file-path">${esc(folderPath(f.folder))}</div></div></div></td><td class="muted">${f.size}</td><td><div class="owner">${avatar(f.owner)}${esc(users[f.owner].name)}</div></td><td>${sharePeople(f)}</td><td class="muted" style="font-size:11px">${f.time}</td></tr>`).join('')
$$('[data-select]').forEach(c=>c.onchange=()=>{c.checked?selected.add(c.dataset.select):selected.delete(c.dataset.select);render()});$$('[data-detail]').forEach(b=>b.onclick=()=>showDetail(b.dataset.detail));$$('[data-row]').forEach(r=>r.ondblclick=()=>showDetail(r.dataset.row))
$('#empty').hidden=visible.length>0;$('#emptyHint').textContent=$('#search').value?'没有匹配的文档,请尝试其他名称。':tab==='shared'?'同事分享给你的文档会出现在这里。':'上传第一份资料,开始整理你的文档。'
$('#totalText').textContent=`共 ${visible.length} 份文档`;$('#selectedCount').textContent=selected.size;$('#selectAll').checked=!!visible.length&&selected.size===visible.length;$('#selectAll').indeterminate=selected.size>0&&selected.size<visible.length
const chosen=files.filter(f=>selected.has(f.id));$('#shareBtn').disabled=chosen.length!==1||!canManage(chosen[0]);$('#downloadBtn').disabled=!chosen.length;$('#deleteBtn').disabled=!chosen.length||!chosen.every(canManage)
$('#deleteBtn').title=chosen.length&&!chosen.every(canManage)?'只有上传人和超级管理员可以删除':'删除选中文档'
for(const id of ['uploadBtn','newFolderBtn','folderPlus'])$('#'+id).hidden=tab!=='mine'
$('#deleteFolderBtn').hidden=!current||!(isAdmin()||current.owner===user)||tab==='shared';$('#deleteFolderBtn').disabled=!!current&&(files.some(f=>f.folder===current.id)||folders.some(f=>f.parent===current.id))
$('#userAvatar').textContent=users[user].name[0]
}
const modal=$('#modal')
function closeModal(){modal.close();pending=[];$('#fileInput').value=''}
function openModal(title,body,actions){$('#modalTitle').textContent=title;$('#modalBody').innerHTML=body;$('#modalFooter').innerHTML=actions.map((a,i)=>`<button class="btn ${a.primary?'primary':''} ${a.danger?'danger':''}" data-modal-action="${i}">${icon(a.icon||'check')}${a.label}</button>`).join('');$$('[data-modal-action]').forEach(b=>b.onclick=actions[Number(b.dataset.modalAction)].run);if(!modal.open)modal.showModal()}
const cancelAction=()=>({label:'取消',icon:'close',run:closeModal})
function summary(f){return `<div class="file-summary">${fileIcon(f)}<div><strong>${esc(f.name)}</strong><small>${f.size} · ${esc(users[f.owner].name)} 上传</small></div></div>`}
function showDetail(id){const f=files.find(f=>f.id===id);if(!f||!canRead(f))return;openModal('文档详情',summary(f)+`<dl class="detail-grid"><div><dt>所在文件夹</dt><dd>${esc(folderPath(f.folder))}</dd></div><div><dt>上传时间</dt><dd>${f.time}</dd></div><div><dt>分享人员</dt><dd>${f.shares.length?f.shares.map(id=>esc(users[id].name)).join('、'):'未分享'}</dd></div><div><dt>我的权限</dt><dd><span class="badge">${canManage(f)?'可管理':'可查看、下载'}</span></dd></div></dl><div class="share-explain">${f.blob?'此文件由你在本次演示中选择,可下载原文件。':'这是演示文档,点击下载将获得一份标明原文件名称的 TXT 示例说明。'}</div>`,[...(canManage(f)?[{label:'删除',icon:'trash',danger:true,run:()=>confirmDelete([f.id])},{label:'设置分享',icon:'users',run:()=>showShare(f.id)}]:[]),{label:'下载文档',icon:'download',primary:true,run:()=>downloadFile(f)}])}
function showShare(id){const f=files.find(f=>f.id===id);if(!f||!canManage(f))return;const candidates=Object.entries(users).filter(([id])=>id!==f.owner&&id!=='admin');openModal('设置分享人员',summary(f)+`<div class="field"><label>分享给<span class="form-label-tip">可多选,取消勾选即收回访问权限</span></label>${candidates.map(([id,u])=>`<label class="person-choice">${avatar(id)}<span>${esc(u.name)}<small>${esc(u.dept)}</small></span><input type="checkbox" name="recipient" value="${id}" ${f.shares.includes(id)?'checked':''} aria-label="分享给${esc(u.name)}"></label>`).join('')}</div><div class="share-explain">分享人员可以查看、下载此文档,不能删除或修改分享设置。</div>`,[cancelAction(),{label:'保存分享',icon:'check',primary:true,run:()=>{f.shares=$$('input[name=recipient]:checked').map(c=>c.value);closeModal();render();notify(f.shares.length?`已分享给 ${f.shares.length} 人`:'已取消分享,仅上传人和超级管理员可见')}}])}
function folderOptions(owner,includeRoot=true){return (includeRoot?'<option value="">根目录</option>':'')+folders.filter(f=>f.owner===owner).map(f=>`<option value="${f.id}" ${folder===f.id?'selected':''}>${esc(folderPath(f.id))}</option>`).join('')}
function newFolder(){openModal('新建文件夹',`<div class="form-grid-2"><div class="field form-col-span-2"><label for="folderName">文件夹名称</label><input id="folderName" type="text" maxlength="50" placeholder="例如:培训资料" autocomplete="off"></div><div class="field form-col-span-2"><label for="parentFolder">上级文件夹</label><select id="parentFolder">${folderOptions(user)}</select></div></div>`,[cancelAction(),{label:'创建文件夹',icon:'check',primary:true,run:()=>{const name=$('#folderName').value.trim(),parent=$('#parentFolder').value;if(!name)return notify('请填写文件夹名称');if(folders.some(f=>f.owner===user&&f.parent===parent&&f.name===name))return notify('同一目录下已存在此名称');const id='f'+Date.now();folders.push({id,name,parent,owner:user});folder=id;tab='mine';expandedScopes.mine=true;closeModal();render();notify('文件夹已创建')}}]);$('#folderName').focus()}
function showUpload(){pending=[];openModal('上传文档',`<div class="field"><label for="uploadFolder">上传到<span class="form-label-tip">选择自己的文件夹</span></label><select id="uploadFolder">${folderOptions(user)}</select></div><div class="field"><label>选择文档<span class="form-label-tip">支持一次选择多个文件</span></label><div class="upload-row" id="uploadRow"></div><p class="muted">文件仅保存在当前页面内存中,刷新后清空。</p></div><div class="share-explain">上传后默认仅自己可见,可在文档列表中设置分享人员。</div>`,[cancelAction(),{label:'完成上传',icon:'upload',primary:true,run:()=>{if(!pending.length)return notify('请先选择文档');const destination=$('#uploadFolder').value;pending.forEach((blob,i)=>files.push({id:'d'+Date.now()+'-'+i,name:blob.name,size:blob.size<1048576?Math.max(1,Math.ceil(blob.size/1024))+' KB':(blob.size/1048576).toFixed(1)+' MB',folder:destination,owner:user,shares:[],time:'2026-10-26 12:00',blob}));const n=pending.length;folder=destination;closeModal();render();notify(`已添加 ${n} 份文档(本地演示)`)}}]);renderUpload()}
function renderUpload(){$('#uploadRow').innerHTML=`<button class="upload-card" id="chooseFiles">${icon('plus')}选择文件</button>`+pending.map((f,i)=>`<div class="upload-item"><button class="icon-btn" data-remove-upload="${i}" aria-label="移除 ${esc(f.name)}">${icon('close')}</button>${icon('file')}<b>${esc(f.name)}</b><span class="muted">已选择</span></div>`).join('');$('#chooseFiles').onclick=()=>$('#fileInput').click();$$('[data-remove-upload]').forEach(b=>b.onclick=()=>{pending.splice(Number(b.dataset.removeUpload),1);renderUpload()})}
function confirmDelete(ids){const targets=files.filter(f=>ids.includes(f.id));if(!targets.length||!targets.every(canManage))return notify('只有上传人和超级管理员可以删除');openModal('删除文档',`<p style="margin-top:0">确定删除这 ${targets.length} 份文档吗?</p><p class="muted" style="overflow-wrap:anywhere">${targets.map(f=>esc(f.name)).join('<br>')}</p><div class="share-explain">删除后,分享人员将无法继续查看或下载。本演示刷新后可恢复。</div>`,[cancelAction(),{label:'确认删除',icon:'trash',danger:true,run:()=>{files=files.filter(f=>!ids.includes(f.id));selected.clear();closeModal();render();notify('文档已删除')}}])}
function downloadFile(f){if(!canRead(f))return;const blob=f.blob||new Blob([`山峡化工文档库 · 演示说明\n\n原文档名称:${f.name}\n这是一份交互演示用说明,不是原始业务文档。\n上传人:${users[f.owner].name}\n文件夹:${folderPath(f.folder)}\n`],{type:'text/plain;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=f.blob?f.name:f.name+'-演示说明.txt';document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),10000);notify(f.blob?'已下载所选原文件':'已下载 TXT 演示说明')}
$('#identitySelect').onchange=e=>{closeModal();user=e.target.value;tab=user==='chen'?'shared':isAdmin()?'all':'mine';folder='';selected.clear();$('#search').value='';render();notify('已切换为'+users[user].name)}
$('#search').oninput=render;$('#selectAll').onchange=e=>{selected=new Set(e.target.checked?visibleFiles().map(f=>f.id):[]);render()};$('#newFolderBtn').onclick=newFolder;$('#folderPlus').onclick=newFolder;$('#uploadBtn').onclick=showUpload;$('#shareBtn').onclick=()=>showShare([...selected][0]);$('#downloadBtn').onclick=()=>files.filter(f=>selected.has(f.id)).forEach(downloadFile);$('#deleteBtn').onclick=()=>confirmDelete([...selected]);$('#closeModal').onclick=closeModal
$('#fileInput').onchange=e=>{pending.push(...e.target.files);e.target.value='';renderUpload()};modal.addEventListener('cancel',()=>{pending=[]});modal.addEventListener('click',e=>{if(e.target===modal){const r=modal.getBoundingClientRect();if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)closeModal()}})
$('#deleteFolderBtn').onclick=()=>{const f=folders.find(f=>f.id===folder);if(!f||!(isAdmin()||f.owner===user)||files.some(d=>d.folder===f.id)||folders.some(d=>d.parent===f.id))return;openModal('删除空文件夹',`确定删除文件夹「${esc(f.name)}」吗?`,[cancelAction(),{label:'确认删除',icon:'trash',danger:true,run:()=>{folders=folders.filter(d=>d.id!==f.id);folder='';closeModal();render();notify('空文件夹已删除')}}])}
render()
</script>
</body>
</html>