Files
zkxw/docs/demos/oa-approval-task-filter-demo.html
T
zlwuandCursor 73abb71c65 docs: add OA approval task center filter design and HTML demo
Document server-side filters, instant search UX, and page field matrix for pending/done/mine/cc lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 10:43:52 +08:00

592 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OA 审批任务中心 · 检索区 Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #1e293b;
--text-muted: #64748b;
--border: #d9e8e2;
--bg-page: #fefefe;
--surface: #ffffff;
--table-head: #f8fafc;
--row-alt: #fafafa;
--row-hover: #f1f5f9;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px;
--placeholder: #94a3b8;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: #f1f5f9;
}
.page {
max-width: 1280px;
margin: 0 auto;
padding: 24px;
}
.demo-banner {
margin-bottom: 16px;
padding: 12px 16px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--primary-bg);
color: var(--primary-dark);
font-size: 13px;
}
.demo-banner strong { font-weight: 600; }
.live-toast {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 50;
min-width: 220px;
padding: 10px 14px;
border-radius: 8px;
border: 1px solid #a7f3d0;
background: #fff;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
color: var(--text);
font-size: 13px;
opacity: 0;
transform: translateY(8px);
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.live-toast.show {
opacity: 1;
transform: translateY(0);
}
.live-toast .muted { color: var(--text-muted); font-size: 12px; margin-top: 2px; }
.page-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.page-tab {
height: 36px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text-muted);
font-size: 13px;
font-weight: 500;
cursor: pointer;
font-family: inherit;
}
.page-tab:hover { color: var(--primary-dark); border-color: #a7f3d0; }
.page-tab.active {
background: var(--primary);
border-color: var(--primary);
color: #fff;
font-weight: 600;
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-top {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.panel-top h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.panel-top h2::before {
content: "";
width: 3px;
height: 16px;
border-radius: 2px;
background: var(--primary);
flex-shrink: 0;
}
/* —— 筛选区(对齐 Fresh Mint §5 / unit / order / 抄送我的) —— */
.filter-block {
padding: 12px 16px 10px;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.filter-form { margin: 0; }
.filter-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px 14px;
margin-bottom: 10px;
}
@media (max-width: 1100px) {
.filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
.filter-grid { grid-template-columns: 1fr; }
}
.filter-field label {
display: block;
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 4px;
}
.input, .select {
width: 100%;
height: 36px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
color: var(--text);
background: var(--surface);
font-family: inherit;
outline: none;
}
.input::placeholder {
color: var(--placeholder);
font-size: 13px;
opacity: 1;
}
.input:focus, .select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
}
.select {
appearance: none;
padding-right: 32px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M3 4.5L6 8l3-3.5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
}
.date-range {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.date-range .input { flex: 1; min-width: 0; }
.date-range .sep {
color: var(--text-muted);
font-size: 12px;
flex-shrink: 0;
}
.filter-actions-row {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 10px;
margin-top: 0;
}
.filter-actions-end {
display: flex;
gap: 8px;
align-items: center;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
border: 1px solid transparent;
}
.btn-ghost {
background: var(--surface);
border-color: var(--border);
color: var(--text);
}
.btn-ghost:hover { background: var(--row-hover); }
.btn-main {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-main:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
.btn svg { width: 14px; height: 14px; flex-shrink: 0; }
.list-body {
border-top: none;
}
.list-wrap { padding: 0; }
table.list-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.list-table th,
.list-table td {
padding: 12px 10px;
text-align: center;
border-bottom: 1px solid #eef2f7;
color: var(--text);
}
.list-table th {
background: var(--table-head);
color: var(--text-muted);
font-weight: 600;
font-size: 12px;
}
.list-table tbody tr:nth-child(even) { background: var(--row-alt); }
.list-table tbody tr:hover { background: var(--row-hover); }
.tag {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.tag.warning { background: #fff7ed; color: #c2410c; }
.tag.success { background: var(--primary-bg); color: var(--primary-dark); }
.tag.danger { background: #fef2f2; color: #dc2626; }
.tag.info { background: #f1f5f9; color: #64748b; }
.btn-op {
display: inline-flex;
align-items: center;
gap: 4px;
height: 28px;
padding: 0 10px;
border-radius: 6px;
border: 1px solid transparent;
font-size: 12px;
font-weight: 500;
cursor: pointer;
font-family: inherit;
}
.btn-op.primary {
background: var(--primary-bg);
color: var(--primary-dark);
border-color: #a7f3d0;
}
.btn-op.neutral {
background: var(--surface);
color: var(--text);
border-color: var(--border);
}
.pagination {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 13px;
}
.pager-btn {
height: 28px;
min-width: 28px;
padding: 0 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
cursor: pointer;
font-family: inherit;
}
.pager-btn.active {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.field-note {
margin-top: 12px;
padding: 10px 12px;
border-radius: 8px;
background: #f8fafc;
border: 1px dashed var(--border);
font-size: 12px;
color: var(--text-muted);
}
.field-note code {
font-size: 11px;
background: #e2e8f0;
padding: 1px 5px;
border-radius: 4px;
color: var(--text);
}
[data-only] { display: none; }
</style>
</head>
<body>
<div class="page">
<div class="demo-banner">
<strong>OA 审批 · 任务中心检索区 Demo</strong>
—— 样式对齐 Fresh Mint;交互为<strong>即时检索</strong>:输入/选择条件后防抖约 300ms 自动请求,无需点「搜索」。试着改下方任一条件即可看到触发提示。
</div>
<div class="page-tabs" role="tablist">
<button type="button" class="page-tab active" data-tab="pending">待办中心</button>
<button type="button" class="page-tab" data-tab="done">已办中心</button>
<button type="button" class="page-tab" data-tab="mine">我发起的</button>
<button type="button" class="page-tab" data-tab="cc">抄送我的</button>
</div>
<section class="panel">
<div class="panel-top">
<h2 id="panelTitle">待办中心</h2>
</div>
<div class="filter-block">
<form class="filter-form" onsubmit="return false;">
<div class="filter-grid">
<div class="filter-field">
<label for="appName">流程名称</label>
<input id="appName" class="input" type="text" placeholder="请输入流程名称" />
</div>
<div class="filter-field" data-only="pending,done,cc">
<label for="initiator">申请人</label>
<input id="initiator" class="input" type="text" placeholder="请输入申请人" />
</div>
<div class="filter-field">
<label>申请时间</label>
<div class="date-range">
<input class="input" type="date" aria-label="申请开始日期" />
<span class="sep">至</span>
<input class="input" type="date" aria-label="申请结束日期" />
</div>
</div>
<div class="filter-field">
<label for="status">流程状态</label>
<select id="status" class="select">
<option value="">全部</option>
<option value="running">进行中</option>
<option value="approved">已同意</option>
<option value="rejected">已拒绝</option>
<option value="returned">已打回</option>
<option value="cancelled">已撤销</option>
<option value="completed">已完成</option>
</select>
</div>
<div class="filter-field" data-only="cc">
<label for="readStatus">阅读状态</label>
<select id="readStatus" class="select">
<option value="unread">未读</option>
<option value="read">已读</option>
<option value="all">全部</option>
</select>
</div>
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<button type="button" class="btn btn-ghost" id="btnReset">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9"/><path d="M3 4v5h5"/></svg>
重置
</button>
<button type="button" class="btn btn-main" id="btnSearch">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
搜索
</button>
</div>
</div>
</form>
<div class="field-note" id="fieldNote">
交互:条件变更 → 防抖 300ms → 自动检索(页码回第 1 页)。本页字段:<code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>
</div>
</div>
<div class="list-body list-wrap">
<table class="list-table">
<thead>
<tr>
<th style="width:68px">序号</th>
<th>流程名称</th>
<th>申请人</th>
<th>申请时间</th>
<th>当前节点</th>
<th>当前审批人</th>
<th>流程状态</th>
<th data-only="cc">阅读状态</th>
<th style="min-width:120px">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>请假审批</td>
<td>张三</td>
<td>2026-08-10 09:20:00</td>
<td>部门负责人</td>
<td>李四</td>
<td><span class="tag warning">进行中</span></td>
<td data-only="cc"><span class="tag danger">未读</span></td>
<td><button type="button" class="btn-op primary">查看</button></td>
</tr>
<tr>
<td>2</td>
<td>加班申请</td>
<td>王五</td>
<td>2026-08-09 18:05:00</td>
<td>人事复核</td>
<td>赵六</td>
<td><span class="tag warning">进行中</span></td>
<td data-only="cc"><span class="tag info">已读</span></td>
<td><button type="button" class="btn-op primary">查看</button></td>
</tr>
<tr>
<td>3</td>
<td>差旅报销</td>
<td>陈七</td>
<td>2026-08-08 11:30:00</td>
<td>—</td>
<td>—</td>
<td><span class="tag success">已同意</span></td>
<td data-only="cc"><span class="tag info">已读</span></td>
<td>
<button type="button" class="btn-op primary">查看</button>
<button type="button" class="btn-op neutral" style="margin-left:6px">打印</button>
</td>
</tr>
</tbody>
</table>
<div class="pagination">
<span>共 3 条</span>
<button type="button" class="pager-btn">10条/页</button>
<button type="button" class="pager-btn">‹</button>
<button type="button" class="pager-btn active">1</button>
<button type="button" class="pager-btn">›</button>
</div>
</div>
</section>
</div>
<div class="live-toast" id="liveToast" aria-live="polite">
<div><strong>已触发即时检索</strong></div>
<div class="muted" id="liveToastDetail">防抖 300ms · 页码重置为 1</div>
</div>
<script>
const titles = {
pending: '待办中心',
done: '已办中心',
mine: '我发起的',
cc: '抄送我的'
};
const notes = {
pending: '交互:条件变更 → 防抖 300ms → 自动检索。字段:<code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>',
done: '交互:条件变更 → 防抖 300ms → 自动检索。字段:<code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>',
mine: '交互:条件变更 → 防抖 300ms → 自动检索。字段:<code>流程名称</code>· <code>申请时间</code>· <code>流程状态</code>(不放申请人)',
cc: '交互:条件变更 → 防抖 300ms → 自动检索。字段:现有 <code>阅读状态</code> + <code>流程名称</code>· <code>申请人</code>· <code>申请时间</code>· <code>流程状态</code>'
};
let toastTimer = null;
let debounceTimer = null;
let searchSeq = 0;
function showLiveSearch(reason) {
searchSeq += 1;
const el = document.getElementById('liveToast');
const detail = document.getElementById('liveToastDetail');
detail.textContent = reason + ' · 防抖 300ms · 页码重置为 1 · #' + searchSeq;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 1600);
}
function scheduleLiveSearch(reason) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => showLiveSearch(reason), 300);
}
function applyTab(tab) {
document.getElementById('panelTitle').textContent = titles[tab];
document.getElementById('fieldNote').innerHTML = notes[tab];
document.querySelectorAll('[data-only]').forEach((el) => {
const keys = el.getAttribute('data-only').split(',');
const show = keys.includes(tab);
if (el.tagName === 'TH' || el.tagName === 'TD') {
el.style.display = show ? 'table-cell' : 'none';
} else {
el.style.display = show ? '' : 'none';
}
});
}
document.querySelectorAll('.page-tab').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('.page-tab').forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
applyTab(btn.dataset.tab);
});
});
document.querySelectorAll('.filter-form .input, .filter-form .select').forEach((el) => {
const evt = el.tagName === 'SELECT' || el.type === 'date' ? 'change' : 'input';
el.addEventListener(evt, () => {
const label = el.closest('.filter-field')?.querySelector('label')?.textContent || '条件';
scheduleLiveSearch('变更「' + label + '」');
});
});
document.getElementById('btnReset').addEventListener('click', () => {
document.querySelectorAll('.filter-form .input').forEach((el) => { el.value = ''; });
document.querySelectorAll('.filter-form .select').forEach((el) => {
el.selectedIndex = 0;
});
scheduleLiveSearch('重置条件');
});
document.getElementById('btnSearch').addEventListener('click', () => {
clearTimeout(debounceTimer);
showLiveSearch('点击「搜索」立即检索');
});
applyTab('pending');
</script>
</body>
</html>