oa-start-picker-list-style-demo.html 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  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</title>
  7. <style>
  8. :root {
  9. --ink: #0f172a;
  10. --muted: #64748b;
  11. --line: #e2e8f0;
  12. --brand: #22c55e;
  13. --brand-dark: #16a34a;
  14. --brand-deep: #15803d;
  15. --brand-soft: #ecfdf5;
  16. --text: #0f291e;
  17. --text-muted: #5f7a6d;
  18. --border-mint: #d8f3e5;
  19. }
  20. * { box-sizing: border-box; }
  21. html, body { margin: 0; min-height: 100%; }
  22. body {
  23. font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
  24. color: var(--ink);
  25. background:
  26. radial-gradient(900px 420px at 8% -8%, rgba(52, 211, 153, 0.16), transparent 55%),
  27. radial-gradient(700px 380px at 92% 0%, rgba(148, 163, 184, 0.14), transparent 50%),
  28. linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
  29. line-height: 1.45;
  30. }
  31. .wrap {
  32. max-width: 1220px;
  33. margin: 0 auto;
  34. padding: 28px 18px 96px;
  35. }
  36. h1 {
  37. margin: 0 0 8px;
  38. font-size: 24px;
  39. letter-spacing: -0.02em;
  40. font-weight: 720;
  41. }
  42. .sub {
  43. margin: 0;
  44. max-width: 760px;
  45. color: var(--muted);
  46. font-size: 14px;
  47. }
  48. .note {
  49. margin: 14px 0 22px;
  50. padding: 10px 12px;
  51. border-radius: 10px;
  52. background: #fff;
  53. border: 1px solid var(--line);
  54. font-size: 13px;
  55. color: var(--muted);
  56. }
  57. .note strong { color: var(--brand-deep); }
  58. .toolbar {
  59. display: flex;
  60. flex-wrap: wrap;
  61. gap: 8px;
  62. margin-bottom: 16px;
  63. }
  64. .chip {
  65. appearance: none;
  66. border: 1px solid var(--line);
  67. background: #fff;
  68. border-radius: 999px;
  69. padding: 8px 14px;
  70. font-size: 13px;
  71. font-weight: 650;
  72. color: #475569;
  73. cursor: pointer;
  74. font-family: inherit;
  75. }
  76. .chip.is-on {
  77. border-color: var(--brand);
  78. background: var(--brand-soft);
  79. color: var(--brand-deep);
  80. box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
  81. }
  82. .layout {
  83. display: grid;
  84. grid-template-columns: 1fr minmax(320px, 380px);
  85. gap: 22px;
  86. align-items: start;
  87. }
  88. @media (max-width: 960px) {
  89. .layout { grid-template-columns: 1fr; }
  90. }
  91. .schemes { display: grid; gap: 10px; }
  92. .scheme {
  93. appearance: none;
  94. width: 100%;
  95. text-align: left;
  96. border: 1px solid var(--line);
  97. background: #fff;
  98. border-radius: 16px;
  99. padding: 14px 16px;
  100. cursor: pointer;
  101. transition: border-color .15s, box-shadow .15s, background .15s;
  102. font-family: inherit;
  103. color: inherit;
  104. }
  105. .scheme:hover {
  106. border-color: #cbd5e1;
  107. box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
  108. }
  109. .scheme.is-on {
  110. border-color: var(--brand);
  111. background: linear-gradient(180deg, #f0fdf4, #fff);
  112. box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
  113. }
  114. .scheme-top {
  115. display: flex;
  116. align-items: center;
  117. gap: 10px;
  118. margin-bottom: 4px;
  119. }
  120. .letter {
  121. width: 28px; height: 28px;
  122. border-radius: 8px;
  123. display: grid; place-items: center;
  124. font-size: 13px; font-weight: 750;
  125. background: #f1f5f9;
  126. color: #334155;
  127. flex-shrink: 0;
  128. }
  129. .scheme.is-on .letter {
  130. background: var(--brand-soft);
  131. color: var(--brand-deep);
  132. }
  133. .scheme h2 {
  134. margin: 0;
  135. font-size: 15px;
  136. font-weight: 680;
  137. }
  138. .scheme p {
  139. margin: 0;
  140. font-size: 12.5px;
  141. color: var(--muted);
  142. padding-left: 38px;
  143. }
  144. .tag {
  145. margin-left: auto;
  146. font-size: 10px;
  147. font-weight: 700;
  148. padding: 2px 8px;
  149. border-radius: 999px;
  150. background: #f1f5f9;
  151. color: #64748b;
  152. white-space: nowrap;
  153. }
  154. .tag.rec {
  155. background: var(--brand-soft);
  156. color: var(--brand-deep);
  157. }
  158. .tag.base {
  159. background: #fef3c7;
  160. color: #92400e;
  161. }
  162. /* —— 右侧手机 —— */
  163. .stage { position: sticky; top: 16px; }
  164. .phone {
  165. background: #111827;
  166. border-radius: 36px;
  167. padding: 10px;
  168. box-shadow: 0 24px 50px rgba(15, 23, 42, 0.18);
  169. }
  170. .phone-screen {
  171. border-radius: 28px;
  172. overflow: hidden;
  173. background: #f1f5f9;
  174. height: 680px;
  175. display: flex;
  176. flex-direction: column;
  177. position: relative;
  178. }
  179. .phone-nav {
  180. height: 48px;
  181. display: flex;
  182. align-items: center;
  183. justify-content: center;
  184. background: #fff;
  185. border-bottom: 1px solid #f1f5f9;
  186. font-size: 15px;
  187. font-weight: 650;
  188. color: var(--text);
  189. position: relative;
  190. flex-shrink: 0;
  191. }
  192. .phone-nav .back {
  193. position: absolute;
  194. left: 14px;
  195. color: var(--text-muted);
  196. font-size: 13px;
  197. font-weight: 500;
  198. }
  199. .form-mock {
  200. flex: 1;
  201. padding: 12px;
  202. display: flex;
  203. flex-direction: column;
  204. gap: 10px;
  205. overflow: auto;
  206. }
  207. .field {
  208. background: #fff;
  209. border-radius: 12px;
  210. border: 1px solid #eef2f6;
  211. padding: 12px 14px;
  212. cursor: pointer;
  213. }
  214. .field:active { opacity: 0.92; }
  215. .field .lab {
  216. font-size: 12px;
  217. color: #94a3b8;
  218. margin-bottom: 6px;
  219. }
  220. .field .val {
  221. display: flex;
  222. align-items: center;
  223. justify-content: space-between;
  224. font-size: 14px;
  225. color: #334155;
  226. }
  227. .field .val.ph { color: #cbd5e1; }
  228. .field .chev { color: #cbd5e1; font-size: 12px; }
  229. /* 遮罩 + 底部弹窗 */
  230. .mask {
  231. position: absolute;
  232. inset: 0;
  233. background: rgba(15, 23, 42, 0.42);
  234. display: flex;
  235. align-items: flex-end;
  236. z-index: 5;
  237. }
  238. .sheet {
  239. width: 100%;
  240. height: 72%;
  241. background: #fff;
  242. border-radius: 18px 18px 0 0;
  243. display: flex;
  244. flex-direction: column;
  245. overflow: hidden;
  246. animation: sheetUp .22s ease-out;
  247. }
  248. @keyframes sheetUp {
  249. from { transform: translateY(18%); opacity: .6; }
  250. to { transform: translateY(0); opacity: 1; }
  251. }
  252. .sheet-hd {
  253. display: flex;
  254. align-items: center;
  255. justify-content: space-between;
  256. padding: 14px 16px;
  257. border-bottom: 1px solid var(--border-mint);
  258. flex-shrink: 0;
  259. }
  260. .sheet-hd .cancel {
  261. min-width: 48px;
  262. color: var(--text-muted);
  263. font-size: 14px;
  264. cursor: pointer;
  265. }
  266. .sheet-hd strong {
  267. font-size: 16px;
  268. font-weight: 700;
  269. color: var(--text);
  270. }
  271. .sheet-hd .ok {
  272. min-width: 48px;
  273. color: var(--brand);
  274. font-size: 14px;
  275. font-weight: 600;
  276. text-align: right;
  277. cursor: pointer;
  278. }
  279. .search {
  280. padding: 10px 12px;
  281. background: #fff;
  282. flex-shrink: 0;
  283. }
  284. .search-inner {
  285. display: flex;
  286. align-items: center;
  287. gap: 8px;
  288. height: 36px;
  289. padding: 0 12px;
  290. border-radius: 999px;
  291. background: #f3f4f6;
  292. color: #94a3b8;
  293. font-size: 13px;
  294. }
  295. .search-inner svg { width: 14px; height: 14px; flex-shrink: 0; }
  296. .pick-list {
  297. flex: 1;
  298. overflow: auto;
  299. -webkit-overflow-scrolling: touch;
  300. }
  301. .pick-item {
  302. display: flex;
  303. align-items: center;
  304. gap: 10px;
  305. cursor: pointer;
  306. -webkit-tap-highlight-color: transparent;
  307. user-select: none;
  308. }
  309. .pick-item:active { opacity: 0.88; }
  310. .pick-main { flex: 1; min-width: 0; }
  311. .pick-title {
  312. font-size: 15px;
  313. font-weight: 600;
  314. color: var(--text);
  315. white-space: nowrap;
  316. overflow: hidden;
  317. text-overflow: ellipsis;
  318. }
  319. .pick-sub {
  320. margin-top: 2px;
  321. font-size: 12px;
  322. color: var(--text-muted);
  323. white-space: nowrap;
  324. overflow: hidden;
  325. text-overflow: ellipsis;
  326. }
  327. .check {
  328. width: 20px;
  329. height: 20px;
  330. border-radius: 50%;
  331. border: 1.5px solid #cbd5e1;
  332. flex-shrink: 0;
  333. display: grid;
  334. place-items: center;
  335. background: #fff;
  336. transition: all .15s;
  337. }
  338. .check.sq { border-radius: 5px; }
  339. .pick-item.is-on .check {
  340. border-color: var(--brand);
  341. background: var(--brand);
  342. color: #fff;
  343. }
  344. .check svg { width: 12px; height: 12px; opacity: 0; }
  345. .pick-item.is-on .check svg { opacity: 1; }
  346. .avatar {
  347. width: 36px;
  348. height: 36px;
  349. border-radius: 50%;
  350. display: grid;
  351. place-items: center;
  352. font-size: 14px;
  353. font-weight: 700;
  354. color: var(--brand-deep);
  355. background: var(--brand-soft);
  356. flex-shrink: 0;
  357. }
  358. .badge {
  359. font-size: 10px;
  360. font-weight: 700;
  361. padding: 2px 7px;
  362. border-radius: 999px;
  363. background: #f1f5f9;
  364. color: #64748b;
  365. flex-shrink: 0;
  366. }
  367. .badge.ok {
  368. background: var(--brand-soft);
  369. color: var(--brand-deep);
  370. }
  371. /* ========== 方案 A:现状基线 ========== */
  372. .s-a .pick-list {
  373. padding: 12px;
  374. background: transparent;
  375. }
  376. .s-a .pick-item {
  377. margin-bottom: 10px;
  378. padding: 12px 14px;
  379. border-radius: 10px;
  380. background: transparent;
  381. }
  382. .s-a .pick-item:last-child { margin-bottom: 0; }
  383. /* ========== 方案 B:柔和卡片 ========== */
  384. .s-b .pick-list {
  385. padding: 12px;
  386. background: #f8fafc;
  387. }
  388. .s-b .pick-item {
  389. margin-bottom: 8px;
  390. padding: 12px 14px;
  391. border-radius: 12px;
  392. background: #fff;
  393. border: 1px solid #eef2f6;
  394. box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
  395. }
  396. .s-b .pick-item:last-child { margin-bottom: 0; }
  397. .s-b .pick-item.is-on {
  398. border-color: #86efac;
  399. background: linear-gradient(180deg, #f0fdf4, #fff);
  400. box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
  401. }
  402. /* ========== 方案 C:分割线行 ========== */
  403. .s-c .pick-list {
  404. padding: 0;
  405. background: #fff;
  406. }
  407. .s-c .pick-item {
  408. padding: 14px 16px;
  409. border-bottom: 1px solid #f1f5f9;
  410. background: #fff;
  411. }
  412. .s-c .pick-item:last-child { border-bottom: none; }
  413. .s-c .pick-item.is-on {
  414. background: var(--brand-soft);
  415. }
  416. .s-c .pick-item.is-on .pick-title { color: var(--brand-deep); }
  417. /* ========== 方案 D:头像 + 右侧勾选 ========== */
  418. .s-d .pick-list {
  419. padding: 8px 12px 16px;
  420. background: #f8fafc;
  421. }
  422. .s-d .pick-item {
  423. margin-bottom: 8px;
  424. padding: 12px 14px;
  425. border-radius: 14px;
  426. background: #fff;
  427. border: 1px solid #eef2f6;
  428. flex-direction: row;
  429. }
  430. .s-d .pick-item .check { order: 3; margin-left: 4px; }
  431. .s-d .pick-item.is-on {
  432. border-color: #bbf7d0;
  433. background: #fff;
  434. }
  435. .s-d .pick-item.is-on .avatar {
  436. background: var(--brand);
  437. color: #fff;
  438. }
  439. /* ========== 方案 E:左侧色条 ========== */
  440. .s-e .pick-list {
  441. padding: 12px;
  442. background: #fff;
  443. }
  444. .s-e .pick-item {
  445. position: relative;
  446. margin-bottom: 8px;
  447. padding: 13px 14px 13px 16px;
  448. border-radius: 10px;
  449. background: #f8fafc;
  450. overflow: hidden;
  451. }
  452. .s-e .pick-item::before {
  453. content: "";
  454. position: absolute;
  455. left: 0; top: 10px; bottom: 10px;
  456. width: 3px;
  457. border-radius: 0 3px 3px 0;
  458. background: transparent;
  459. transition: background .15s;
  460. }
  461. .s-e .pick-item.is-on {
  462. background: var(--brand-soft);
  463. }
  464. .s-e .pick-item.is-on::before {
  465. background: var(--brand);
  466. }
  467. /* ========== 方案 F:紧凑密排 ========== */
  468. .s-f .pick-list {
  469. padding: 4px 0;
  470. background: #fff;
  471. }
  472. .s-f .pick-item {
  473. padding: 10px 16px;
  474. gap: 8px;
  475. border-bottom: 1px solid #f8fafc;
  476. }
  477. .s-f .pick-title { font-size: 14px; font-weight: 550; }
  478. .s-f .pick-sub { font-size: 11px; }
  479. .s-f .check { width: 18px; height: 18px; }
  480. .s-f .pick-item.is-on { background: #f0fdf4; }
  481. /* ========== 方案 G:整行高亮 + 右勾 ========== */
  482. .s-g .pick-list {
  483. padding: 12px;
  484. background: #f1f5f9;
  485. }
  486. .s-g .pick-item {
  487. margin-bottom: 8px;
  488. padding: 14px 14px;
  489. border-radius: 12px;
  490. background: #fff;
  491. border: 1.5px solid transparent;
  492. }
  493. .s-g .pick-item .check {
  494. order: 3;
  495. margin-left: auto;
  496. border: none;
  497. background: #e2e8f0;
  498. color: transparent;
  499. }
  500. .s-g .pick-item.is-on {
  501. border-color: var(--brand);
  502. background: #fff;
  503. box-shadow: 0 4px 14px rgba(22, 163, 74, 0.12);
  504. }
  505. .s-g .pick-item.is-on .check {
  506. background: var(--brand);
  507. color: #fff;
  508. }
  509. .s-g .pick-item.is-on .check svg { opacity: 1; }
  510. .s-g .pick-item:not(.is-on) .check svg { opacity: 0; }
  511. /* ========== 方案 H:双行元信息卡 ========== */
  512. .s-h .pick-list {
  513. padding: 12px;
  514. background: linear-gradient(180deg, #f0fdf4 0%, #f8fafc 40%);
  515. }
  516. .s-h .pick-item {
  517. margin-bottom: 10px;
  518. padding: 14px;
  519. border-radius: 14px;
  520. background: rgba(255,255,255,0.95);
  521. border: 1px solid #e2e8f0;
  522. align-items: flex-start;
  523. }
  524. .s-h .pick-item.is-on {
  525. border-color: #86efac;
  526. box-shadow: 0 8px 20px rgba(22, 163, 74, 0.1);
  527. }
  528. .s-h .meta-row {
  529. display: flex;
  530. align-items: center;
  531. gap: 6px;
  532. margin-top: 8px;
  533. flex-wrap: wrap;
  534. }
  535. .s-h .meta-pill {
  536. font-size: 10px;
  537. font-weight: 650;
  538. padding: 2px 8px;
  539. border-radius: 999px;
  540. background: #f1f5f9;
  541. color: #64748b;
  542. }
  543. .s-h .pick-item.is-on .meta-pill.pri {
  544. background: var(--brand-soft);
  545. color: var(--brand-deep);
  546. }
  547. /* ========== 方案 I:单选点选(无复选框感) ========== */
  548. .s-i .pick-list {
  549. padding: 8px 12px 16px;
  550. background: #fff;
  551. }
  552. .s-i .pick-item {
  553. margin-bottom: 6px;
  554. padding: 13px 14px;
  555. border-radius: 12px;
  556. background: #f8fafc;
  557. border: 1px solid transparent;
  558. }
  559. .s-i .pick-item .check {
  560. width: 18px;
  561. height: 18px;
  562. border-width: 2px;
  563. }
  564. .s-i .pick-item.is-on {
  565. background: var(--brand-soft);
  566. border-color: #86efac;
  567. }
  568. .s-i .pick-item.is-on .pick-title { color: var(--brand-deep); }
  569. .s-i .pick-item.is-on .check {
  570. box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
  571. }
  572. /* ========== 方案 J:分组标题 ========== */
  573. .s-j .pick-list {
  574. padding: 0 0 12px;
  575. background: #f8fafc;
  576. }
  577. .s-j .group-title {
  578. padding: 10px 16px 6px;
  579. font-size: 12px;
  580. font-weight: 650;
  581. color: #94a3b8;
  582. letter-spacing: 0.02em;
  583. }
  584. .s-j .group-block {
  585. margin: 0 12px 10px;
  586. background: #fff;
  587. border-radius: 12px;
  588. border: 1px solid #eef2f6;
  589. overflow: hidden;
  590. }
  591. .s-j .pick-item {
  592. padding: 12px 14px;
  593. border-bottom: 1px solid #f8fafc;
  594. }
  595. .s-j .pick-item:last-child { border-bottom: none; }
  596. .s-j .pick-item.is-on { background: #f0fdf4; }
  597. .stage-meta {
  598. margin-top: 14px;
  599. padding: 12px 14px;
  600. background: rgba(255,255,255,.92);
  601. border: 1px solid var(--line);
  602. border-radius: 14px;
  603. font-size: 13px;
  604. color: var(--muted);
  605. }
  606. .stage-meta strong { color: var(--brand-deep); }
  607. .stage-meta .why {
  608. margin-top: 6px;
  609. font-size: 12.5px;
  610. line-height: 1.55;
  611. }
  612. .hint {
  613. margin-top: 8px;
  614. font-size: 12px;
  615. color: #94a3b8;
  616. }
  617. </style>
  618. </head>
  619. <body>
  620. <div class="wrap">
  621. <h1>发起审批 · 下拉选择弹窗列表样式</h1>
  622. <p class="sub">
  623. 针对 OA 发起页中人员 / 部门 / 岗位 / 收款账户 / 关联审批等底部弹窗列表,提供多套视觉方案。
  624. 左侧切换方案,右侧手机预览可点选体验;上方可切换「单选 / 多选」与「数据场景」。
  625. </p>
  626. <div class="note">
  627. Token 对齐 mobile-h5「薄荷微风」:主色 <strong>#22c55e</strong>、软底 <strong>#ecfdf5</strong>、正文 <strong>#0f291e</strong>。
  628. 当前线上基线为方案 A(透明 cell + 间距,选中态弱)。
  629. </div>
  630. <div class="toolbar">
  631. <button type="button" class="chip is-on" data-mode="multi">多选</button>
  632. <button type="button" class="chip" data-mode="single">单选</button>
  633. <span style="width:8px"></span>
  634. <button type="button" class="chip is-on" data-scene="person">人员</button>
  635. <button type="button" class="chip" data-scene="dept">部门</button>
  636. <button type="button" class="chip" data-scene="option">简单选项</button>
  637. <button type="button" class="chip" data-scene="related">关联审批</button>
  638. </div>
  639. <div class="layout">
  640. <div class="schemes" id="schemes"></div>
  641. <div class="stage">
  642. <div class="phone">
  643. <div class="phone-screen" id="phone">
  644. <div class="phone-nav">
  645. <span class="back">‹</span>
  646. <span>发起审批</span>
  647. </div>
  648. <div class="form-mock">
  649. <div class="field" id="openField">
  650. <div class="lab" id="fieldLab">审批人</div>
  651. <div class="val ph" id="fieldVal">
  652. <span>请选择</span>
  653. <span class="chev">›</span>
  654. </div>
  655. </div>
  656. <div class="field">
  657. <div class="lab">事由</div>
  658. <div class="val ph"><span>请输入</span></div>
  659. </div>
  660. <p class="hint">点击上方字段打开选择弹窗。切换方案时弹窗保持打开,便于对比。</p>
  661. </div>
  662. <div class="mask" id="mask" hidden>
  663. <div class="sheet" id="sheet">
  664. <div class="sheet-hd">
  665. <span class="cancel" id="btnCancel">取消</span>
  666. <strong id="sheetTitle">选择人员</strong>
  667. <span class="ok" id="btnOk">确定</span>
  668. </div>
  669. <div class="search">
  670. <div class="search-inner">
  671. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  672. <circle cx="11" cy="11" r="7"/><path d="M20 20l-3-3"/>
  673. </svg>
  674. <span id="searchPh">搜索姓名或工号</span>
  675. </div>
  676. </div>
  677. <div class="pick-list" id="pickList"></div>
  678. </div>
  679. </div>
  680. </div>
  681. </div>
  682. <div class="stage-meta">
  683. 当前方案:<strong id="metaName">A · 现状基线</strong>
  684. <div class="why" id="metaWhy"></div>
  685. </div>
  686. </div>
  687. </div>
  688. </div>
  689. <script>
  690. const SCHEMES = [
  691. {
  692. id: 'a',
  693. letter: 'A',
  694. name: '现状基线',
  695. tag: '当前',
  696. tagClass: 'base',
  697. desc: '透明 cell + 10px 间距,与现网 oa-pick__list 接近;选中态几乎不可见。',
  698. why: '层级弱、选中反馈不足,长列表时条目边界不清晰。适合作为改造前对照。'
  699. },
  700. {
  701. id: 'b',
  702. letter: 'B',
  703. name: '柔和白卡片',
  704. tag: '',
  705. tagClass: '',
  706. desc: '灰底上白卡片 + 浅描边;选中后薄荷绿描边与外环光。',
  707. why: '卡片边界清晰、选中反馈强,适配人员/部门/岗位等通用列表,落地成本低。'
  708. },
  709. {
  710. id: 'c',
  711. letter: 'C',
  712. name: '分割线行表',
  713. tag: '已采用',
  714. tagClass: 'rec',
  715. desc: '白底连续行 + 发丝分割线;选中整行浅绿底。',
  716. why: '信息密度高、偏系统原生感;适合简单选项、枚举类单选。当前已落地到 OA 选择弹窗。'
  717. },
  718. {
  719. id: 'd',
  720. letter: 'D',
  721. name: '头像 + 右勾选',
  722. tag: '人员',
  723. tagClass: 'rec',
  724. desc: '左侧头像字、中部姓名/副标题、右侧圆形勾选;选中头像变主色。',
  725. why: '专为人员/抄送场景;身份识别强。部门/简单选项场景可隐藏头像。'
  726. },
  727. {
  728. id: 'e',
  729. letter: 'E',
  730. name: '左侧色条',
  731. tag: '',
  732. tagClass: '',
  733. desc: '灰底圆角行,选中出现左侧品牌色竖条 + 软绿底。',
  734. why: '选中态含蓄但不丢识别度,适合希望「少边框、少阴影」的清爽风。'
  735. },
  736. {
  737. id: 'f',
  738. letter: 'F',
  739. name: '紧凑密排',
  740. tag: '长列表',
  741. tagClass: '',
  742. desc: '更小行高与字号,弱分割线;一屏展示更多选项。',
  743. why: '人员很多、或枚举项较长时优先;牺牲一点呼吸感换效率。'
  744. },
  745. {
  746. id: 'g',
  747. letter: 'G',
  748. name: '描边高亮 + 右勾',
  749. tag: '',
  750. tagClass: '',
  751. desc: '灰底面板内白卡片;选中品牌描边 + 阴影,勾选移到右侧。',
  752. why: '选中态最醒目,适合「必须看清已选」的多选场景(审批人、抄送人)。'
  753. },
  754. {
  755. id: 'h',
  756. letter: 'H',
  757. name: '双行元信息卡',
  758. tag: '关联单',
  759. tagClass: 'rec',
  760. desc: '标题 + 副标题 + 底部 pill 元数据(单号/状态/时间)。',
  761. why: '专为关联审批、收款账户等「需要多字段」的选项;普通短列表略重。'
  762. },
  763. {
  764. id: 'i',
  765. letter: 'I',
  766. name: '轻量点选',
  767. tag: '单选友好',
  768. tagClass: '',
  769. desc: '浅灰圆角块,选中软绿底 + 描边;勾选圈带外环光。',
  770. why: '适合部门、岗位、简单下拉等单选为主的字段;多选也可用。'
  771. },
  772. {
  773. id: 'j',
  774. letter: 'J',
  775. name: '分组块列表',
  776. tag: '',
  777. tagClass: '',
  778. desc: '按字母/部门分组,组内白卡片连续行。',
  779. why: '选项可按组织/首字母归类时体验最好;需后端或前端分组能力。'
  780. }
  781. ]
  782. const SCENES = {
  783. person: {
  784. title: '选择人员',
  785. lab: '审批人',
  786. search: '搜索姓名或工号',
  787. groups: [
  788. {
  789. title: '研发中心',
  790. items: [
  791. { id: '1', title: '张晓明', sub: '工号 · 10086', avatar: '张', meta: ['研发', '在职'] },
  792. { id: '2', title: '李思远', sub: '工号 · 10087', avatar: '李', meta: ['研发', '在职'] },
  793. { id: '3', title: '王芳', sub: '工号 · 10091', avatar: '王', meta: ['产品', '在职'] }
  794. ]
  795. },
  796. {
  797. title: '职能支持',
  798. items: [
  799. { id: '4', title: '陈静', sub: '工号 · 20012', avatar: '陈', meta: ['人事', '在职'] },
  800. { id: '5', title: '赵磊', sub: '工号 · 20018', avatar: '赵', meta: ['财务', '在职'] }
  801. ]
  802. }
  803. ]
  804. },
  805. dept: {
  806. title: '选择部门',
  807. lab: '所属部门',
  808. search: '搜索部门名称',
  809. groups: [
  810. {
  811. title: '组织架构',
  812. items: [
  813. { id: 'd1', title: '中科兴皖 / 研发中心', sub: '一级部门', avatar: '研', meta: ['12 人'] },
  814. { id: 'd2', title: '中科兴皖 / 产品中心', sub: '一级部门', avatar: '产', meta: ['8 人'] },
  815. { id: 'd3', title: '中科兴皖 / 运营中心', sub: '一级部门', avatar: '运', meta: ['15 人'] },
  816. { id: 'd4', title: '中科兴皖 / 财务部', sub: '一级部门', avatar: '财', meta: ['5 人'] }
  817. ]
  818. }
  819. ]
  820. },
  821. option: {
  822. title: '选择类型',
  823. lab: '出差类型',
  824. search: '搜索选项',
  825. groups: [
  826. {
  827. title: '常用',
  828. items: [
  829. { id: 'o1', title: '市内出差', sub: '当日往返', avatar: '市', meta: [] },
  830. { id: 'o2', title: '省内出差', sub: '需填写行程', avatar: '省', meta: [] },
  831. { id: 'o3', title: '省外出差', sub: '需关联预算', avatar: '外', meta: [] },
  832. { id: 'o4', title: '海外出差', sub: '需提前审批', avatar: '海', meta: [] }
  833. ]
  834. }
  835. ]
  836. },
  837. related: {
  838. title: '关联审批',
  839. lab: '关联单据',
  840. search: '搜索单号或标题',
  841. groups: [
  842. {
  843. title: '最近',
  844. items: [
  845. { id: 'r1', title: '出差申请 · 上海客户拜访', sub: 'OA-20260320-018', avatar: '差', meta: ['已通过', '3/18'] },
  846. { id: 'r2', title: '费用报销 · 差旅费', sub: 'OA-20260318-042', avatar: '报', meta: ['审批中', '3/18'] },
  847. { id: 'r3', title: '用章申请 · 合同用印', sub: 'OA-20260315-009', avatar: '章', meta: ['已通过', '3/15'] }
  848. ]
  849. }
  850. ]
  851. }
  852. }
  853. const state = {
  854. scheme: 'c',
  855. mode: 'multi',
  856. scene: 'person',
  857. selected: new Set(['1', '2']),
  858. open: true
  859. }
  860. const checkSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M5 12l5 5L20 7"/></svg>`
  861. function flatItems() {
  862. return SCENES[state.scene].groups.flatMap((g) => g.items)
  863. }
  864. function renderSchemes() {
  865. const box = document.getElementById('schemes')
  866. box.innerHTML = SCHEMES.map((s) => `
  867. <button type="button" class="scheme ${state.scheme === s.id ? 'is-on' : ''}" data-id="${s.id}">
  868. <div class="scheme-top">
  869. <span class="letter">${s.letter}</span>
  870. <h2>${s.name}</h2>
  871. ${s.tag ? `<span class="tag ${s.tagClass}">${s.tag}</span>` : ''}
  872. </div>
  873. <p>${s.desc}</p>
  874. </button>
  875. `).join('')
  876. box.querySelectorAll('.scheme').forEach((el) => {
  877. el.addEventListener('click', () => {
  878. state.scheme = el.dataset.id
  879. apply()
  880. })
  881. })
  882. }
  883. function renderList() {
  884. const list = document.getElementById('pickList')
  885. const scene = SCENES[state.scene]
  886. const sq = state.mode === 'multi' ? 'sq' : ''
  887. const showAvatar = state.scheme === 'd' || state.scene === 'person' && state.scheme === 'h'
  888. const useGroups = state.scheme === 'j'
  889. if (useGroups) {
  890. list.innerHTML = scene.groups.map((g) => `
  891. <div class="group-title">${g.title}</div>
  892. <div class="group-block">
  893. ${g.items.map((it) => itemHtml(it, sq, true)).join('')}
  894. </div>
  895. `).join('')
  896. } else if (state.scheme === 'h') {
  897. list.innerHTML = flatItems().map((it) => `
  898. <div class="pick-item ${state.selected.has(it.id) ? 'is-on' : ''}" data-id="${it.id}">
  899. <span class="check ${sq}">${checkSvg}</span>
  900. <div class="pick-main">
  901. <div class="pick-title">${it.title}</div>
  902. <div class="pick-sub">${it.sub}</div>
  903. ${it.meta?.length ? `<div class="meta-row">${it.meta.map((m, i) => `<span class="meta-pill ${i === 0 ? 'pri' : ''}">${m}</span>`).join('')}</div>` : ''}
  904. </div>
  905. </div>
  906. `).join('')
  907. } else {
  908. list.innerHTML = flatItems().map((it) => itemHtml(it, sq, state.scheme === 'd')).join('')
  909. }
  910. list.querySelectorAll('.pick-item').forEach((el) => {
  911. el.addEventListener('click', () => toggle(el.dataset.id))
  912. })
  913. }
  914. function itemHtml(it, sq, withAvatar) {
  915. return `
  916. <div class="pick-item ${state.selected.has(it.id) ? 'is-on' : ''}" data-id="${it.id}">
  917. <span class="check ${sq}">${checkSvg}</span>
  918. ${withAvatar ? `<span class="avatar">${it.avatar || it.title.slice(0, 1)}</span>` : ''}
  919. <div class="pick-main">
  920. <div class="pick-title">${it.title}</div>
  921. ${it.sub ? `<div class="pick-sub">${it.sub}</div>` : ''}
  922. </div>
  923. </div>
  924. `
  925. }
  926. function toggle(id) {
  927. if (state.mode === 'single') {
  928. state.selected = new Set([id])
  929. } else {
  930. if (state.selected.has(id)) state.selected.delete(id)
  931. else state.selected.add(id)
  932. }
  933. updateFieldVal()
  934. renderList()
  935. }
  936. function updateFieldVal() {
  937. const el = document.getElementById('fieldVal')
  938. const map = new Map(flatItems().map((x) => [x.id, x.title]))
  939. const names = [...state.selected].map((id) => map.get(id)).filter(Boolean)
  940. if (!names.length) {
  941. el.className = 'val ph'
  942. el.innerHTML = `<span>请选择</span><span class="chev">›</span>`
  943. } else {
  944. el.className = 'val'
  945. el.innerHTML = `<span>${names.join('、')}</span><span class="chev">›</span>`
  946. }
  947. }
  948. function apply() {
  949. const phone = document.getElementById('phone')
  950. const sheet = document.getElementById('sheet')
  951. const scene = SCENES[state.scene]
  952. const scheme = SCHEMES.find((s) => s.id === state.scheme)
  953. phone.className = 'phone-screen s-' + state.scheme
  954. sheet.className = 'sheet s-' + state.scheme
  955. document.getElementById('sheetTitle').textContent = scene.title
  956. document.getElementById('fieldLab').textContent = scene.lab
  957. document.getElementById('searchPh').textContent = scene.search
  958. document.getElementById('metaName').textContent = `${scheme.letter} · ${scheme.name}`
  959. document.getElementById('metaWhy').textContent = scheme.why
  960. document.getElementById('mask').hidden = !state.open
  961. document.getElementById('btnOk').style.visibility = state.mode === 'single' ? 'hidden' : 'visible'
  962. renderSchemes()
  963. renderList()
  964. updateFieldVal()
  965. }
  966. document.getElementById('openField').addEventListener('click', () => {
  967. state.open = true
  968. apply()
  969. })
  970. document.getElementById('btnCancel').addEventListener('click', () => {
  971. state.open = false
  972. apply()
  973. })
  974. document.getElementById('btnOk').addEventListener('click', () => {
  975. state.open = false
  976. apply()
  977. })
  978. document.getElementById('mask').addEventListener('click', (e) => {
  979. if (e.target.id === 'mask') {
  980. state.open = false
  981. apply()
  982. }
  983. })
  984. document.querySelectorAll('.chip[data-mode]').forEach((el) => {
  985. el.addEventListener('click', () => {
  986. document.querySelectorAll('.chip[data-mode]').forEach((c) => c.classList.remove('is-on'))
  987. el.classList.add('is-on')
  988. state.mode = el.dataset.mode
  989. if (state.mode === 'single' && state.selected.size > 1) {
  990. const first = [...state.selected][0]
  991. state.selected = new Set(first ? [first] : [])
  992. }
  993. apply()
  994. })
  995. })
  996. document.querySelectorAll('.chip[data-scene]').forEach((el) => {
  997. el.addEventListener('click', () => {
  998. document.querySelectorAll('.chip[data-scene]').forEach((c) => c.classList.remove('is-on'))
  999. el.classList.add('is-on')
  1000. state.scene = el.dataset.scene
  1001. const ids = flatItems().map((x) => x.id)
  1002. state.selected = new Set(ids.slice(0, state.mode === 'multi' ? 2 : 1))
  1003. state.open = true
  1004. apply()
  1005. })
  1006. })
  1007. apply()
  1008. </script>
  1009. </body>
  1010. </html>