oa-cc-unread-badge-demo.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>OA 抄送我的 · 未读角标 Demo</title>
  7. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  8. <style>
  9. :root {
  10. --primary: #10b981;
  11. --primary-dark: #059669;
  12. --primary-bg: #ecfdf5;
  13. --text: #1e293b;
  14. --text-muted: #64748b;
  15. --border: #d9e8e2;
  16. --bg-page: #f8fafc;
  17. --surface: #ffffff;
  18. --table-head: #f8fafc;
  19. --row-alt: #fafafa;
  20. --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  21. --radius: 10px;
  22. --warn: #d97706;
  23. --warn-bg: #fffbeb;
  24. --danger: #fd4e4e;
  25. --badge-zero: #94a3b8;
  26. }
  27. * { box-sizing: border-box; }
  28. body {
  29. margin: 0;
  30. font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
  31. font-size: 14px;
  32. line-height: 1.5;
  33. color: var(--text);
  34. background: #eef2f0;
  35. padding: 24px;
  36. }
  37. .page-head {
  38. max-width: 1180px;
  39. margin: 0 auto 16px;
  40. }
  41. .page-head h1 {
  42. margin: 0 0 6px;
  43. font-size: 22px;
  44. font-weight: 700;
  45. }
  46. .page-head p {
  47. margin: 0;
  48. color: var(--text-muted);
  49. font-size: 13px;
  50. }
  51. .shell-wrap {
  52. max-width: 1180px;
  53. margin: 0 auto;
  54. background: var(--surface);
  55. border: 1px solid var(--border);
  56. border-radius: var(--radius);
  57. box-shadow: var(--shadow);
  58. overflow: hidden;
  59. }
  60. .shell { display: flex; min-height: 620px; }
  61. .sidebar {
  62. width: 220px;
  63. flex-shrink: 0;
  64. border-right: 1px solid var(--border);
  65. background: #fff;
  66. padding: 16px 12px;
  67. }
  68. .logo {
  69. font-weight: 700;
  70. padding: 4px 12px 16px;
  71. border-bottom: 1px solid var(--border);
  72. margin-bottom: 12px;
  73. }
  74. .logo span { color: var(--primary); }
  75. .nav-group {
  76. font-size: 12px;
  77. font-weight: 700;
  78. color: var(--text-muted);
  79. padding: 8px 12px;
  80. letter-spacing: 0.02em;
  81. }
  82. .nav-item {
  83. position: relative;
  84. display: flex;
  85. align-items: center;
  86. gap: 8px;
  87. padding: 9px 12px;
  88. border-radius: 8px;
  89. color: var(--text);
  90. text-decoration: none;
  91. font-size: 13px;
  92. margin-bottom: 2px;
  93. cursor: pointer;
  94. border: none;
  95. background: transparent;
  96. width: 100%;
  97. text-align: left;
  98. font-family: inherit;
  99. }
  100. .nav-item:hover { background: #f1f5f9; }
  101. .nav-item.active {
  102. background: var(--primary-bg);
  103. color: var(--primary-dark);
  104. font-weight: 600;
  105. }
  106. .nav-item .menu-name { flex: 1; min-width: 0; }
  107. /* 对齐 web-admin .art-text-badge */
  108. .art-text-badge {
  109. min-width: 20px;
  110. height: 18px;
  111. line-height: 17px;
  112. padding: 0 5px;
  113. font-size: 10px;
  114. font-weight: 700;
  115. color: #fff;
  116. text-align: center;
  117. background: var(--danger);
  118. border-radius: 4px;
  119. flex-shrink: 0;
  120. }
  121. .art-text-badge.is-zero {
  122. background: var(--badge-zero);
  123. }
  124. .nav-item.parent { font-weight: 600; }
  125. .nav-children { padding-left: 8px; margin-bottom: 8px; }
  126. .main { flex: 1; background: var(--bg-page); padding: 20px; min-width: 0; }
  127. .content-card {
  128. background: #fff;
  129. border: 1px solid var(--border);
  130. border-radius: var(--radius);
  131. overflow: hidden;
  132. }
  133. .panel-top {
  134. padding: 16px 20px;
  135. border-bottom: 1px solid var(--border);
  136. display: flex;
  137. align-items: center;
  138. gap: 12px;
  139. }
  140. .panel-top h2 {
  141. margin: 0;
  142. font-size: 16px;
  143. font-weight: 700;
  144. display: flex;
  145. align-items: center;
  146. gap: 8px;
  147. }
  148. .panel-top h2::before {
  149. content: '';
  150. width: 3px;
  151. height: 16px;
  152. background: var(--primary);
  153. border-radius: 2px;
  154. }
  155. .panel-meta {
  156. margin-left: auto;
  157. font-size: 12px;
  158. color: var(--text-muted);
  159. }
  160. .panel-meta strong { color: var(--danger); }
  161. .list-body { padding: 0; }
  162. table { width: 100%; border-collapse: collapse; }
  163. th, td {
  164. padding: 12px 14px;
  165. text-align: center;
  166. border-bottom: 1px solid #eef2f0;
  167. font-size: 13px;
  168. }
  169. th {
  170. background: var(--table-head);
  171. color: var(--text-muted);
  172. font-weight: 600;
  173. font-size: 12px;
  174. }
  175. tr:nth-child(even) td { background: var(--row-alt); }
  176. tr.unread td:first-child .title-cell {
  177. font-weight: 700;
  178. }
  179. .title-cell {
  180. display: inline-flex;
  181. align-items: center;
  182. gap: 6px;
  183. justify-content: center;
  184. }
  185. .dot-unread {
  186. width: 7px;
  187. height: 7px;
  188. border-radius: 50%;
  189. background: var(--danger);
  190. flex-shrink: 0;
  191. }
  192. .tag {
  193. display: inline-block;
  194. padding: 2px 8px;
  195. border-radius: 4px;
  196. font-size: 12px;
  197. font-weight: 600;
  198. }
  199. .tag-running { background: var(--primary-bg); color: var(--primary-dark); }
  200. .tag-done { background: #f1f5f9; color: var(--text-muted); }
  201. .tag-unread { background: #fef2f2; color: #dc2626; }
  202. .tag-read { background: #f1f5f9; color: var(--text-muted); }
  203. .ops { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
  204. .btn {
  205. display: inline-flex;
  206. align-items: center;
  207. gap: 5px;
  208. height: 30px;
  209. padding: 0 10px;
  210. border-radius: 8px;
  211. border: 1px solid var(--border);
  212. background: #fff;
  213. color: var(--text);
  214. font-size: 12px;
  215. font-weight: 500;
  216. cursor: pointer;
  217. font-family: inherit;
  218. }
  219. .btn:hover { background: #f8fafc; }
  220. .btn-primary {
  221. color: var(--primary-dark);
  222. border-color: #a7f3d0;
  223. background: var(--primary-bg);
  224. }
  225. .btn-primary:hover { background: #d1fae5; }
  226. .btn:disabled {
  227. opacity: 0.45;
  228. cursor: not-allowed;
  229. }
  230. .toast {
  231. position: fixed;
  232. right: 28px;
  233. bottom: 28px;
  234. background: #0f172a;
  235. color: #fff;
  236. padding: 10px 14px;
  237. border-radius: 8px;
  238. font-size: 13px;
  239. opacity: 0;
  240. transform: translateY(8px);
  241. transition: 0.2s ease;
  242. pointer-events: none;
  243. z-index: 20;
  244. }
  245. .toast.show { opacity: 1; transform: translateY(0); }
  246. .note {
  247. max-width: 1180px;
  248. margin: 14px auto 0;
  249. padding: 12px 14px;
  250. background: #fff;
  251. border: 1px dashed #a7f3d0;
  252. border-radius: 8px;
  253. color: var(--text-muted);
  254. font-size: 13px;
  255. }
  256. .note strong { color: var(--primary-dark); }
  257. .note ul { margin: 8px 0 0; padding-left: 18px; }
  258. .ref-row {
  259. max-width: 1180px;
  260. margin: 16px auto 0;
  261. display: grid;
  262. grid-template-columns: 1fr 1fr;
  263. gap: 12px;
  264. }
  265. .ref-card {
  266. background: #fff;
  267. border: 1px solid var(--border);
  268. border-radius: 10px;
  269. padding: 14px 16px;
  270. }
  271. .ref-card h3 {
  272. margin: 0 0 10px;
  273. font-size: 13px;
  274. font-weight: 700;
  275. }
  276. .ref-menu {
  277. display: flex;
  278. align-items: center;
  279. gap: 8px;
  280. padding: 10px 12px;
  281. border-radius: 8px;
  282. background: var(--primary-bg);
  283. color: var(--primary-dark);
  284. font-weight: 600;
  285. font-size: 13px;
  286. }
  287. .ref-menu .spacer { flex: 1; }
  288. .legend {
  289. margin-top: 8px;
  290. font-size: 12px;
  291. color: var(--text-muted);
  292. }
  293. /* 简易详情抽屉示意 */
  294. .drawer-mask {
  295. display: none;
  296. position: fixed;
  297. inset: 0;
  298. background: rgba(15, 23, 42, 0.35);
  299. z-index: 30;
  300. align-items: stretch;
  301. justify-content: flex-end;
  302. }
  303. .drawer-mask.show { display: flex; }
  304. .drawer {
  305. width: min(420px, 100%);
  306. background: #fff;
  307. box-shadow: -8px 0 30px rgba(0, 0, 0, 0.12);
  308. display: flex;
  309. flex-direction: column;
  310. }
  311. .drawer-head {
  312. padding: 16px 18px;
  313. border-bottom: 1px solid var(--border);
  314. display: flex;
  315. align-items: center;
  316. gap: 10px;
  317. }
  318. .drawer-head h3 { margin: 0; font-size: 15px; flex: 1; }
  319. .drawer-body { padding: 18px; flex: 1; color: var(--text-muted); font-size: 13px; }
  320. .drawer-body dl { margin: 0; display: grid; gap: 10px; }
  321. .drawer-body dt { font-size: 11px; color: var(--text-muted); }
  322. .drawer-body dd { margin: 2px 0 0; color: var(--text); font-weight: 600; }
  323. .drawer-foot {
  324. padding: 12px 18px;
  325. border-top: 1px solid var(--border);
  326. display: flex;
  327. justify-content: flex-end;
  328. gap: 8px;
  329. }
  330. .btn-lg { height: 36px; padding: 0 14px; font-size: 13px; }
  331. .btn-main {
  332. background: var(--primary);
  333. border-color: var(--primary);
  334. color: #fff;
  335. }
  336. .btn-main:hover { background: var(--primary-dark); }
  337. </style>
  338. </head>
  339. <body>
  340. <div class="page-head">
  341. <h1>OA「抄送我的」未读角标 · Demo</h1>
  342. <p>侧栏数字对齐「质检工单」红色文字角标;列表支持「已读」,点「查看」默认标记已读。</p>
  343. </div>
  344. <div class="shell-wrap">
  345. <div class="shell">
  346. <aside class="sidebar">
  347. <div class="logo">陇耘<span>鲜达</span></div>
  348. <div class="nav-group">质量检测(参照)</div>
  349. <div class="nav-item parent">
  350. <span class="menu-name">质检工单</span>
  351. <span class="art-text-badge">12</span>
  352. </div>
  353. <div class="nav-group">OA审批</div>
  354. <div class="nav-children">
  355. <button class="nav-item" type="button">待处理</button>
  356. <button class="nav-item" type="button">已处理</button>
  357. <button class="nav-item" type="button">我发起的</button>
  358. <button class="nav-item active" type="button" id="nav-cc">
  359. <span class="menu-name">抄送我的</span>
  360. <span class="art-text-badge" id="menu-badge">3</span>
  361. </button>
  362. <button class="nav-item" type="button">草稿箱</button>
  363. </div>
  364. </aside>
  365. <main class="main">
  366. <section class="content-card">
  367. <div class="panel-top">
  368. <h2>抄送我的</h2>
  369. <div class="panel-meta">未读 <strong id="unread-count">3</strong> 条 · 可点操作栏按钮体验</div>
  370. </div>
  371. <div class="list-body">
  372. <table>
  373. <thead>
  374. <tr>
  375. <th style="min-width:68px">序号</th>
  376. <th style="min-width:160px">流程名称</th>
  377. <th style="min-width:100px">申请人</th>
  378. <th style="min-width:150px">申请时间</th>
  379. <th style="min-width:110px">当前节点</th>
  380. <th style="min-width:100px">流程状态</th>
  381. <th style="min-width:90px">阅读状态</th>
  382. <th style="min-width:280px">操作</th>
  383. </tr>
  384. </thead>
  385. <tbody id="tbody"></tbody>
  386. </table>
  387. </div>
  388. </section>
  389. </main>
  390. </div>
  391. </div>
  392. <div class="ref-row">
  393. <div class="ref-card">
  394. <h3>参照:质检工单菜单角标</h3>
  395. <div class="ref-menu">
  396. <span>质检工单</span>
  397. <span class="spacer"></span>
  398. <span class="art-text-badge">12</span>
  399. </div>
  400. <p class="legend">红色小块数字 · 0 时灰底(本 Demo 未读为 0 时隐藏角标,也可改成灰「0」)</p>
  401. </div>
  402. <div class="ref-card">
  403. <h3>本需求:抄送我的</h3>
  404. <div class="ref-menu">
  405. <span>抄送我的</span>
  406. <span class="spacer"></span>
  407. <span class="art-text-badge" id="ref-badge">3</span>
  408. </div>
  409. <p class="legend">统计「抄送给我且尚未阅读」的记录数;列表操作可消减</p>
  410. </div>
  411. </div>
  412. <div class="note">
  413. <strong>交互约定(Demo 可点):</strong>
  414. <ul>
  415. <li><strong>已读</strong>:仅标记已读,不打开详情;按钮在已读后禁用。</li>
  416. <li><strong>查看</strong>:打开详情示意抽屉,同时默认标记为已读,侧栏数字同步减少。</li>
  417. <li>未读行:流程名称前有红色圆点;阅读状态列显示「未读 / 已读」Tag。</li>
  418. </ul>
  419. </div>
  420. <div class="drawer-mask" id="drawer">
  421. <div class="drawer">
  422. <div class="drawer-head">
  423. <h3 id="drawer-title">审批详情</h3>
  424. <button class="btn btn-lg" type="button" id="drawer-close">关闭</button>
  425. </div>
  426. <div class="drawer-body">
  427. <dl>
  428. <div>
  429. <dt>流程名称</dt>
  430. <dd id="d-name">-</dd>
  431. </div>
  432. <div>
  433. <dt>申请人</dt>
  434. <dd id="d-user">-</dd>
  435. </div>
  436. <div>
  437. <dt>申请时间</dt>
  438. <dd id="d-time">-</dd>
  439. </div>
  440. <div>
  441. <dt>说明</dt>
  442. <dd style="font-weight:500;color:var(--text-muted)">打开详情即视为已读(与需求一致)。</dd>
  443. </div>
  444. </dl>
  445. </div>
  446. <div class="drawer-foot">
  447. <button class="btn btn-lg btn-main" type="button" id="drawer-ok">知道了</button>
  448. </div>
  449. </div>
  450. </div>
  451. <div class="toast" id="toast"></div>
  452. <script>
  453. const rows = [
  454. { id: '1', name: '请假申请', user: '张三', time: '2026-08-05 09:12', node: '部门负责人', status: 'running', read: false },
  455. { id: '2', name: '费用报销', user: '李四', time: '2026-08-04 16:40', node: '财务审核', status: 'running', read: false },
  456. { id: '3', name: '出差申请', user: '王五', time: '2026-08-03 11:05', node: '结束', status: 'done', read: false },
  457. { id: '4', name: '用章申请', user: '赵六', time: '2026-08-01 14:22', node: '结束', status: 'done', read: true },
  458. { id: '5', name: '采购申请', user: '钱七', time: '2026-07-28 10:08', node: '总经理', status: 'running', read: true }
  459. ]
  460. const tbody = document.getElementById('tbody')
  461. const menuBadge = document.getElementById('menu-badge')
  462. const refBadge = document.getElementById('ref-badge')
  463. const unreadCountEl = document.getElementById('unread-count')
  464. const toastEl = document.getElementById('toast')
  465. const drawer = document.getElementById('drawer')
  466. function unreadCount() {
  467. return rows.filter((r) => !r.read).length
  468. }
  469. function formatBadge(n) {
  470. if (n <= 0) return ''
  471. return n > 99 ? '99+' : String(n)
  472. }
  473. function syncBadge() {
  474. const n = unreadCount()
  475. const text = formatBadge(n)
  476. unreadCountEl.textContent = String(n)
  477. ;[menuBadge, refBadge].forEach((el) => {
  478. if (!text) {
  479. el.style.display = 'none'
  480. el.textContent = ''
  481. } else {
  482. el.style.display = ''
  483. el.textContent = text
  484. el.classList.toggle('is-zero', false)
  485. }
  486. })
  487. }
  488. function toast(msg) {
  489. toastEl.textContent = msg
  490. toastEl.classList.add('show')
  491. clearTimeout(toast._t)
  492. toast._t = setTimeout(() => toastEl.classList.remove('show'), 1600)
  493. }
  494. function markRead(id, source) {
  495. const row = rows.find((r) => r.id === id)
  496. if (!row || row.read) return false
  497. row.read = true
  498. render()
  499. toast(source === 'view' ? '已查看,标记为已读' : '已标记为已读')
  500. return true
  501. }
  502. function openDetail(id) {
  503. const row = rows.find((r) => r.id === id)
  504. if (!row) return
  505. markRead(id, 'view')
  506. document.getElementById('drawer-title').textContent = '抄送详情'
  507. document.getElementById('d-name').textContent = row.name
  508. document.getElementById('d-user').textContent = row.user
  509. document.getElementById('d-time').textContent = row.time
  510. drawer.classList.add('show')
  511. }
  512. function closeDrawer() {
  513. drawer.classList.remove('show')
  514. }
  515. function render() {
  516. tbody.innerHTML = rows
  517. .map((r, i) => {
  518. const statusTag =
  519. r.status === 'running'
  520. ? '<span class="tag tag-running">进行中</span>'
  521. : '<span class="tag tag-done">已完成</span>'
  522. const readTag = r.read
  523. ? '<span class="tag tag-read">已读</span>'
  524. : '<span class="tag tag-unread">未读</span>'
  525. const title = r.read
  526. ? `<span class="title-cell">${r.name}</span>`
  527. : `<span class="title-cell"><span class="dot-unread"></span>${r.name}</span>`
  528. return `<tr class="${r.read ? '' : 'unread'}" data-id="${r.id}">
  529. <td>${i + 1}</td>
  530. <td>${title}</td>
  531. <td>${r.user}</td>
  532. <td>${r.time}</td>
  533. <td>${r.node}</td>
  534. <td>${statusTag}</td>
  535. <td>${readTag}</td>
  536. <td>
  537. <div class="ops">
  538. <button class="btn btn-primary" data-act="view" data-id="${r.id}" type="button">👁 查看</button>
  539. <button class="btn" data-act="read" data-id="${r.id}" type="button" ${r.read ? 'disabled' : ''}>✓ 已读</button>
  540. <button class="btn" type="button" disabled>🖨 打印</button>
  541. </div>
  542. </td>
  543. </tr>`
  544. })
  545. .join('')
  546. syncBadge()
  547. }
  548. tbody.addEventListener('click', (e) => {
  549. const btn = e.target.closest('button[data-act]')
  550. if (!btn) return
  551. const id = btn.getAttribute('data-id')
  552. const act = btn.getAttribute('data-act')
  553. if (act === 'read') markRead(id, 'button')
  554. if (act === 'view') openDetail(id)
  555. })
  556. document.getElementById('drawer-close').addEventListener('click', closeDrawer)
  557. document.getElementById('drawer-ok').addEventListener('click', closeDrawer)
  558. drawer.addEventListener('click', (e) => {
  559. if (e.target === drawer) closeDrawer()
  560. })
  561. render()
  562. </script>
  563. </body>
  564. </html>