| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>关联审批单 · 选单卡片模式 Demo</title>
- <style>
- :root {
- --primary: #10b981;
- --primary-dark: #059669;
- --primary-bg: #ecfdf5;
- --primary-border: #a7f3d0;
- --text: #1e293b;
- --text-muted: #64748b;
- --text-faint: #94a3b8;
- --border: #e2e8f0;
- --border-soft: #f1f5f9;
- --bg: #f1f5f9;
- --surface: #ffffff;
- --warn: #d97706;
- --danger: #ef4444;
- --success: #059669;
- --info: #64748b;
- --shadow: 0 12px 40px rgba(15, 23, 42, 0.12);
- --radius: 10px;
- }
- * { box-sizing: border-box; }
- body {
- margin: 0;
- min-height: 100vh;
- font-family: Inter, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
- font-size: 14px;
- line-height: 1.5;
- color: var(--text);
- background:
- radial-gradient(1200px 400px at 10% -10%, #d1fae5 0%, transparent 55%),
- radial-gradient(900px 360px at 100% 0%, #e0f2fe 0%, transparent 50%),
- var(--bg);
- }
- .page {
- max-width: 960px;
- margin: 0 auto;
- padding: 28px 20px 80px;
- }
- h1 {
- margin: 0 0 6px;
- font-size: 22px;
- font-weight: 700;
- color: var(--text);
- }
- .lead {
- margin: 0 0 20px;
- color: var(--text-muted);
- max-width: 720px;
- }
- .notes {
- margin-bottom: 20px;
- padding: 12px 14px;
- border: 1px dashed var(--primary-border);
- border-radius: var(--radius);
- background: #fff;
- color: var(--text);
- font-size: 13px;
- }
- .notes strong { color: var(--primary-dark); }
- .notes ul { margin: 8px 0 0; padding-left: 18px; }
- .notes li { margin: 2px 0; }
- /* Dialog shell */
- .dialog {
- background: var(--surface);
- border-radius: 12px;
- box-shadow: var(--shadow);
- border: 1px solid var(--border);
- overflow: hidden;
- display: flex;
- flex-direction: column;
- max-height: calc(100vh - 120px);
- }
- .dialog__header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 14px 18px;
- border-bottom: 1px solid var(--border);
- }
- .dialog__title {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
- }
- .dialog__close {
- width: 28px;
- height: 28px;
- border: 0;
- border-radius: 6px;
- background: transparent;
- color: var(--text-muted);
- font-size: 18px;
- cursor: pointer;
- }
- .dialog__close:hover { background: var(--border-soft); }
- .dialog__body {
- padding: 0;
- overflow: auto;
- flex: 1;
- }
- .dialog__footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 12px 18px;
- border-top: 1px solid var(--border);
- background: #f8fafc;
- }
- .dialog__footer-left {
- color: var(--text-muted);
- font-size: 13px;
- }
- .dialog__footer-left em {
- font-style: normal;
- color: var(--primary-dark);
- font-weight: 600;
- }
- .dialog__footer-right { display: flex; gap: 8px; }
- .btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- height: 36px;
- padding: 0 14px;
- border-radius: 8px;
- border: 1px solid var(--border);
- background: #fff;
- color: var(--text);
- font-size: 14px;
- cursor: pointer;
- }
- .btn:hover { background: #f8fafc; }
- .btn--primary {
- border-color: var(--primary);
- background: var(--primary);
- color: #fff;
- }
- .btn--primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
- .btn--primary:disabled {
- opacity: 0.45;
- cursor: not-allowed;
- }
- /* Tabs */
- .tabs {
- display: flex;
- gap: 0;
- padding: 0 18px;
- border-bottom: 1px solid var(--border);
- background: #fff;
- }
- .tab {
- position: relative;
- height: 44px;
- padding: 0 16px;
- border: 0;
- background: transparent;
- color: var(--text-muted);
- font-size: 14px;
- cursor: pointer;
- }
- .tab:hover { color: var(--text); }
- .tab.is-active {
- color: var(--primary-dark);
- font-weight: 600;
- }
- .tab.is-active::after {
- content: "";
- position: absolute;
- left: 12px;
- right: 12px;
- bottom: 0;
- height: 2px;
- border-radius: 1px;
- background: var(--primary);
- }
- .tab .count {
- margin-left: 4px;
- color: var(--text-faint);
- font-weight: 400;
- font-size: 12px;
- }
- /* Filters */
- .filters {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- align-items: center;
- padding: 12px 18px;
- border-bottom: 1px solid var(--border-soft);
- background: #fff;
- }
- .field {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .field label {
- font-size: 12px;
- color: var(--text-muted);
- white-space: nowrap;
- }
- .input, .select {
- height: 34px;
- padding: 0 10px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: #fff;
- color: var(--text);
- font-size: 13px;
- outline: none;
- }
- .input { min-width: 200px; }
- .input:focus, .select:focus {
- border-color: var(--primary);
- box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
- }
- .select { min-width: 120px; }
- /* Card list */
- .card-list {
- background: #fff;
- }
- .card {
- display: flex;
- gap: 12px;
- align-items: flex-start;
- padding: 14px 18px;
- border-bottom: 1px solid var(--border-soft);
- cursor: pointer;
- transition: background 0.12s;
- }
- .card:hover { background: #f8fafc; }
- .card.is-selected { background: var(--primary-bg); }
- .card.is-disabled {
- opacity: 0.55;
- cursor: not-allowed;
- }
- .card__check {
- flex-shrink: 0;
- margin-top: 10px;
- }
- .card__check input {
- width: 16px;
- height: 16px;
- accent-color: var(--primary);
- cursor: pointer;
- }
- .card__avatar {
- flex-shrink: 0;
- width: 40px;
- height: 40px;
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #fff;
- font-size: 13px;
- font-weight: 600;
- letter-spacing: 0.5px;
- margin-top: 2px;
- }
- .card__main {
- flex: 1;
- min-width: 0;
- }
- .card__title {
- margin: 0 0 4px;
- font-size: 14px;
- font-weight: 600;
- color: var(--text);
- line-height: 1.4;
- }
- .card__meta {
- margin: 0;
- padding: 0;
- list-style: none;
- color: var(--text-muted);
- font-size: 12.5px;
- }
- .card__meta li {
- margin: 1px 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .card__time {
- margin-top: 6px;
- color: var(--text-faint);
- font-size: 12px;
- }
- .card__right {
- flex-shrink: 0;
- text-align: right;
- min-width: 100px;
- padding-top: 2px;
- }
- .card__actor {
- font-size: 12.5px;
- color: var(--warn);
- line-height: 1.4;
- }
- .card__status {
- margin-top: 4px;
- font-size: 12.5px;
- font-weight: 500;
- }
- .st-running { color: var(--warn); }
- .st-completed { color: var(--success); }
- .st-rejected { color: var(--danger); }
- .st-returned { color: var(--warn); }
- .st-cancelled { color: var(--info); }
- .empty {
- padding: 48px 18px;
- text-align: center;
- color: var(--text-faint);
- font-size: 13px;
- }
- .pager {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 8px;
- padding: 10px 18px 14px;
- color: var(--text-muted);
- font-size: 12px;
- background: #fff;
- }
- .pager button {
- height: 28px;
- padding: 0 10px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: #fff;
- cursor: pointer;
- font-size: 12px;
- }
- .pager button:disabled { opacity: 0.4; cursor: not-allowed; }
- .legend {
- margin-top: 18px;
- font-size: 12px;
- color: var(--text-muted);
- }
- </style>
- </head>
- <body>
- <div class="page">
- <h1>关联审批单 · 选单卡片模式</h1>
- <p class="lead">
- 需求草案 Demo:去掉「仅已通过」限制;三 Tab(我发起的 / 我审批的 / 抄送我的);
- 钉钉式卡片;展示部门与表单摘要;支持筛选。交互可点选,便于对齐产品预期。
- </p>
- <div class="notes">
- <strong>本 Demo 约定(待评审)</strong>
- <ul>
- <li>Tab 默认落在「我发起的」;切换 Tab 清空当前页勾选草稿(已确认关联保留在表单外)。</li>
- <li>状态不限制:处理中 / 已通过 / 已拒绝 / 已打回 / 已撤销均可出现;右侧展示当前处理人 + 状态文案。</li>
- <li>卡片主信息:标题、部门、前 2 个业务字段摘要、申请时间;左勾选 + 发起人头像字。</li>
- <li>筛选:仅关键词(流程名 / 标题)。</li>
- </ul>
- </div>
- <div class="dialog" role="dialog" aria-label="选择关联审批单">
- <div class="dialog__header">
- <h2 class="dialog__title">选择关联审批单</h2>
- <button class="dialog__close" type="button" aria-label="关闭">×</button>
- </div>
- <div class="tabs" role="tablist">
- <button class="tab is-active" data-tab="mine" type="button">我发起的<span class="count" id="count-mine"></span></button>
- <button class="tab" data-tab="done" type="button">我审批的<span class="count" id="count-done"></span></button>
- <button class="tab" data-tab="cc" type="button">抄送我的<span class="count" id="count-cc"></span></button>
- </div>
- <div class="filters">
- <div class="field">
- <label>关键词</label>
- <input class="input" id="kw" type="search" placeholder="搜索流程名 / 标题" />
- </div>
- <button class="btn" type="button" id="btnSearch">查询</button>
- <button class="btn" type="button" id="btnReset">重置</button>
- </div>
- <div class="dialog__body">
- <div class="card-list" id="list"></div>
- <div class="pager">
- <span id="pageInfo">第 1 页</span>
- <button type="button" id="prevPage">上一页</button>
- <button type="button" id="nextPage">下一页</button>
- </div>
- </div>
- <div class="dialog__footer">
- <div class="dialog__footer-left">已选 <em id="selCount">0</em> 项</div>
- <div class="dialog__footer-right">
- <button class="btn" type="button">取消</button>
- <button class="btn btn--primary" type="button" id="btnOk" disabled>确定</button>
- </div>
- </div>
- </div>
- <p class="legend">
- 静态 Mock · Fresh Mint 配色 · 打开本文件即可预览:
- <code>docs/mockups/oa-related-approval-picker-card-demo.html</code>
- </p>
- </div>
- <script>
- const AVATAR_COLORS = ["#3b82f6", "#10b981", "#8b5cf6", "#f59e0b", "#06b6d4", "#ef4444"];
- const STATUS_TEXT = {
- running: "处理中",
- completed: "已通过",
- rejected: "已拒绝",
- returned: "已打回",
- cancelled: "已撤销",
- };
- /** 已关联(模拟表单已选,不可再选) */
- const alreadyLinked = new Set(["m2"]);
- const DATA = {
- mine: [
- {
- id: "m1",
- appName: "人事审批",
- title: "吴自龙提交的人事审批",
- initiatorName: "吴自龙",
- deptName: "研发中心 / 平台组",
- fields: [
- { label: "数字输入框", value: "123" },
- { label: "单行输入框", value: "123" },
- ],
- applyTime: "2026-07-23 13:50",
- status: "running",
- actor: "吴震 (审批人)",
- },
- {
- id: "m2",
- appName: "请假",
- title: "吴自龙提交的请假",
- initiatorName: "吴自龙",
- deptName: "研发中心 / 平台组",
- fields: [
- { label: "请假类型", value: "年假" },
- { label: "时长", value: "1.5 天" },
- ],
- applyTime: "2026-07-20 09:12",
- status: "completed",
- actor: "",
- },
- {
- id: "m3",
- appName: "报销",
- title: "吴自龙提交的报销",
- initiatorName: "吴自龙",
- deptName: "研发中心 / 平台组",
- fields: [
- { label: "报销金额", value: "¥1,280.00" },
- { label: "事由", value: "客户拜访差旅" },
- ],
- applyTime: "2026-07-18 16:40",
- status: "rejected",
- actor: "",
- },
- {
- id: "m4",
- appName: "采购",
- title: "吴自龙提交的采购",
- initiatorName: "吴自龙",
- deptName: "研发中心 / 平台组",
- fields: [
- { label: "采购品类", value: "办公设备" },
- { label: "预算", value: "¥8,000" },
- ],
- applyTime: "2026-07-10 11:05",
- status: "returned",
- actor: "",
- },
- ],
- done: [
- {
- id: "d1",
- appName: "请假",
- title: "张三提交的请假",
- initiatorName: "张三",
- deptName: "市场部 / 华东区",
- fields: [
- { label: "请假类型", value: "事假" },
- { label: "时长", value: "0.5 天" },
- ],
- applyTime: "2026-07-22 10:20",
- status: "running",
- actor: "吴自龙 (审批人)",
- },
- {
- id: "d2",
- appName: "人事审批",
- title: "李四提交的人事审批",
- initiatorName: "李四",
- deptName: "人事行政部",
- fields: [
- { label: "调整类型", value: "转正" },
- { label: "生效日期", value: "2026-08-01" },
- ],
- applyTime: "2026-07-15 14:33",
- status: "completed",
- actor: "",
- },
- {
- id: "d3",
- appName: "报销",
- title: "王五提交的报销",
- initiatorName: "王五",
- deptName: "财务部",
- fields: [
- { label: "报销金额", value: "¥560.00" },
- { label: "科目", value: "招待费" },
- ],
- applyTime: "2026-07-12 08:50",
- status: "cancelled",
- actor: "",
- },
- ],
- cc: [
- {
- id: "c1",
- appName: "采购",
- title: "赵六提交的采购",
- initiatorName: "赵六",
- deptName: "供应链 / 采购组",
- fields: [
- { label: "供应商", value: "某某科技" },
- { label: "金额", value: "¥12,000" },
- ],
- applyTime: "2026-07-21 17:08",
- status: "running",
- actor: "陈七 (审批人)",
- },
- {
- id: "c2",
- appName: "人事审批",
- title: "钱八提交的人事审批",
- initiatorName: "钱八",
- deptName: "研发中心 / 测试组",
- fields: [
- { label: "岗位", value: "高级测试工程师" },
- { label: "职级", value: "P6" },
- ],
- applyTime: "2026-07-08 09:40",
- status: "completed",
- actor: "",
- },
- ],
- };
- const state = {
- tab: "mine",
- kw: "",
- page: 1,
- pageSize: 5,
- selected: new Set(),
- };
- function shortName(name) {
- const s = String(name || "").trim();
- return s.length <= 2 ? s : s.slice(-2);
- }
- function avatarColor(name) {
- let h = 0;
- for (let i = 0; i < name.length; i++) h = (h + name.charCodeAt(i) * 17) % AVATAR_COLORS.length;
- return AVATAR_COLORS[h];
- }
- function statusClass(st) {
- return "st-" + (st || "running");
- }
- function filtered() {
- return (DATA[state.tab] || []).filter((row) => {
- if (state.kw) {
- const blob = [row.appName, row.title].join(" ").toLowerCase();
- if (!blob.includes(state.kw.toLowerCase())) return false;
- }
- return true;
- });
- }
- function renderCounts() {
- for (const k of ["mine", "done", "cc"]) {
- const el = document.getElementById("count-" + k);
- if (el) el.textContent = "(" + DATA[k].length + ")";
- }
- }
- function renderList() {
- const all = filtered();
- const totalPages = Math.max(1, Math.ceil(all.length / state.pageSize));
- if (state.page > totalPages) state.page = totalPages;
- const start = (state.page - 1) * state.pageSize;
- const pageRows = all.slice(start, start + state.pageSize);
- const list = document.getElementById("list");
- if (!pageRows.length) {
- list.innerHTML = '<div class="empty">暂无符合条件的审批单</div>';
- } else {
- list.innerHTML = pageRows
- .map((row) => {
- const disabled = alreadyLinked.has(row.id);
- const checked = state.selected.has(row.id);
- const meta = [
- row.deptName ? `<li>部门:${escapeHtml(row.deptName)}</li>` : "",
- ...(row.fields || [])
- .slice(0, 2)
- .map((f) => `<li>${escapeHtml(f.label)}:${escapeHtml(f.value)}</li>`),
- ].join("");
- const actor =
- row.status === "running" && row.actor
- ? `<div class="card__actor">${escapeHtml(row.actor)}</div>`
- : "";
- return `
- <div class="card ${checked ? "is-selected" : ""} ${disabled ? "is-disabled" : ""}"
- data-id="${row.id}" data-disabled="${disabled ? "1" : "0"}">
- <div class="card__check">
- <input type="checkbox" ${checked ? "checked" : ""} ${disabled ? "disabled" : ""} />
- </div>
- <div class="card__avatar" style="background:${avatarColor(row.initiatorName)}">
- ${escapeHtml(shortName(row.initiatorName))}
- </div>
- <div class="card__main">
- <h3 class="card__title">${escapeHtml(row.title)}</h3>
- <ul class="card__meta">${meta}</ul>
- <div class="card__time">${escapeHtml(row.applyTime)}</div>
- </div>
- <div class="card__right">
- ${actor}
- <div class="card__status ${statusClass(row.status)}">${STATUS_TEXT[row.status] || row.status}</div>
- </div>
- </div>`;
- })
- .join("");
- }
- document.getElementById("pageInfo").textContent =
- `共 ${all.length} 条 · 第 ${state.page}/${totalPages} 页`;
- document.getElementById("prevPage").disabled = state.page <= 1;
- document.getElementById("nextPage").disabled = state.page >= totalPages;
- document.getElementById("selCount").textContent = String(state.selected.size);
- document.getElementById("btnOk").disabled = state.selected.size === 0;
- }
- function escapeHtml(s) {
- return String(s || "")
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """);
- }
- document.querySelectorAll(".tab").forEach((btn) => {
- btn.addEventListener("click", () => {
- document.querySelectorAll(".tab").forEach((b) => b.classList.remove("is-active"));
- btn.classList.add("is-active");
- state.tab = btn.dataset.tab;
- state.page = 1;
- state.selected.clear();
- renderList();
- });
- });
- document.getElementById("btnSearch").addEventListener("click", () => {
- state.kw = document.getElementById("kw").value.trim();
- state.page = 1;
- renderList();
- });
- document.getElementById("btnReset").addEventListener("click", () => {
- document.getElementById("kw").value = "";
- state.kw = "";
- state.page = 1;
- renderList();
- });
- document.getElementById("kw").addEventListener("keydown", (e) => {
- if (e.key === "Enter") document.getElementById("btnSearch").click();
- });
- document.getElementById("prevPage").addEventListener("click", () => {
- if (state.page > 1) {
- state.page -= 1;
- renderList();
- }
- });
- document.getElementById("nextPage").addEventListener("click", () => {
- state.page += 1;
- renderList();
- });
- document.getElementById("list").addEventListener("click", (e) => {
- const card = e.target.closest(".card");
- if (!card || card.dataset.disabled === "1") return;
- const id = card.dataset.id;
- if (state.selected.has(id)) state.selected.delete(id);
- else state.selected.add(id);
- renderList();
- });
- renderCounts();
- renderList();
- </script>
- </body>
- </html>
|