oa-approval-start-flow-preview-demo.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  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" />
  6. <title>发起审批 · 表单底部审批流预览 Demo(Fresh Mint)</title>
  7. <style>
  8. :root {
  9. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-bg: #ecfdf5;
  12. --text: #0f172a;
  13. --text-2: #334155;
  14. --muted: #94a3b8;
  15. --border: #e2e8f0;
  16. --bg: #f1f5f9;
  17. --card: #fff;
  18. --warn: #d97706;
  19. --warn-bg: #fff7ed;
  20. --cc: #0891b2;
  21. --cc-bg: #ecfeff;
  22. --radius: 10px;
  23. }
  24. * { box-sizing: border-box; }
  25. body {
  26. margin: 0;
  27. min-height: 100vh;
  28. font: 14px/1.5 Inter, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
  29. color: var(--text);
  30. background:
  31. linear-gradient(180deg, #eefcf6 0%, var(--bg) 160px) fixed;
  32. }
  33. .page {
  34. max-width: 960px;
  35. margin: 0 auto;
  36. padding: 24px 20px 72px;
  37. }
  38. .demo-banner {
  39. display: flex;
  40. flex-wrap: wrap;
  41. gap: 10px 16px;
  42. align-items: center;
  43. justify-content: space-between;
  44. margin-bottom: 20px;
  45. padding: 12px 16px;
  46. background: #fff;
  47. border: 1px solid var(--border);
  48. border-radius: var(--radius);
  49. }
  50. .demo-banner b { color: var(--primary-dark); }
  51. .demo-banner span { color: var(--muted); font-size: 13px; }
  52. .tag {
  53. display: inline-block;
  54. padding: 2px 8px;
  55. border-radius: 999px;
  56. background: var(--primary-bg);
  57. color: var(--primary-dark);
  58. font-size: 12px;
  59. font-weight: 600;
  60. }
  61. .mode-switch {
  62. display: flex;
  63. flex-wrap: wrap;
  64. gap: 8px;
  65. margin-bottom: 20px;
  66. }
  67. .mode-btn {
  68. border: 1px solid var(--border);
  69. background: #fff;
  70. border-radius: 8px;
  71. padding: 8px 14px;
  72. font: inherit;
  73. font-size: 13px;
  74. cursor: pointer;
  75. color: var(--text-2);
  76. }
  77. .mode-btn:hover { border-color: #a7f3d0; background: var(--primary-bg); }
  78. .mode-btn.on {
  79. border-color: var(--primary);
  80. background: var(--primary-bg);
  81. color: var(--primary-dark);
  82. font-weight: 600;
  83. }
  84. /* 遮罩 + 弹窗 */
  85. .mask {
  86. display: flex;
  87. align-items: flex-start;
  88. justify-content: center;
  89. padding: 24px 12px 40px;
  90. min-height: 70vh;
  91. background: rgba(15, 23, 42, 0.35);
  92. border-radius: 12px;
  93. }
  94. .dialog {
  95. width: min(720px, 100%);
  96. max-height: calc(100vh - 80px);
  97. display: flex;
  98. flex-direction: column;
  99. background: var(--card);
  100. border-radius: 12px;
  101. box-shadow: 0 16px 48px rgba(15, 23, 42, 0.18);
  102. overflow: hidden;
  103. }
  104. .dialog__hd {
  105. flex-shrink: 0;
  106. padding: 16px 20px;
  107. border-bottom: 1px solid var(--border);
  108. font-size: 16px;
  109. font-weight: 600;
  110. }
  111. .dialog__bd {
  112. flex: 1;
  113. overflow: auto;
  114. padding: 16px 20px 8px;
  115. }
  116. .dialog__ft {
  117. flex-shrink: 0;
  118. display: flex;
  119. justify-content: flex-end;
  120. gap: 10px;
  121. padding: 12px 20px 16px;
  122. border-top: 1px solid var(--border);
  123. background: #fafbfc;
  124. }
  125. .btn {
  126. display: inline-flex;
  127. align-items: center;
  128. gap: 6px;
  129. height: 36px;
  130. padding: 0 16px;
  131. border-radius: 8px;
  132. border: 1px solid var(--border);
  133. background: #fff;
  134. font: inherit;
  135. font-size: 13px;
  136. cursor: pointer;
  137. }
  138. .btn.primary {
  139. background: var(--primary);
  140. border-color: var(--primary);
  141. color: #fff;
  142. }
  143. /* 表单区 */
  144. .form-stack label {
  145. display: block;
  146. margin: 0 0 6px;
  147. font-size: 12px;
  148. color: var(--muted);
  149. font-weight: 500;
  150. }
  151. .form-stack .field { margin-bottom: 14px; }
  152. .form-stack .req::after {
  153. content: " *";
  154. color: #dc2626;
  155. }
  156. .form-stack input,
  157. .form-stack textarea,
  158. .form-stack select {
  159. width: 100%;
  160. border: 1px solid var(--border);
  161. border-radius: 8px;
  162. padding: 8px 12px;
  163. font: inherit;
  164. background: #fff;
  165. }
  166. .form-stack input:focus,
  167. .form-stack textarea:focus,
  168. .form-stack select:focus {
  169. outline: none;
  170. border-color: var(--primary);
  171. box-shadow: 0 0 0 3px var(--primary-bg);
  172. }
  173. .form-grid-2 {
  174. display: grid;
  175. grid-template-columns: 1fr 1fr;
  176. gap: 0 16px;
  177. }
  178. @media (max-width: 560px) {
  179. .form-grid-2 { grid-template-columns: 1fr; }
  180. }
  181. /* —— 钉钉式审批流预览(表单底部) —— */
  182. .flow-preview {
  183. margin: 8px 0 12px;
  184. padding: 12px 12px 8px;
  185. background: #f8fafc;
  186. border: 1px solid var(--border);
  187. border-radius: 10px;
  188. }
  189. .flow-preview__title {
  190. display: flex;
  191. align-items: center;
  192. justify-content: space-between;
  193. margin-bottom: 10px;
  194. font-size: 13px;
  195. font-weight: 600;
  196. color: var(--text-2);
  197. }
  198. .flow-preview__title .hint {
  199. font-weight: 400;
  200. font-size: 12px;
  201. color: var(--muted);
  202. }
  203. .flow-chain {
  204. display: flex;
  205. flex-direction: column;
  206. align-items: stretch;
  207. gap: 0;
  208. }
  209. .flow-step {
  210. display: flex;
  211. gap: 8px;
  212. align-items: stretch;
  213. }
  214. .flow-rail {
  215. display: flex;
  216. flex-direction: column;
  217. align-items: center;
  218. width: 16px;
  219. flex-shrink: 0;
  220. }
  221. /* 统一灰色圆点,不区分节点类型配色 */
  222. .flow-dot {
  223. width: 10px;
  224. height: 10px;
  225. margin-top: 12px;
  226. border-radius: 50%;
  227. background: #94a3b8;
  228. flex-shrink: 0;
  229. box-shadow: 0 0 0 3px #e2e8f0;
  230. }
  231. .flow-dot.end {
  232. background: #cbd5e1;
  233. box-shadow: 0 0 0 3px #f1f5f9;
  234. }
  235. .flow-line {
  236. width: 2px;
  237. flex: 1;
  238. min-height: 10px;
  239. margin: 4px 0;
  240. background: #e2e8f0;
  241. }
  242. .flow-step:last-child .flow-line { display: none; }
  243. /* 单行紧凑卡片 · 方案 B */
  244. .flow-card {
  245. flex: 1;
  246. min-width: 0;
  247. margin-bottom: 6px;
  248. padding: 9px 12px;
  249. background: #fff;
  250. border: 1px solid var(--border);
  251. border-radius: 6px;
  252. }
  253. .flow-card__row {
  254. display: flex;
  255. flex-wrap: nowrap;
  256. align-items: center;
  257. gap: 6px 8px;
  258. min-width: 0;
  259. overflow: hidden;
  260. }
  261. .flow-card__name {
  262. flex-shrink: 0;
  263. font-size: 12px;
  264. font-weight: 600;
  265. color: var(--text);
  266. line-height: 1.2;
  267. white-space: nowrap;
  268. }
  269. .flow-card__meta {
  270. flex-shrink: 1;
  271. min-width: 0;
  272. overflow: hidden;
  273. text-overflow: ellipsis;
  274. white-space: nowrap;
  275. font-size: 11px;
  276. color: var(--muted);
  277. line-height: 1.2;
  278. }
  279. .flow-card__sep {
  280. flex-shrink: 0;
  281. color: #cbd5e1;
  282. font-size: 11px;
  283. }
  284. .flow-card__people {
  285. display: inline-flex;
  286. flex-wrap: nowrap;
  287. align-items: center;
  288. gap: 4px;
  289. margin-left: auto;
  290. flex-shrink: 0;
  291. max-width: 45%;
  292. overflow: hidden;
  293. }
  294. .person-chip {
  295. display: inline-flex;
  296. align-items: center;
  297. gap: 3px;
  298. padding: 1px 6px 1px 2px;
  299. background: #f1f5f9;
  300. border-radius: 999px;
  301. font-size: 11px;
  302. color: var(--text-2);
  303. line-height: 1.2;
  304. white-space: nowrap;
  305. }
  306. .person-chip .dot {
  307. width: 16px;
  308. height: 16px;
  309. border-radius: 50%;
  310. display: grid;
  311. place-items: center;
  312. font-size: 9px;
  313. font-weight: 700;
  314. color: #fff;
  315. background: var(--primary);
  316. }
  317. .person-chip.pick {
  318. background: var(--warn-bg);
  319. color: var(--warn);
  320. border: 1px dashed #fdba74;
  321. cursor: pointer;
  322. padding: 2px 8px;
  323. }
  324. .person-chip.pick:hover { background: #ffedd5; }
  325. .person-chip.role {
  326. background: var(--primary-bg);
  327. color: var(--primary-dark);
  328. }
  329. .badge-mini {
  330. display: inline-block;
  331. flex-shrink: 0;
  332. padding: 0 5px;
  333. border-radius: 4px;
  334. font-size: 10px;
  335. font-weight: 500;
  336. line-height: 16px;
  337. white-space: nowrap;
  338. }
  339. .badge-mini.or { background: #fff7ed; color: var(--warn); }
  340. .badge-mini.and { background: #eff6ff; color: #2563eb; }
  341. .badge-mini.cc { background: var(--cc-bg); color: var(--cc); }
  342. .branch-box {
  343. margin: 2px 0 8px 24px;
  344. padding: 6px 10px;
  345. border: 1px dashed #cbd5e1;
  346. border-radius: 6px;
  347. background: #fff;
  348. font-size: 11px;
  349. color: var(--muted);
  350. }
  351. .branch-box b { color: var(--text-2); }
  352. .branch-box.active {
  353. border-color: #86efac;
  354. background: var(--primary-bg);
  355. color: var(--primary-dark);
  356. }
  357. .note {
  358. margin-top: 16px;
  359. padding: 12px 14px;
  360. background: #fffbeb;
  361. border: 1px solid #fde68a;
  362. border-radius: 8px;
  363. font-size: 13px;
  364. color: #92400e;
  365. }
  366. .note code {
  367. font-size: 12px;
  368. background: #fff;
  369. padding: 1px 5px;
  370. border-radius: 4px;
  371. }
  372. </style>
  373. </head>
  374. <body>
  375. <div class="page">
  376. <div class="demo-banner">
  377. <div>
  378. <b>发起审批 · 方案 B(紧凑卡片)</b>
  379. <span>解析到具体人名;节点卡片缩小以适配弹窗滚动区</span>
  380. </div>
  381. <span class="tag">Fresh Mint · Demo</span>
  382. </div>
  383. <div class="mode-switch" role="tablist">
  384. <button type="button" class="mode-btn on" data-mode="B" id="btn-b">方案 B · 解析到具体人名(推荐)</button>
  385. <button type="button" class="mode-btn" data-mode="A" id="btn-a">方案 A · 仅配置摘要</button>
  386. <button type="button" class="mode-btn" data-mode="C" id="btn-c">方案 C · 含自选 + 条件提示</button>
  387. </div>
  388. <div class="mask">
  389. <div class="dialog" role="dialog" aria-label="发起审批">
  390. <div class="dialog__hd">员工请假申请</div>
  391. <div class="dialog__bd">
  392. <!-- 业务表单 -->
  393. <div class="form-stack">
  394. <div class="form-grid-2">
  395. <div class="field">
  396. <label class="req">请假类型</label>
  397. <select id="leaveType">
  398. <option value="annual">年假</option>
  399. <option value="sick">病假</option>
  400. <option value="personal">事假</option>
  401. </select>
  402. </div>
  403. <div class="field">
  404. <label class="req">请假天数</label>
  405. <input type="number" id="days" value="2" min="0.5" step="0.5" />
  406. </div>
  407. </div>
  408. <div class="field">
  409. <label class="req">请假事由</label>
  410. <textarea rows="2" placeholder="请输入事由">家中有事</textarea>
  411. </div>
  412. </div>
  413. <!-- 审批流预览 -->
  414. <div class="flow-preview">
  415. <div class="flow-preview__title">
  416. <span>审批流程</span>
  417. <span class="hint" id="flowHint">按当前发起人解析审批人</span>
  418. </div>
  419. <div class="flow-chain" id="flowChain"></div>
  420. </div>
  421. </div>
  422. <div class="dialog__ft">
  423. <button type="button" class="btn">取消</button>
  424. <button type="button" class="btn primary">提交</button>
  425. </div>
  426. </div>
  427. </div>
  428. <div class="note">
  429. 文件路径:<code>docs/mockups/oa-approval-start-flow-preview-demo.html</code><br />
  430. 落地建议:在 <code>oa-approval/start/index.vue</code> 弹窗内、表单下方增加只读流程预览;自选审批人可内嵌到对应节点,替代现在顶部独立字段。
  431. 用上方三个按钮切换 A / B / C 方案对比。
  432. </div>
  433. </div>
  434. <script>
  435. const modes = {
  436. A: {
  437. hint: '仅展示节点配置类型(不解析人名)',
  438. render() {
  439. return [
  440. step('start', '发起人', '当前登录用户', null),
  441. step('approval', '直属主管', '发起人的主管', null, 'or'),
  442. step('approval', '一级审批', '工作流角色 · 总经理', null, 'or'),
  443. step('cc', '抄送人', '发起人自选', null, 'cc'),
  444. step('end', '结束', '', null)
  445. ].join('')
  446. }
  447. },
  448. B: {
  449. hint: '按当前发起人解析审批人(推荐首版)',
  450. render() {
  451. return [
  452. step('start', '发起人', '', people([{ n: '张', name: '张三' }])),
  453. step(
  454. 'approval',
  455. '直属主管',
  456. '发起人的主管',
  457. people([{ n: '李', name: '李主管' }]),
  458. 'or'
  459. ),
  460. step(
  461. 'approval',
  462. '一级审批',
  463. '工作流角色 · 总经理',
  464. people([
  465. { n: '王', name: '王总' },
  466. { n: '赵', name: '赵总' }
  467. ]),
  468. 'or'
  469. ),
  470. step(
  471. 'cc',
  472. '抄送人',
  473. '人事专员',
  474. people([{ n: '陈', name: '陈人事' }]),
  475. 'cc'
  476. ),
  477. step('end', '结束', '', null)
  478. ].join('')
  479. }
  480. },
  481. C: {
  482. hint: '自选在节点内点选;条件分支随表单天数高亮',
  483. render() {
  484. const days = Number(document.getElementById('days').value) || 0
  485. const needCeo = days > 3
  486. return [
  487. step('start', '发起人', '', people([{ n: '张', name: '张三' }])),
  488. step(
  489. 'approval',
  490. '直属主管',
  491. '发起人的主管',
  492. people([{ n: '李', name: '李主管' }]),
  493. 'or'
  494. ),
  495. `<div class="branch-box ${needCeo ? '' : 'active'}">
  496. <b>条件分支</b> · 请假天数 ≤ 3 天 → 走人事抄送(当前 ${days} 天)
  497. </div>`,
  498. needCeo
  499. ? step(
  500. 'approval',
  501. '总经理审批',
  502. '发起人自选',
  503. `<span class="person-chip pick" title="点击选择审批人">+ 选择审批人</span>`,
  504. 'and'
  505. )
  506. : '',
  507. `<div class="branch-box ${needCeo ? 'active' : ''}">
  508. <b>条件分支</b> · 请假天数 &gt; 3 天 → 需总经理审批
  509. </div>`,
  510. step(
  511. 'cc',
  512. '抄送人',
  513. '发起人自选',
  514. `<span class="person-chip pick">+ 选择抄送人</span>`,
  515. 'cc'
  516. ),
  517. step('end', '结束', '', null)
  518. ].join('')
  519. }
  520. }
  521. }
  522. function people(list) {
  523. return list
  524. .map(
  525. (p) =>
  526. `<span class="person-chip"><span class="dot">${p.n}</span>${p.name}</span>`
  527. )
  528. .join('')
  529. }
  530. /** type, nodeName, meta, peopleHtml, badge — 左侧统一灰色圆 */
  531. function step(type, name, meta, peopleHtml, badge) {
  532. const badgeHtml = badge
  533. ? `<span class="badge-mini ${badge}">${
  534. badge === 'or' ? '或签' : badge === 'and' ? '会签' : '抄送'
  535. }</span>`
  536. : ''
  537. const metaPart = meta
  538. ? `<span class="flow-card__sep">·</span><span class="flow-card__meta" title="${meta}">${meta}</span>`
  539. : ''
  540. const peoplePart = peopleHtml
  541. ? `<span class="flow-card__people">${peopleHtml}</span>`
  542. : ''
  543. return `
  544. <div class="flow-step">
  545. <div class="flow-rail">
  546. <div class="flow-dot ${type === 'end' ? 'end' : ''}"></div>
  547. <div class="flow-line"></div>
  548. </div>
  549. <div class="flow-card">
  550. <div class="flow-card__row">
  551. <span class="flow-card__name">${name}</span>
  552. ${badgeHtml}
  553. ${metaPart}
  554. ${peoplePart}
  555. </div>
  556. </div>
  557. </div>`
  558. }
  559. let current = 'B'
  560. function render() {
  561. const mode = modes[current]
  562. document.getElementById('flowHint').textContent = mode.hint
  563. document.getElementById('flowChain').innerHTML = mode.render()
  564. document.querySelectorAll('.mode-btn').forEach((btn) => {
  565. btn.classList.toggle('on', btn.dataset.mode === current)
  566. })
  567. }
  568. document.querySelectorAll('.mode-btn').forEach((btn) => {
  569. btn.addEventListener('click', () => {
  570. current = btn.dataset.mode
  571. render()
  572. })
  573. })
  574. document.getElementById('days').addEventListener('input', () => {
  575. if (current === 'C') render()
  576. })
  577. document.getElementById('leaveType').addEventListener('change', () => {
  578. if (current === 'C') render()
  579. })
  580. render()
  581. </script>
  582. </body>
  583. </html>