This commit is contained in:
zlwu committed 2026-08-11 11:13:01 +08:00
1 parent 80138c6388
commit 693e8a678e
682 files changed
+211375 -5306

No files matched your search

@@ -0,0 +1,720 @@
<!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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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>