zkxw-mobile-oa-pending-batch-actions-demo.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  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, maximum-scale=1" />
  6. <title>待办中心 · 批量通过 / 打回 Demo</title>
  7. <style>
  8. :root {
  9. --brand: #0a9f6d;
  10. --brand-dark: #087f57;
  11. --brand-soft: #e9f9f2;
  12. --warn: #d97706;
  13. --warn-soft: #fff7e8;
  14. --danger: #ef4444;
  15. --danger-soft: #fef2f2;
  16. --nav-bg: #ecfdf5;
  17. --page-bg: #f5f8f7;
  18. --text: #1f2a26;
  19. --text-muted: #6b7b74;
  20. --border: #e8eaed;
  21. }
  22. * { box-sizing: border-box; }
  23. body {
  24. margin: 0;
  25. font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
  26. color: var(--text);
  27. background:
  28. radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
  29. linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
  30. line-height: 1.45;
  31. }
  32. .stage { max-width: 1180px; margin: 0 auto; padding: 28px 16px 64px; }
  33. .hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
  34. .hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 820px; }
  35. .rec {
  36. margin: 16px 0 0;
  37. padding: 14px 16px;
  38. border-radius: 14px;
  39. background: var(--brand-soft);
  40. border: 1px solid #b7ebd4;
  41. font-size: 13px;
  42. color: #166534;
  43. line-height: 1.55;
  44. }
  45. .rec b { color: var(--brand-dark); }
  46. .tabs {
  47. display: flex; flex-wrap: wrap; gap: 10px;
  48. margin: 20px 0 18px;
  49. }
  50. .tab {
  51. appearance: none; border: 1px solid #e2e8f0; background: #fff;
  52. border-radius: 999px; padding: 10px 16px; font-size: 13px;
  53. font-weight: 600; color: #475569; cursor: pointer;
  54. }
  55. .tab.is-active {
  56. color: var(--brand-dark); border-color: var(--brand);
  57. background: var(--brand-soft);
  58. box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.16);
  59. }
  60. .layout {
  61. display: grid;
  62. grid-template-columns: minmax(240px, 300px) 1fr;
  63. gap: 20px; align-items: start;
  64. }
  65. @media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
  66. .panel {
  67. background: rgba(255,255,255,.94);
  68. border: 1px solid #e2e8f0; border-radius: 16px; padding: 16px;
  69. box-shadow: 0 10px 28px rgba(15,23,42,.05);
  70. }
  71. .panel h2 { margin: 0 0 4px; font-size: 15px; }
  72. .panel .sub { margin: 0 0 10px; font-size: 12px; color: #64748b; }
  73. .panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
  74. .panel li { margin: 6px 0; }
  75. .panel .pick {
  76. margin-top: 12px; padding: 10px 12px; border-radius: 10px;
  77. background: #fff7e8; border: 1px solid #fde68a;
  78. font-size: 12px; color: #92400e;
  79. }
  80. .panel .ok {
  81. margin-top: 10px; padding: 10px 12px; border-radius: 10px;
  82. background: #ecfdf5; border: 1px solid #a7f3d0;
  83. font-size: 12px; color: #047857;
  84. }
  85. .phones { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
  86. .phone {
  87. width: 375px; background: #fff; border-radius: 28px; overflow: hidden;
  88. box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
  89. position: relative;
  90. }
  91. .cap {
  92. text-align: center; font-size: 11px; font-weight: 650; color: #64748b;
  93. padding: 8px; background: #f8fafc; border-bottom: 1px solid #eef2f0;
  94. }
  95. .nav {
  96. height: 44px; display: flex; align-items: center; padding: 0 8px 0 4px;
  97. background: var(--nav-bg); border-bottom: 1px solid #d8f3e5;
  98. position: relative;
  99. }
  100. .nav-back {
  101. width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
  102. color: #0f291e; flex-shrink: 0;
  103. }
  104. .nav-back::before {
  105. content: ""; width: 10px; height: 10px;
  106. border-left: 2px solid #0f291e; border-bottom: 2px solid #0f291e;
  107. transform: rotate(45deg); margin-left: 4px;
  108. }
  109. .nav-title {
  110. position: absolute; left: 50%; transform: translateX(-50%);
  111. font-size: 16px; font-weight: 650; color: #0f291e;
  112. }
  113. .nav-action {
  114. margin-left: auto;
  115. min-width: 48px;
  116. height: 36px;
  117. padding: 0 10px;
  118. display: inline-flex;
  119. align-items: center;
  120. justify-content: center;
  121. font-size: 14px;
  122. font-weight: 650;
  123. color: var(--brand-dark);
  124. cursor: pointer;
  125. user-select: none;
  126. }
  127. .nav-action.is-done { color: #64748b; }
  128. .nav-action:active { opacity: 0.72; }
  129. .page {
  130. min-height: 640px;
  131. background: var(--page-bg);
  132. padding-bottom: 24px;
  133. position: relative;
  134. }
  135. .toolbar {
  136. padding: 10px 12px;
  137. background: #fff;
  138. border-bottom: 1px solid #f1f5f9;
  139. }
  140. .search-box {
  141. height: 36px;
  142. display: flex; align-items: center; gap: 8px;
  143. padding: 0 10px; border-radius: 10px;
  144. background: #f1f5f9; border: 1px solid transparent;
  145. }
  146. .search-ico {
  147. width: 13px; height: 13px; border: 1.5px solid #94a3b8; border-radius: 50%;
  148. position: relative; flex-shrink: 0;
  149. }
  150. .search-ico::after {
  151. content: ""; position: absolute; width: 6px; height: 1.5px;
  152. right: -4px; bottom: -2px; background: #94a3b8; border-radius: 2px;
  153. transform: rotate(45deg);
  154. }
  155. .search-ph { font-size: 13px; color: #b0b7c3; }
  156. .batch-bar {
  157. display: none;
  158. align-items: center; justify-content: space-between; gap: 8px;
  159. margin: 8px 12px 0; padding: 10px 12px;
  160. border-radius: 10px; background: #fff;
  161. border: 1px solid var(--border);
  162. box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
  163. }
  164. .batch-bar.is-show { display: flex; }
  165. .batch-bar__hint { font-size: 13px; color: #646a73; flex-shrink: 0; }
  166. .batch-bar__actions { display: flex; gap: 8px; margin-left: auto; }
  167. .batch-bar__btn {
  168. min-height: 32px; padding: 0 14px;
  169. display: inline-flex; align-items: center; justify-content: center;
  170. border-radius: 8px; font-size: 13px; font-weight: 600;
  171. border: none; cursor: pointer;
  172. }
  173. .batch-bar__btn.is-ok {
  174. color: #fff; background: var(--brand);
  175. }
  176. .batch-bar__btn.is-return {
  177. color: var(--warn); background: var(--warn-soft);
  178. border: 1px solid #fde68a;
  179. }
  180. .batch-bar__btn:disabled {
  181. opacity: 0.45; cursor: not-allowed;
  182. }
  183. .list {
  184. padding: 10px 12px 0;
  185. display: flex; flex-direction: column; gap: 10px;
  186. }
  187. .row {
  188. display: flex; align-items: stretch; gap: 0;
  189. }
  190. .check {
  191. display: none; align-items: flex-start;
  192. padding: 18px 0 0 2px; margin-right: 6px; flex-shrink: 0;
  193. cursor: pointer;
  194. }
  195. .page.is-manage .check { display: flex; }
  196. .check__box {
  197. width: 20px; height: 20px; border-radius: 50%;
  198. border: 1.5px solid #cbd5e1; background: #fff;
  199. display: flex; align-items: center; justify-content: center;
  200. font-size: 12px; color: #fff; font-weight: 700;
  201. }
  202. .check__box.on {
  203. background: var(--brand); border-color: var(--brand);
  204. }
  205. .card {
  206. flex: 1; min-width: 0;
  207. background: #fff; border-radius: 14px;
  208. border: 1px solid #d8f3e5;
  209. box-shadow: 0 4px 16px rgba(22, 163, 74, 0.07);
  210. padding: 12px 14px; cursor: pointer;
  211. }
  212. .card.is-selected {
  213. border-color: #86efac;
  214. box-shadow: 0 0 0 2px rgba(10, 159, 109, 0.18);
  215. }
  216. .card__head {
  217. display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
  218. }
  219. .card__title {
  220. font-size: 15px; font-weight: 700; color: #0f291e; line-height: 1.35;
  221. }
  222. .card__time { flex-shrink: 0; font-size: 11px; color: #94a3b8; margin-top: 2px; }
  223. .card__meta { margin-top: 8px; }
  224. .card__meta-line {
  225. display: block; font-size: 12px; color: var(--text-muted); line-height: 1.5;
  226. white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  227. }
  228. .card__foot {
  229. margin-top: 10px; display: flex; align-items: center; justify-content: space-between;
  230. }
  231. .card__user { display: flex; align-items: center; gap: 6px; min-width: 0; }
  232. .card__avatar {
  233. width: 22px; height: 22px; border-radius: 6px;
  234. background: var(--brand); color: #fff;
  235. font-size: 10px; font-weight: 700;
  236. display: flex; align-items: center; justify-content: center;
  237. }
  238. .card__user-text { font-size: 12px; color: #64748b; }
  239. .card__status {
  240. font-size: 11px; font-weight: 650; padding: 3px 8px; border-radius: 999px;
  241. background: rgba(217, 119, 6, 0.12); color: var(--warn);
  242. }
  243. /* 底部意见弹层 */
  244. .sheet-mask {
  245. display: none; position: absolute; inset: 0;
  246. background: rgba(15, 23, 42, 0.45); z-index: 20;
  247. }
  248. .sheet-mask.is-show { display: block; }
  249. .sheet {
  250. position: absolute; left: 0; right: 0; bottom: 0; z-index: 21;
  251. background: #fff; border-radius: 16px 16px 0 0;
  252. padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px));
  253. transform: translateY(110%);
  254. transition: transform .22s ease;
  255. box-shadow: 0 -8px 28px rgba(15, 23, 42, 0.12);
  256. }
  257. .sheet.is-show { transform: translateY(0); }
  258. .sheet__hd {
  259. display: flex; align-items: center; justify-content: space-between;
  260. margin-bottom: 10px;
  261. }
  262. .sheet__title { font-size: 16px; font-weight: 700; }
  263. .sheet__close {
  264. border: none; background: transparent; color: #64748b;
  265. font-size: 13px; cursor: pointer; padding: 4px 0;
  266. }
  267. .sheet__hint {
  268. margin: 0 0 8px; font-size: 12px; color: #64748b;
  269. }
  270. .sheet__hint strong { color: var(--brand-dark); }
  271. .sheet__ta {
  272. width: 100%; min-height: 96px; resize: none;
  273. border: 1px solid #e2e8f0; border-radius: 10px;
  274. padding: 10px 12px; font: inherit; font-size: 14px;
  275. color: var(--text); outline: none;
  276. background: #f8fafc;
  277. }
  278. .sheet__ta:focus {
  279. border-color: #86efac; background: #fff;
  280. box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.12);
  281. }
  282. .sheet__ft {
  283. margin-top: 12px; display: flex; gap: 10px;
  284. }
  285. .sheet__btn {
  286. flex: 1; height: 40px; border-radius: 10px;
  287. border: none; font-size: 15px; font-weight: 650; cursor: pointer;
  288. }
  289. .sheet__btn.is-cancel {
  290. background: #f1f5f9; color: #475569;
  291. }
  292. .sheet__btn.is-ok {
  293. background: var(--brand); color: #fff;
  294. }
  295. .sheet__btn.is-return {
  296. background: var(--warn); color: #fff;
  297. }
  298. .toast {
  299. position: absolute; left: 50%; bottom: 88px; z-index: 30;
  300. transform: translateX(-50%);
  301. padding: 8px 14px; border-radius: 8px;
  302. background: rgba(15, 23, 42, 0.88); color: #fff;
  303. font-size: 13px; white-space: nowrap;
  304. opacity: 0; pointer-events: none; transition: opacity .2s;
  305. }
  306. .toast.is-show { opacity: 1; }
  307. .legend {
  308. margin-top: 22px;
  309. display: grid;
  310. grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  311. gap: 12px;
  312. }
  313. .legend .box {
  314. background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
  315. padding: 12px 14px; font-size: 12px; color: #475569;
  316. }
  317. .legend h3 { margin: 0 0 6px; font-size: 13px; color: #0f291e; }
  318. </style>
  319. </head>
  320. <body>
  321. <div class="stage">
  322. <div class="hero">
  323. <h1>待办中心 · 批量通过 / 打回</h1>
  324. <p>
  325. 交互对齐抄送「管理 + 多选 + 底栏批量操作」。右侧手机可点击演示:进入管理 → 勾选 → 批量通过 / 打回 → 填写统一意见。
  326. </p>
  327. <div class="rec">
  328. <b>推荐方案(本 Demo):</b>
  329. <b>标题栏右侧「管理 / 完成」</b>(对齐草稿箱自定义顶栏);底栏「批量通过」「批量打回」;
  330. 统一意见框(通过默认「同意」可空,打回必填);前端串行调现有单条接口,汇总成功/失败。不含批量拒绝。
  331. </div>
  332. </div>
  333. <div class="tabs" id="sceneTabs">
  334. <button type="button" class="tab is-active" data-scene="browse">① 浏览态</button>
  335. <button type="button" class="tab" data-scene="manage">② 管理多选</button>
  336. <button type="button" class="tab" data-scene="approve">③ 批量通过弹层</button>
  337. <button type="button" class="tab" data-scene="return">④ 批量打回弹层</button>
  338. </div>
  339. <div class="layout">
  340. <aside class="panel" id="sidePanel">
  341. <h2>① 浏览态</h2>
  342. <p class="sub">日常查看 / 点卡片进详情审批</p>
  343. <ul>
  344. <li>标题栏右侧「管理」(对齐草稿箱自定义顶栏)</li>
  345. <li>搜索区仅保留检索,不再放管理按钮</li>
  346. <li>点卡片仍进入审批详情(webview)</li>
  347. </ul>
  348. <div class="pick">右侧手机可点标题栏「管理」亲自走一遍流程。</div>
  349. </aside>
  350. <div class="phones">
  351. <div class="phone">
  352. <div class="cap">可交互 Demo · 375px</div>
  353. <div class="nav">
  354. <div class="nav-back" aria-hidden="true"></div>
  355. <div class="nav-title">待办中心</div>
  356. <div class="nav-action" id="manageBtn">管理</div>
  357. </div>
  358. <div class="page" id="page">
  359. <div class="toolbar">
  360. <div class="search-box">
  361. <span class="search-ico" aria-hidden="true"></span>
  362. <span class="search-ph">搜索流程名称、申请人</span>
  363. </div>
  364. </div>
  365. <div class="batch-bar" id="batchBar">
  366. <span class="batch-bar__hint" id="batchHint">已选 0 项</span>
  367. <div class="batch-bar__actions">
  368. <button type="button" class="batch-bar__btn is-return" id="btnReturn">批量打回</button>
  369. <button type="button" class="batch-bar__btn is-ok" id="btnApprove">批量通过</button>
  370. </div>
  371. </div>
  372. <div class="list" id="list"></div>
  373. <div class="sheet-mask" id="sheetMask"></div>
  374. <div class="sheet" id="sheet">
  375. <div class="sheet__hd">
  376. <div class="sheet__title" id="sheetTitle">批量通过</div>
  377. <button type="button" class="sheet__close" id="sheetClose">取消</button>
  378. </div>
  379. <p class="sheet__hint" id="sheetHint">将对 <strong>2</strong> 条待办执行操作</p>
  380. <textarea class="sheet__ta" id="sheetInput" placeholder="审批意见"></textarea>
  381. <div class="sheet__ft">
  382. <button type="button" class="sheet__btn is-cancel" id="sheetCancel">取消</button>
  383. <button type="button" class="sheet__btn is-ok" id="sheetConfirm">确定</button>
  384. </div>
  385. </div>
  386. <div class="toast" id="toast"></div>
  387. </div>
  388. </div>
  389. </div>
  390. </div>
  391. <div class="legend">
  392. <div class="box">
  393. <h3>意见规则</h3>
  394. 批量通过:默认填入「同意」,可改,可空(与单条一致)。<br />
  395. 批量打回:意见必填,占位「请填写打回意见(必填)」。
  396. </div>
  397. <div class="box">
  398. <h3>接口策略</h3>
  399. 暂无批量 API:前端对选中 taskId 串行调用<br />
  400. <code>POST /tasks/{id}/approve</code>、<code>/return</code>。<br />
  401. 结束后 toast:「成功 n,失败 m」。
  402. </div>
  403. <div class="box">
  404. <h3>边界</h3>
  405. 仅当前页已加载列表可勾选;不包含「拒绝」。<br />
  406. 若流程关闭打回(allowReturn=false),单条失败计入失败数。
  407. </div>
  408. </div>
  409. </div>
  410. <script>
  411. const TASKS = [
  412. {
  413. id: 't1',
  414. title: '请假申请',
  415. time: '今天 09:12',
  416. meta: ['请假类型:事假', '请假天数:1'],
  417. user: '王品龙',
  418. avatar: '王品'
  419. },
  420. {
  421. id: 't2',
  422. title: '费用报销',
  423. time: '昨天 18:40',
  424. meta: ['报销金额:1280.00', '事由:差旅交通'],
  425. user: '吴家猛',
  426. avatar: '吴家'
  427. },
  428. {
  429. id: 't3',
  430. title: '加班申请',
  431. time: '08-10',
  432. meta: ['加班时长:3小时', '加班日期:2026-08-09'],
  433. user: '郑德禄',
  434. avatar: '郑德'
  435. },
  436. {
  437. id: 't4',
  438. title: '出差申请',
  439. time: '08-09',
  440. meta: ['目的地:合肥', '出差天数:2'],
  441. user: '李明',
  442. avatar: '李明'
  443. }
  444. ]
  445. const state = {
  446. manage: false,
  447. selected: new Set(),
  448. sheetKind: null // approve | return
  449. }
  450. const page = document.getElementById('page')
  451. const listEl = document.getElementById('list')
  452. const manageBtn = document.getElementById('manageBtn')
  453. const batchBar = document.getElementById('batchBar')
  454. const batchHint = document.getElementById('batchHint')
  455. const btnApprove = document.getElementById('btnApprove')
  456. const btnReturn = document.getElementById('btnReturn')
  457. const sheet = document.getElementById('sheet')
  458. const sheetMask = document.getElementById('sheetMask')
  459. const sheetTitle = document.getElementById('sheetTitle')
  460. const sheetHint = document.getElementById('sheetHint')
  461. const sheetInput = document.getElementById('sheetInput')
  462. const sheetConfirm = document.getElementById('sheetConfirm')
  463. const toastEl = document.getElementById('toast')
  464. const sidePanel = document.getElementById('sidePanel')
  465. const SCENE_COPY = {
  466. browse: {
  467. title: '① 浏览态',
  468. sub: '日常查看 / 点卡片进详情审批',
  469. items: [
  470. '标题栏右侧「管理」(对齐草稿箱自定义顶栏)',
  471. '搜索区仅保留检索,不再放管理按钮',
  472. '点卡片仍进入审批详情(webview)'
  473. ],
  474. tip: '右侧手机可点标题栏「管理」亲自走一遍流程。',
  475. tipClass: 'pick'
  476. },
  477. manage: {
  478. title: '② 管理多选',
  479. sub: '勾选多条待办,底栏出现批量操作',
  480. items: [
  481. '标题栏按钮变为「完成」,点按退出并清空勾选',
  482. '左侧圆形勾选,选中卡片高亮描边',
  483. '底栏:已选 N 项 + 批量打回 + 批量通过',
  484. '未勾选时两个按钮禁用'
  485. ],
  486. tip: '主行动为「批量通过」(品牌绿);打回为次要警告色。',
  487. tipClass: 'ok'
  488. },
  489. approve: {
  490. title: '③ 批量通过弹层',
  491. sub: '统一意见,一次确认',
  492. items: [
  493. '标题「批量通过」,提示将对 N 条执行',
  494. '默认意见「同意」,可修改或清空',
  495. '确定后串行 approve,刷新列表'
  496. ],
  497. tip: '与详情页单条「同意」语义一致。',
  498. tipClass: 'ok'
  499. },
  500. return: {
  501. title: '④ 批量打回弹层',
  502. sub: '打回意见必填',
  503. items: [
  504. '标题「批量打回」,主按钮琥珀警告色',
  505. '意见为空时 toast「打回意见必填」',
  506. '部分失败时提示成功/失败条数'
  507. ],
  508. tip: '不在此 Demo 做「批量拒绝」,避免误操作风险。',
  509. tipClass: 'pick'
  510. }
  511. }
  512. function renderSide(scene) {
  513. const c = SCENE_COPY[scene]
  514. sidePanel.innerHTML =
  515. '<h2>' + c.title + '</h2>' +
  516. '<p class="sub">' + c.sub + '</p>' +
  517. '<ul>' + c.items.map((x) => '<li>' + x + '</li>').join('') + '</ul>' +
  518. '<div class="' + c.tipClass + '">' + c.tip + '</div>'
  519. }
  520. function renderList() {
  521. listEl.innerHTML = TASKS.map((t) => {
  522. const on = state.selected.has(t.id)
  523. return (
  524. '<div class="row" data-id="' + t.id + '">' +
  525. '<div class="check" data-check="' + t.id + '">' +
  526. '<div class="check__box' + (on ? ' on' : '') + '">' + (on ? '✓' : '') + '</div>' +
  527. '</div>' +
  528. '<div class="card' + (on ? ' is-selected' : '') + '" data-card="' + t.id + '">' +
  529. '<div class="card__head">' +
  530. '<div class="card__title">' + t.title + '</div>' +
  531. '<div class="card__time">' + t.time + '</div>' +
  532. '</div>' +
  533. '<div class="card__meta">' +
  534. t.meta.map((m) => '<span class="card__meta-line">' + m + '</span>').join('') +
  535. '</div>' +
  536. '<div class="card__foot">' +
  537. '<div class="card__user">' +
  538. '<div class="card__avatar">' + t.avatar + '</div>' +
  539. '<div class="card__user-text">由' + t.user + '提交</div>' +
  540. '</div>' +
  541. '<div class="card__status">进行中</div>' +
  542. '</div>' +
  543. '</div>' +
  544. '</div>'
  545. )
  546. }).join('')
  547. }
  548. function syncChrome() {
  549. page.classList.toggle('is-manage', state.manage)
  550. manageBtn.textContent = state.manage ? '完成' : '管理'
  551. manageBtn.classList.toggle('is-done', state.manage)
  552. const n = state.selected.size
  553. batchBar.classList.toggle('is-show', state.manage && n > 0)
  554. batchHint.textContent = '已选 ' + n + ' 项'
  555. btnApprove.disabled = n === 0
  556. btnReturn.disabled = n === 0
  557. }
  558. function toast(msg) {
  559. toastEl.textContent = msg
  560. toastEl.classList.add('is-show')
  561. clearTimeout(toast._t)
  562. toast._t = setTimeout(() => toastEl.classList.remove('is-show'), 1800)
  563. }
  564. function openSheet(kind) {
  565. const n = state.selected.size
  566. if (!n) return
  567. state.sheetKind = kind
  568. sheetTitle.textContent = kind === 'approve' ? '批量通过' : '批量打回'
  569. sheetHint.innerHTML = '将对 <strong>' + n + '</strong> 条待办执行操作'
  570. sheetInput.value = kind === 'approve' ? '同意' : ''
  571. sheetInput.placeholder =
  572. kind === 'approve' ? '审批意见(可选)' : '请填写打回意见(必填)'
  573. sheetConfirm.textContent = '确定'
  574. sheetConfirm.className =
  575. 'sheet__btn ' + (kind === 'approve' ? 'is-ok' : 'is-return')
  576. sheetMask.classList.add('is-show')
  577. sheet.classList.add('is-show')
  578. setTimeout(() => sheetInput.focus(), 80)
  579. }
  580. function closeSheet() {
  581. state.sheetKind = null
  582. sheetMask.classList.remove('is-show')
  583. sheet.classList.remove('is-show')
  584. }
  585. function setManage(on) {
  586. state.manage = on
  587. if (!on) state.selected.clear()
  588. renderList()
  589. syncChrome()
  590. }
  591. function applyScene(scene) {
  592. document.querySelectorAll('#sceneTabs .tab').forEach((el) => {
  593. el.classList.toggle('is-active', el.dataset.scene === scene)
  594. })
  595. renderSide(scene)
  596. closeSheet()
  597. if (scene === 'browse') {
  598. setManage(false)
  599. } else if (scene === 'manage') {
  600. state.selected = new Set(['t1', 't2'])
  601. setManage(true)
  602. } else if (scene === 'approve') {
  603. state.selected = new Set(['t1', 't2'])
  604. setManage(true)
  605. openSheet('approve')
  606. } else if (scene === 'return') {
  607. state.selected = new Set(['t1', 't3'])
  608. setManage(true)
  609. openSheet('return')
  610. }
  611. }
  612. manageBtn.addEventListener('click', () => setManage(!state.manage))
  613. listEl.addEventListener('click', (e) => {
  614. const check = e.target.closest('[data-check]')
  615. const card = e.target.closest('[data-card]')
  616. const id = (check && check.dataset.check) || (card && card.dataset.card)
  617. if (!id) return
  618. if (state.manage) {
  619. if (state.selected.has(id)) state.selected.delete(id)
  620. else state.selected.add(id)
  621. renderList()
  622. syncChrome()
  623. return
  624. }
  625. if (card) toast('进入审批详情(Demo)')
  626. })
  627. btnApprove.addEventListener('click', () => openSheet('approve'))
  628. btnReturn.addEventListener('click', () => openSheet('return'))
  629. sheetMask.addEventListener('click', closeSheet)
  630. document.getElementById('sheetClose').addEventListener('click', closeSheet)
  631. document.getElementById('sheetCancel').addEventListener('click', closeSheet)
  632. sheetConfirm.addEventListener('click', () => {
  633. const kind = state.sheetKind
  634. const comment = String(sheetInput.value || '').trim()
  635. const n = state.selected.size
  636. if (kind === 'return' && !comment) {
  637. toast('打回意见必填')
  638. return
  639. }
  640. closeSheet()
  641. const label = kind === 'approve' ? '通过' : '打回'
  642. toast('已' + label + ' ' + n + ' 条(Demo)')
  643. state.selected.clear()
  644. setManage(false)
  645. })
  646. document.getElementById('sceneTabs').addEventListener('click', (e) => {
  647. const tab = e.target.closest('.tab')
  648. if (!tab) return
  649. applyScene(tab.dataset.scene)
  650. })
  651. renderList()
  652. syncChrome()
  653. renderSide('browse')
  654. </script>
  655. </body>
  656. </html>