oa-approval-task-filter-demo.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  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 rel="preconnect" href="https://fonts.googleapis.com" />
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  10. <style>
  11. :root {
  12. --primary: #10b981;
  13. --primary-dark: #059669;
  14. --primary-bg: #ecfdf5;
  15. --text: #1e293b;
  16. --text-muted: #64748b;
  17. --border: #d9e8e2;
  18. --bg-page: #fefefe;
  19. --surface: #ffffff;
  20. --table-head: #f8fafc;
  21. --row-alt: #fafafa;
  22. --row-hover: #f1f5f9;
  23. --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  24. --radius: 10px;
  25. --placeholder: #94a3b8;
  26. }
  27. * { box-sizing: border-box; }
  28. body {
  29. margin: 0;
  30. min-height: 100vh;
  31. font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  32. font-size: 14px;
  33. line-height: 1.5;
  34. color: var(--text);
  35. background: #f1f5f9;
  36. }
  37. .page {
  38. max-width: 1280px;
  39. margin: 0 auto;
  40. padding: 24px;
  41. }
  42. .demo-banner {
  43. margin-bottom: 16px;
  44. padding: 12px 16px;
  45. border: 1px solid var(--border);
  46. border-radius: 8px;
  47. background: var(--primary-bg);
  48. color: var(--primary-dark);
  49. font-size: 13px;
  50. }
  51. .demo-banner strong { font-weight: 600; }
  52. .live-toast {
  53. position: fixed;
  54. right: 24px;
  55. bottom: 24px;
  56. z-index: 50;
  57. min-width: 220px;
  58. padding: 10px 14px;
  59. border-radius: 8px;
  60. border: 1px solid #a7f3d0;
  61. background: #fff;
  62. box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
  63. color: var(--text);
  64. font-size: 13px;
  65. opacity: 0;
  66. transform: translateY(8px);
  67. pointer-events: none;
  68. transition: opacity 0.2s ease, transform 0.2s ease;
  69. }
  70. .live-toast.show {
  71. opacity: 1;
  72. transform: translateY(0);
  73. }
  74. .live-toast .muted { color: var(--text-muted); font-size: 12px; margin-top: 2px; }
  75. .page-tabs {
  76. display: flex;
  77. flex-wrap: wrap;
  78. gap: 8px;
  79. margin-bottom: 16px;
  80. }
  81. .page-tab {
  82. height: 36px;
  83. padding: 0 14px;
  84. border: 1px solid var(--border);
  85. border-radius: 8px;
  86. background: var(--surface);
  87. color: var(--text-muted);
  88. font-size: 13px;
  89. font-weight: 500;
  90. cursor: pointer;
  91. font-family: inherit;
  92. }
  93. .page-tab:hover { color: var(--primary-dark); border-color: #a7f3d0; }
  94. .page-tab.active {
  95. background: var(--primary);
  96. border-color: var(--primary);
  97. color: #fff;
  98. font-weight: 600;
  99. }
  100. .panel {
  101. background: var(--surface);
  102. border: 1px solid var(--border);
  103. border-radius: var(--radius);
  104. box-shadow: var(--shadow);
  105. overflow: hidden;
  106. }
  107. .panel-top {
  108. display: flex;
  109. align-items: center;
  110. padding: 14px 16px;
  111. border-bottom: 1px solid var(--border);
  112. }
  113. .panel-top h2 {
  114. margin: 0;
  115. font-size: 16px;
  116. font-weight: 600;
  117. color: var(--text);
  118. display: flex;
  119. align-items: center;
  120. gap: 8px;
  121. }
  122. .panel-top h2::before {
  123. content: "";
  124. width: 3px;
  125. height: 16px;
  126. border-radius: 2px;
  127. background: var(--primary);
  128. flex-shrink: 0;
  129. }
  130. /* —— 筛选区(对齐 Fresh Mint §5 / unit / order / 抄送我的) —— */
  131. .filter-block {
  132. padding: 12px 16px 10px;
  133. border-bottom: 1px solid var(--border);
  134. background: var(--surface);
  135. }
  136. .filter-form { margin: 0; }
  137. .filter-grid {
  138. display: grid;
  139. grid-template-columns: repeat(4, minmax(0, 1fr));
  140. gap: 10px 14px;
  141. margin-bottom: 10px;
  142. }
  143. @media (max-width: 1100px) {
  144. .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  145. }
  146. @media (max-width: 560px) {
  147. .filter-grid { grid-template-columns: 1fr; }
  148. }
  149. .filter-field label {
  150. display: block;
  151. font-size: 11px;
  152. font-weight: 500;
  153. color: var(--text-muted);
  154. margin-bottom: 4px;
  155. }
  156. .input, .select {
  157. width: 100%;
  158. height: 36px;
  159. padding: 0 12px;
  160. border: 1px solid var(--border);
  161. border-radius: 8px;
  162. font-size: 14px;
  163. color: var(--text);
  164. background: var(--surface);
  165. font-family: inherit;
  166. outline: none;
  167. }
  168. .input::placeholder {
  169. color: var(--placeholder);
  170. font-size: 13px;
  171. opacity: 1;
  172. }
  173. .input:focus, .select:focus {
  174. border-color: var(--primary);
  175. box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
  176. }
  177. .select {
  178. appearance: none;
  179. padding-right: 32px;
  180. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M3 4.5L6 8l3-3.5'/%3E%3C/svg%3E");
  181. background-repeat: no-repeat;
  182. background-position: right 12px center;
  183. }
  184. .date-range {
  185. display: flex;
  186. align-items: center;
  187. gap: 8px;
  188. min-width: 0;
  189. }
  190. .date-range .input { flex: 1; min-width: 0; }
  191. .date-range .sep {
  192. color: var(--text-muted);
  193. font-size: 12px;
  194. flex-shrink: 0;
  195. }
  196. .filter-actions-row {
  197. display: flex;
  198. justify-content: flex-end;
  199. align-items: center;
  200. padding-top: 10px;
  201. margin-top: 0;
  202. }
  203. .filter-actions-end {
  204. display: flex;
  205. gap: 8px;
  206. align-items: center;
  207. }
  208. .btn {
  209. display: inline-flex;
  210. align-items: center;
  211. justify-content: center;
  212. gap: 6px;
  213. height: 36px;
  214. padding: 0 14px;
  215. border-radius: 8px;
  216. font-size: 14px;
  217. font-weight: 600;
  218. font-family: inherit;
  219. cursor: pointer;
  220. border: 1px solid transparent;
  221. }
  222. .btn-ghost {
  223. background: var(--surface);
  224. border-color: var(--border);
  225. color: var(--text);
  226. }
  227. .btn-ghost:hover { background: var(--row-hover); }
  228. .btn-main {
  229. background: var(--primary);
  230. border-color: var(--primary);
  231. color: #fff;
  232. }
  233. .btn-main:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
  234. .btn svg { width: 14px; height: 14px; flex-shrink: 0; }
  235. .list-body {
  236. border-top: none;
  237. }
  238. .list-wrap { padding: 0; }
  239. table.list-table {
  240. width: 100%;
  241. border-collapse: collapse;
  242. font-size: 13px;
  243. }
  244. .list-table th,
  245. .list-table td {
  246. padding: 12px 10px;
  247. text-align: center;
  248. border-bottom: 1px solid #eef2f7;
  249. color: var(--text);
  250. }
  251. .list-table th {
  252. background: var(--table-head);
  253. color: var(--text-muted);
  254. font-weight: 600;
  255. font-size: 12px;
  256. }
  257. .list-table tbody tr:nth-child(even) { background: var(--row-alt); }
  258. .list-table tbody tr:hover { background: var(--row-hover); }
  259. .tag {
  260. display: inline-flex;
  261. align-items: center;
  262. height: 22px;
  263. padding: 0 8px;
  264. border-radius: 4px;
  265. font-size: 12px;
  266. font-weight: 500;
  267. }
  268. .tag.warning { background: #fff7ed; color: #c2410c; }
  269. .tag.success { background: var(--primary-bg); color: var(--primary-dark); }
  270. .tag.danger { background: #fef2f2; color: #dc2626; }
  271. .tag.info { background: #f1f5f9; color: #64748b; }
  272. .btn-op {
  273. display: inline-flex;
  274. align-items: center;
  275. gap: 4px;
  276. height: 28px;
  277. padding: 0 10px;
  278. border-radius: 6px;
  279. border: 1px solid transparent;
  280. font-size: 12px;
  281. font-weight: 500;
  282. cursor: pointer;
  283. font-family: inherit;
  284. }
  285. .btn-op.primary {
  286. background: var(--primary-bg);
  287. color: var(--primary-dark);
  288. border-color: #a7f3d0;
  289. }
  290. .btn-op.neutral {
  291. background: var(--surface);
  292. color: var(--text);
  293. border-color: var(--border);
  294. }
  295. .pagination {
  296. display: flex;
  297. justify-content: flex-end;
  298. align-items: center;
  299. gap: 8px;
  300. padding: 12px 16px;
  301. border-top: 1px solid var(--border);
  302. color: var(--text-muted);
  303. font-size: 13px;
  304. }
  305. .pager-btn {
  306. height: 28px;
  307. min-width: 28px;
  308. padding: 0 8px;
  309. border: 1px solid var(--border);
  310. border-radius: 6px;
  311. background: var(--surface);
  312. color: var(--text);
  313. cursor: pointer;
  314. font-family: inherit;
  315. }
  316. .pager-btn.active {
  317. background: var(--primary);
  318. border-color: var(--primary);
  319. color: #fff;
  320. }
  321. .field-note {
  322. margin-top: 12px;
  323. padding: 10px 12px;
  324. border-radius: 8px;
  325. background: #f8fafc;
  326. border: 1px dashed var(--border);
  327. font-size: 12px;
  328. color: var(--text-muted);
  329. }
  330. .field-note code {
  331. font-size: 11px;
  332. background: #e2e8f0;
  333. padding: 1px 5px;
  334. border-radius: 4px;
  335. color: var(--text);
  336. }
  337. [data-only] { display: none; }
  338. </style>
  339. </head>
  340. <body>
  341. <div class="page">
  342. <div class="demo-banner">
  343. <strong>OA 审批 · 任务中心检索区 Demo</strong>
  344. —— 样式对齐 Fresh Mint;交互为<strong>即时检索</strong>:输入/选择条件后防抖约 300ms 自动请求,无需点「搜索」。试着改下方任一条件即可看到触发提示。
  345. </div>
  346. <div class="page-tabs" role="tablist">
  347. <button type="button" class="page-tab active" data-tab="pending">待办中心</button>
  348. <button type="button" class="page-tab" data-tab="done">已办中心</button>
  349. <button type="button" class="page-tab" data-tab="mine">我发起的</button>
  350. <button type="button" class="page-tab" data-tab="cc">抄送我的</button>
  351. </div>
  352. <section class="panel">
  353. <div class="panel-top">
  354. <h2 id="panelTitle">待办中心</h2>
  355. </div>
  356. <div class="filter-block">
  357. <form class="filter-form" onsubmit="return false;">
  358. <div class="filter-grid">
  359. <div class="filter-field">
  360. <label for="appName">流程名称</label>
  361. <input id="appName" class="input" type="text" placeholder="请输入流程名称" />
  362. </div>
  363. <div class="filter-field" data-only="pending,done,cc">
  364. <label for="initiator">申请人</label>
  365. <input id="initiator" class="input" type="text" placeholder="请输入申请人" />
  366. </div>
  367. <div class="filter-field">
  368. <label>申请时间</label>
  369. <div class="date-range">
  370. <input class="input" type="date" aria-label="申请开始日期" />
  371. <span class="sep">至</span>
  372. <input class="input" type="date" aria-label="申请结束日期" />
  373. </div>
  374. </div>
  375. <div class="filter-field">
  376. <label for="status">流程状态</label>
  377. <select id="status" class="select">
  378. <option value="">全部</option>
  379. <option value="running">进行中</option>
  380. <option value="approved">已同意</option>
  381. <option value="rejected">已拒绝</option>
  382. <option value="returned">已打回</option>
  383. <option value="cancelled">已撤销</option>
  384. <option value="completed">已完成</option>
  385. </select>
  386. </div>
  387. <div class="filter-field" data-only="cc">
  388. <label for="readStatus">阅读状态</label>
  389. <select id="readStatus" class="select">
  390. <option value="unread">未读</option>
  391. <option value="read">已读</option>
  392. <option value="all">全部</option>
  393. </select>
  394. </div>
  395. </div>
  396. <div class="filter-actions-row">
  397. <div class="filter-actions-end">
  398. <button type="button" class="btn btn-ghost" id="btnReset">
  399. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9"/><path d="M3 4v5h5"/></svg>
  400. 重置
  401. </button>
  402. <button type="button" class="btn btn-main" id="btnSearch">
  403. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
  404. 搜索
  405. </button>
  406. </div>
  407. </div>
  408. </form>
  409. <div class="field-note" id="fieldNote">
  410. 交互:条件变更 → 防抖 300ms → 自动检索(页码回第 1 页)。本页字段:<code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>
  411. </div>
  412. </div>
  413. <div class="list-body list-wrap">
  414. <table class="list-table">
  415. <thead>
  416. <tr>
  417. <th style="width:68px">序号</th>
  418. <th>流程名称</th>
  419. <th>申请人</th>
  420. <th>申请时间</th>
  421. <th>当前节点</th>
  422. <th>当前审批人</th>
  423. <th>流程状态</th>
  424. <th data-only="cc">阅读状态</th>
  425. <th style="min-width:120px">操作</th>
  426. </tr>
  427. </thead>
  428. <tbody>
  429. <tr>
  430. <td>1</td>
  431. <td>请假审批</td>
  432. <td>张三</td>
  433. <td>2026-08-10 09:20:00</td>
  434. <td>部门负责人</td>
  435. <td>李四</td>
  436. <td><span class="tag warning">进行中</span></td>
  437. <td data-only="cc"><span class="tag danger">未读</span></td>
  438. <td><button type="button" class="btn-op primary">查看</button></td>
  439. </tr>
  440. <tr>
  441. <td>2</td>
  442. <td>加班申请</td>
  443. <td>王五</td>
  444. <td>2026-08-09 18:05:00</td>
  445. <td>人事复核</td>
  446. <td>赵六</td>
  447. <td><span class="tag warning">进行中</span></td>
  448. <td data-only="cc"><span class="tag info">已读</span></td>
  449. <td><button type="button" class="btn-op primary">查看</button></td>
  450. </tr>
  451. <tr>
  452. <td>3</td>
  453. <td>差旅报销</td>
  454. <td>陈七</td>
  455. <td>2026-08-08 11:30:00</td>
  456. <td>—</td>
  457. <td>—</td>
  458. <td><span class="tag success">已同意</span></td>
  459. <td data-only="cc"><span class="tag info">已读</span></td>
  460. <td>
  461. <button type="button" class="btn-op primary">查看</button>
  462. <button type="button" class="btn-op neutral" style="margin-left:6px">打印</button>
  463. </td>
  464. </tr>
  465. </tbody>
  466. </table>
  467. <div class="pagination">
  468. <span>共 3 条</span>
  469. <button type="button" class="pager-btn">10条/页</button>
  470. <button type="button" class="pager-btn">‹</button>
  471. <button type="button" class="pager-btn active">1</button>
  472. <button type="button" class="pager-btn">›</button>
  473. </div>
  474. </div>
  475. </section>
  476. </div>
  477. <div class="live-toast" id="liveToast" aria-live="polite">
  478. <div><strong>已触发即时检索</strong></div>
  479. <div class="muted" id="liveToastDetail">防抖 300ms · 页码重置为 1</div>
  480. </div>
  481. <script>
  482. const titles = {
  483. pending: '待办中心',
  484. done: '已办中心',
  485. mine: '我发起的',
  486. cc: '抄送我的'
  487. };
  488. const notes = {
  489. pending: '交互:条件变更 → 防抖 300ms → 自动检索。字段:<code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>',
  490. done: '交互:条件变更 → 防抖 300ms → 自动检索。字段:<code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>',
  491. mine: '交互:条件变更 → 防抖 300ms → 自动检索。字段:<code>流程名称</code>· <code>申请时间</code>· <code>流程状态</code>(不放申请人)',
  492. cc: '交互:条件变更 → 防抖 300ms → 自动检索。字段:现有 <code>阅读状态</code> + <code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>'
  493. };
  494. let toastTimer = null;
  495. let debounceTimer = null;
  496. let searchSeq = 0;
  497. function showLiveSearch(reason) {
  498. searchSeq += 1;
  499. const el = document.getElementById('liveToast');
  500. const detail = document.getElementById('liveToastDetail');
  501. detail.textContent = reason + ' · 防抖 300ms · 页码重置为 1 · #' + searchSeq;
  502. el.classList.add('show');
  503. clearTimeout(toastTimer);
  504. toastTimer = setTimeout(() => el.classList.remove('show'), 1600);
  505. }
  506. function scheduleLiveSearch(reason) {
  507. clearTimeout(debounceTimer);
  508. debounceTimer = setTimeout(() => showLiveSearch(reason), 300);
  509. }
  510. function applyTab(tab) {
  511. document.getElementById('panelTitle').textContent = titles[tab];
  512. document.getElementById('fieldNote').innerHTML = notes[tab];
  513. document.querySelectorAll('[data-only]').forEach((el) => {
  514. const keys = el.getAttribute('data-only').split(',');
  515. const show = keys.includes(tab);
  516. if (el.tagName === 'TH' || el.tagName === 'TD') {
  517. el.style.display = show ? 'table-cell' : 'none';
  518. } else {
  519. el.style.display = show ? '' : 'none';
  520. }
  521. });
  522. }
  523. document.querySelectorAll('.page-tab').forEach((btn) => {
  524. btn.addEventListener('click', () => {
  525. document.querySelectorAll('.page-tab').forEach((b) => b.classList.remove('active'));
  526. btn.classList.add('active');
  527. applyTab(btn.dataset.tab);
  528. });
  529. });
  530. document.querySelectorAll('.filter-form .input, .filter-form .select').forEach((el) => {
  531. const evt = el.tagName === 'SELECT' || el.type === 'date' ? 'change' : 'input';
  532. el.addEventListener(evt, () => {
  533. const label = el.closest('.filter-field')?.querySelector('label')?.textContent || '条件';
  534. scheduleLiveSearch('变更「' + label + '」');
  535. });
  536. });
  537. document.getElementById('btnReset').addEventListener('click', () => {
  538. document.querySelectorAll('.filter-form .input').forEach((el) => { el.value = ''; });
  539. document.querySelectorAll('.filter-form .select').forEach((el) => {
  540. el.selectedIndex = 0;
  541. });
  542. scheduleLiveSearch('重置条件');
  543. });
  544. document.getElementById('btnSearch').addEventListener('click', () => {
  545. clearTimeout(debounceTimer);
  546. showLiveSearch('点击「搜索」立即检索');
  547. });
  548. applyTab('pending');
  549. </script>
  550. </body>
  551. </html>