696 lines
24 KiB
HTML
696 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||
<title>待办中心 · 批量通过 / 打回 Demo</title>
|
||
<style>
|
||
:root {
|
||
--brand: #0a9f6d;
|
||
--brand-dark: #087f57;
|
||
--brand-soft: #e9f9f2;
|
||
--warn: #d97706;
|
||
--warn-soft: #fff7e8;
|
||
--danger: #ef4444;
|
||
--danger-soft: #fef2f2;
|
||
--nav-bg: #ecfdf5;
|
||
--page-bg: #f5f8f7;
|
||
--text: #1f2a26;
|
||
--text-muted: #6b7b74;
|
||
--border: #e8eaed;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||
color: var(--text);
|
||
background:
|
||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||
line-height: 1.45;
|
||
}
|
||
.stage { max-width: 1180px; margin: 0 auto; padding: 28px 16px 64px; }
|
||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 820px; }
|
||
.rec {
|
||
margin: 16px 0 0;
|
||
padding: 14px 16px;
|
||
border-radius: 14px;
|
||
background: var(--brand-soft);
|
||
border: 1px solid #b7ebd4;
|
||
font-size: 13px;
|
||
color: #166534;
|
||
line-height: 1.55;
|
||
}
|
||
.rec b { color: var(--brand-dark); }
|
||
|
||
.tabs {
|
||
display: flex; flex-wrap: wrap; gap: 10px;
|
||
margin: 20px 0 18px;
|
||
}
|
||
.tab {
|
||
appearance: none; border: 1px solid #e2e8f0; background: #fff;
|
||
border-radius: 999px; padding: 10px 16px; font-size: 13px;
|
||
font-weight: 600; color: #475569; cursor: pointer;
|
||
}
|
||
.tab.is-active {
|
||
color: var(--brand-dark); border-color: var(--brand);
|
||
background: var(--brand-soft);
|
||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.16);
|
||
}
|
||
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||
gap: 20px; align-items: start;
|
||
}
|
||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||
|
||
.panel {
|
||
background: rgba(255,255,255,.94);
|
||
border: 1px solid #e2e8f0; border-radius: 16px; padding: 16px;
|
||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||
}
|
||
.panel h2 { margin: 0 0 4px; font-size: 15px; }
|
||
.panel .sub { margin: 0 0 10px; font-size: 12px; color: #64748b; }
|
||
.panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
|
||
.panel li { margin: 6px 0; }
|
||
.panel .pick {
|
||
margin-top: 12px; padding: 10px 12px; border-radius: 10px;
|
||
background: #fff7e8; border: 1px solid #fde68a;
|
||
font-size: 12px; color: #92400e;
|
||
}
|
||
.panel .ok {
|
||
margin-top: 10px; padding: 10px 12px; border-radius: 10px;
|
||
background: #ecfdf5; border: 1px solid #a7f3d0;
|
||
font-size: 12px; color: #047857;
|
||
}
|
||
|
||
.phones { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
|
||
.phone {
|
||
width: 375px; background: #fff; border-radius: 28px; overflow: hidden;
|
||
box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
|
||
position: relative;
|
||
}
|
||
.cap {
|
||
text-align: center; font-size: 11px; font-weight: 650; color: #64748b;
|
||
padding: 8px; background: #f8fafc; border-bottom: 1px solid #eef2f0;
|
||
}
|
||
|
||
.nav {
|
||
height: 44px; display: flex; align-items: center; padding: 0 8px 0 4px;
|
||
background: var(--nav-bg); border-bottom: 1px solid #d8f3e5;
|
||
position: relative;
|
||
}
|
||
.nav-back {
|
||
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
|
||
color: #0f291e; flex-shrink: 0;
|
||
}
|
||
.nav-back::before {
|
||
content: ""; width: 10px; height: 10px;
|
||
border-left: 2px solid #0f291e; border-bottom: 2px solid #0f291e;
|
||
transform: rotate(45deg); margin-left: 4px;
|
||
}
|
||
.nav-title {
|
||
position: absolute; left: 50%; transform: translateX(-50%);
|
||
font-size: 16px; font-weight: 650; color: #0f291e;
|
||
}
|
||
.nav-action {
|
||
margin-left: auto;
|
||
min-width: 48px;
|
||
height: 36px;
|
||
padding: 0 10px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: 650;
|
||
color: var(--brand-dark);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.nav-action.is-done { color: #64748b; }
|
||
.nav-action:active { opacity: 0.72; }
|
||
|
||
.page {
|
||
min-height: 640px;
|
||
background: var(--page-bg);
|
||
padding-bottom: 24px;
|
||
position: relative;
|
||
}
|
||
|
||
.toolbar {
|
||
padding: 10px 12px;
|
||
background: #fff;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
}
|
||
.search-box {
|
||
height: 36px;
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 0 10px; border-radius: 10px;
|
||
background: #f1f5f9; border: 1px solid transparent;
|
||
}
|
||
.search-ico {
|
||
width: 13px; height: 13px; border: 1.5px solid #94a3b8; border-radius: 50%;
|
||
position: relative; flex-shrink: 0;
|
||
}
|
||
.search-ico::after {
|
||
content: ""; position: absolute; width: 6px; height: 1.5px;
|
||
right: -4px; bottom: -2px; background: #94a3b8; border-radius: 2px;
|
||
transform: rotate(45deg);
|
||
}
|
||
.search-ph { font-size: 13px; color: #b0b7c3; }
|
||
|
||
.batch-bar {
|
||
display: none;
|
||
align-items: center; justify-content: space-between; gap: 8px;
|
||
margin: 8px 12px 0; padding: 10px 12px;
|
||
border-radius: 10px; background: #fff;
|
||
border: 1px solid var(--border);
|
||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
|
||
}
|
||
.batch-bar.is-show { display: flex; }
|
||
.batch-bar__hint { font-size: 13px; color: #646a73; flex-shrink: 0; }
|
||
.batch-bar__actions { display: flex; gap: 8px; margin-left: auto; }
|
||
.batch-bar__btn {
|
||
min-height: 32px; padding: 0 14px;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
border-radius: 8px; font-size: 13px; font-weight: 600;
|
||
border: none; cursor: pointer;
|
||
}
|
||
.batch-bar__btn.is-ok {
|
||
color: #fff; background: var(--brand);
|
||
}
|
||
.batch-bar__btn.is-return {
|
||
color: var(--warn); background: var(--warn-soft);
|
||
border: 1px solid #fde68a;
|
||
}
|
||
.batch-bar__btn:disabled {
|
||
opacity: 0.45; cursor: not-allowed;
|
||
}
|
||
|
||
.list {
|
||
padding: 10px 12px 0;
|
||
display: flex; flex-direction: column; gap: 10px;
|
||
}
|
||
.row {
|
||
display: flex; align-items: stretch; gap: 0;
|
||
}
|
||
.check {
|
||
display: none; align-items: flex-start;
|
||
padding: 18px 0 0 2px; margin-right: 6px; flex-shrink: 0;
|
||
cursor: pointer;
|
||
}
|
||
.page.is-manage .check { display: flex; }
|
||
.check__box {
|
||
width: 20px; height: 20px; border-radius: 50%;
|
||
border: 1.5px solid #cbd5e1; background: #fff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 12px; color: #fff; font-weight: 700;
|
||
}
|
||
.check__box.on {
|
||
background: var(--brand); border-color: var(--brand);
|
||
}
|
||
|
||
.card {
|
||
flex: 1; min-width: 0;
|
||
background: #fff; border-radius: 14px;
|
||
border: 1px solid #d8f3e5;
|
||
box-shadow: 0 4px 16px rgba(22, 163, 74, 0.07);
|
||
padding: 12px 14px; cursor: pointer;
|
||
}
|
||
.card.is-selected {
|
||
border-color: #86efac;
|
||
box-shadow: 0 0 0 2px rgba(10, 159, 109, 0.18);
|
||
}
|
||
.card__head {
|
||
display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
|
||
}
|
||
.card__title {
|
||
font-size: 15px; font-weight: 700; color: #0f291e; line-height: 1.35;
|
||
}
|
||
.card__time { flex-shrink: 0; font-size: 11px; color: #94a3b8; margin-top: 2px; }
|
||
.card__meta { margin-top: 8px; }
|
||
.card__meta-line {
|
||
display: block; font-size: 12px; color: var(--text-muted); line-height: 1.5;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.card__foot {
|
||
margin-top: 10px; display: flex; align-items: center; justify-content: space-between;
|
||
}
|
||
.card__user { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
||
.card__avatar {
|
||
width: 22px; height: 22px; border-radius: 6px;
|
||
background: var(--brand); color: #fff;
|
||
font-size: 10px; font-weight: 700;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.card__user-text { font-size: 12px; color: #64748b; }
|
||
.card__status {
|
||
font-size: 11px; font-weight: 650; padding: 3px 8px; border-radius: 999px;
|
||
background: rgba(217, 119, 6, 0.12); color: var(--warn);
|
||
}
|
||
|
||
/* 底部意见弹层 */
|
||
.sheet-mask {
|
||
display: none; position: absolute; inset: 0;
|
||
background: rgba(15, 23, 42, 0.45); z-index: 20;
|
||
}
|
||
.sheet-mask.is-show { display: block; }
|
||
.sheet {
|
||
position: absolute; left: 0; right: 0; bottom: 0; z-index: 21;
|
||
background: #fff; border-radius: 16px 16px 0 0;
|
||
padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||
transform: translateY(110%);
|
||
transition: transform .22s ease;
|
||
box-shadow: 0 -8px 28px rgba(15, 23, 42, 0.12);
|
||
}
|
||
.sheet.is-show { transform: translateY(0); }
|
||
.sheet__hd {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
.sheet__title { font-size: 16px; font-weight: 700; }
|
||
.sheet__close {
|
||
border: none; background: transparent; color: #64748b;
|
||
font-size: 13px; cursor: pointer; padding: 4px 0;
|
||
}
|
||
.sheet__hint {
|
||
margin: 0 0 8px; font-size: 12px; color: #64748b;
|
||
}
|
||
.sheet__hint strong { color: var(--brand-dark); }
|
||
.sheet__ta {
|
||
width: 100%; min-height: 96px; resize: none;
|
||
border: 1px solid #e2e8f0; border-radius: 10px;
|
||
padding: 10px 12px; font: inherit; font-size: 14px;
|
||
color: var(--text); outline: none;
|
||
background: #f8fafc;
|
||
}
|
||
.sheet__ta:focus {
|
||
border-color: #86efac; background: #fff;
|
||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.12);
|
||
}
|
||
.sheet__ft {
|
||
margin-top: 12px; display: flex; gap: 10px;
|
||
}
|
||
.sheet__btn {
|
||
flex: 1; height: 40px; border-radius: 10px;
|
||
border: none; font-size: 15px; font-weight: 650; cursor: pointer;
|
||
}
|
||
.sheet__btn.is-cancel {
|
||
background: #f1f5f9; color: #475569;
|
||
}
|
||
.sheet__btn.is-ok {
|
||
background: var(--brand); color: #fff;
|
||
}
|
||
.sheet__btn.is-return {
|
||
background: var(--warn); color: #fff;
|
||
}
|
||
|
||
.toast {
|
||
position: absolute; left: 50%; bottom: 88px; z-index: 30;
|
||
transform: translateX(-50%);
|
||
padding: 8px 14px; border-radius: 8px;
|
||
background: rgba(15, 23, 42, 0.88); color: #fff;
|
||
font-size: 13px; white-space: nowrap;
|
||
opacity: 0; pointer-events: none; transition: opacity .2s;
|
||
}
|
||
.toast.is-show { opacity: 1; }
|
||
|
||
.legend {
|
||
margin-top: 22px;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.legend .box {
|
||
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
|
||
padding: 12px 14px; font-size: 12px; color: #475569;
|
||
}
|
||
.legend h3 { margin: 0 0 6px; font-size: 13px; color: #0f291e; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="stage">
|
||
<div class="hero">
|
||
<h1>待办中心 · 批量通过 / 打回</h1>
|
||
<p>
|
||
交互对齐抄送「管理 + 多选 + 底栏批量操作」。右侧手机可点击演示:进入管理 → 勾选 → 批量通过 / 打回 → 填写统一意见。
|
||
</p>
|
||
<div class="rec">
|
||
<b>推荐方案(本 Demo):</b>
|
||
<b>标题栏右侧「管理 / 完成」</b>(对齐草稿箱自定义顶栏);底栏「批量通过」「批量打回」;
|
||
统一意见框(通过默认「同意」可空,打回必填);前端串行调现有单条接口,汇总成功/失败。不含批量拒绝。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs" id="sceneTabs">
|
||
<button type="button" class="tab is-active" data-scene="browse">① 浏览态</button>
|
||
<button type="button" class="tab" data-scene="manage">② 管理多选</button>
|
||
<button type="button" class="tab" data-scene="approve">③ 批量通过弹层</button>
|
||
<button type="button" class="tab" data-scene="return">④ 批量打回弹层</button>
|
||
</div>
|
||
|
||
<div class="layout">
|
||
<aside class="panel" id="sidePanel">
|
||
<h2>① 浏览态</h2>
|
||
<p class="sub">日常查看 / 点卡片进详情审批</p>
|
||
<ul>
|
||
<li>标题栏右侧「管理」(对齐草稿箱自定义顶栏)</li>
|
||
<li>搜索区仅保留检索,不再放管理按钮</li>
|
||
<li>点卡片仍进入审批详情(webview)</li>
|
||
</ul>
|
||
<div class="pick">右侧手机可点标题栏「管理」亲自走一遍流程。</div>
|
||
</aside>
|
||
|
||
<div class="phones">
|
||
<div class="phone">
|
||
<div class="cap">可交互 Demo · 375px</div>
|
||
<div class="nav">
|
||
<div class="nav-back" aria-hidden="true"></div>
|
||
<div class="nav-title">待办中心</div>
|
||
<div class="nav-action" id="manageBtn">管理</div>
|
||
</div>
|
||
<div class="page" id="page">
|
||
<div class="toolbar">
|
||
<div class="search-box">
|
||
<span class="search-ico" aria-hidden="true"></span>
|
||
<span class="search-ph">搜索流程名称、申请人</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="batch-bar" id="batchBar">
|
||
<span class="batch-bar__hint" id="batchHint">已选 0 项</span>
|
||
<div class="batch-bar__actions">
|
||
<button type="button" class="batch-bar__btn is-return" id="btnReturn">批量打回</button>
|
||
<button type="button" class="batch-bar__btn is-ok" id="btnApprove">批量通过</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="list" id="list"></div>
|
||
|
||
<div class="sheet-mask" id="sheetMask"></div>
|
||
<div class="sheet" id="sheet">
|
||
<div class="sheet__hd">
|
||
<div class="sheet__title" id="sheetTitle">批量通过</div>
|
||
<button type="button" class="sheet__close" id="sheetClose">取消</button>
|
||
</div>
|
||
<p class="sheet__hint" id="sheetHint">将对 <strong>2</strong> 条待办执行操作</p>
|
||
<textarea class="sheet__ta" id="sheetInput" placeholder="审批意见"></textarea>
|
||
<div class="sheet__ft">
|
||
<button type="button" class="sheet__btn is-cancel" id="sheetCancel">取消</button>
|
||
<button type="button" class="sheet__btn is-ok" id="sheetConfirm">确定</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="legend">
|
||
<div class="box">
|
||
<h3>意见规则</h3>
|
||
批量通过:默认填入「同意」,可改,可空(与单条一致)。<br />
|
||
批量打回:意见必填,占位「请填写打回意见(必填)」。
|
||
</div>
|
||
<div class="box">
|
||
<h3>接口策略</h3>
|
||
暂无批量 API:前端对选中 taskId 串行调用<br />
|
||
<code>POST /tasks/{id}/approve</code>、<code>/return</code>。<br />
|
||
结束后 toast:「成功 n,失败 m」。
|
||
</div>
|
||
<div class="box">
|
||
<h3>边界</h3>
|
||
仅当前页已加载列表可勾选;不包含「拒绝」。<br />
|
||
若流程关闭打回(allowReturn=false),单条失败计入失败数。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const TASKS = [
|
||
{
|
||
id: 't1',
|
||
title: '请假申请',
|
||
time: '今天 09:12',
|
||
meta: ['请假类型:事假', '请假天数:1'],
|
||
user: '王品龙',
|
||
avatar: '王品'
|
||
},
|
||
{
|
||
id: 't2',
|
||
title: '费用报销',
|
||
time: '昨天 18:40',
|
||
meta: ['报销金额:1280.00', '事由:差旅交通'],
|
||
user: '吴家猛',
|
||
avatar: '吴家'
|
||
},
|
||
{
|
||
id: 't3',
|
||
title: '加班申请',
|
||
time: '08-10',
|
||
meta: ['加班时长:3小时', '加班日期:2026-08-09'],
|
||
user: '郑德禄',
|
||
avatar: '郑德'
|
||
},
|
||
{
|
||
id: 't4',
|
||
title: '出差申请',
|
||
time: '08-09',
|
||
meta: ['目的地:合肥', '出差天数:2'],
|
||
user: '李明',
|
||
avatar: '李明'
|
||
}
|
||
]
|
||
|
||
const state = {
|
||
manage: false,
|
||
selected: new Set(),
|
||
sheetKind: null // approve | return
|
||
}
|
||
|
||
const page = document.getElementById('page')
|
||
const listEl = document.getElementById('list')
|
||
const manageBtn = document.getElementById('manageBtn')
|
||
const batchBar = document.getElementById('batchBar')
|
||
const batchHint = document.getElementById('batchHint')
|
||
const btnApprove = document.getElementById('btnApprove')
|
||
const btnReturn = document.getElementById('btnReturn')
|
||
const sheet = document.getElementById('sheet')
|
||
const sheetMask = document.getElementById('sheetMask')
|
||
const sheetTitle = document.getElementById('sheetTitle')
|
||
const sheetHint = document.getElementById('sheetHint')
|
||
const sheetInput = document.getElementById('sheetInput')
|
||
const sheetConfirm = document.getElementById('sheetConfirm')
|
||
const toastEl = document.getElementById('toast')
|
||
const sidePanel = document.getElementById('sidePanel')
|
||
|
||
const SCENE_COPY = {
|
||
browse: {
|
||
title: '① 浏览态',
|
||
sub: '日常查看 / 点卡片进详情审批',
|
||
items: [
|
||
'标题栏右侧「管理」(对齐草稿箱自定义顶栏)',
|
||
'搜索区仅保留检索,不再放管理按钮',
|
||
'点卡片仍进入审批详情(webview)'
|
||
],
|
||
tip: '右侧手机可点标题栏「管理」亲自走一遍流程。',
|
||
tipClass: 'pick'
|
||
},
|
||
manage: {
|
||
title: '② 管理多选',
|
||
sub: '勾选多条待办,底栏出现批量操作',
|
||
items: [
|
||
'标题栏按钮变为「完成」,点按退出并清空勾选',
|
||
'左侧圆形勾选,选中卡片高亮描边',
|
||
'底栏:已选 N 项 + 批量打回 + 批量通过',
|
||
'未勾选时两个按钮禁用'
|
||
],
|
||
tip: '主行动为「批量通过」(品牌绿);打回为次要警告色。',
|
||
tipClass: 'ok'
|
||
},
|
||
approve: {
|
||
title: '③ 批量通过弹层',
|
||
sub: '统一意见,一次确认',
|
||
items: [
|
||
'标题「批量通过」,提示将对 N 条执行',
|
||
'默认意见「同意」,可修改或清空',
|
||
'确定后串行 approve,刷新列表'
|
||
],
|
||
tip: '与详情页单条「同意」语义一致。',
|
||
tipClass: 'ok'
|
||
},
|
||
return: {
|
||
title: '④ 批量打回弹层',
|
||
sub: '打回意见必填',
|
||
items: [
|
||
'标题「批量打回」,主按钮琥珀警告色',
|
||
'意见为空时 toast「打回意见必填」',
|
||
'部分失败时提示成功/失败条数'
|
||
],
|
||
tip: '不在此 Demo 做「批量拒绝」,避免误操作风险。',
|
||
tipClass: 'pick'
|
||
}
|
||
}
|
||
|
||
function renderSide(scene) {
|
||
const c = SCENE_COPY[scene]
|
||
sidePanel.innerHTML =
|
||
'<h2>' + c.title + '</h2>' +
|
||
'<p class="sub">' + c.sub + '</p>' +
|
||
'<ul>' + c.items.map((x) => '<li>' + x + '</li>').join('') + '</ul>' +
|
||
'<div class="' + c.tipClass + '">' + c.tip + '</div>'
|
||
}
|
||
|
||
function renderList() {
|
||
listEl.innerHTML = TASKS.map((t) => {
|
||
const on = state.selected.has(t.id)
|
||
return (
|
||
'<div class="row" data-id="' + t.id + '">' +
|
||
'<div class="check" data-check="' + t.id + '">' +
|
||
'<div class="check__box' + (on ? ' on' : '') + '">' + (on ? '✓' : '') + '</div>' +
|
||
'</div>' +
|
||
'<div class="card' + (on ? ' is-selected' : '') + '" data-card="' + t.id + '">' +
|
||
'<div class="card__head">' +
|
||
'<div class="card__title">' + t.title + '</div>' +
|
||
'<div class="card__time">' + t.time + '</div>' +
|
||
'</div>' +
|
||
'<div class="card__meta">' +
|
||
t.meta.map((m) => '<span class="card__meta-line">' + m + '</span>').join('') +
|
||
'</div>' +
|
||
'<div class="card__foot">' +
|
||
'<div class="card__user">' +
|
||
'<div class="card__avatar">' + t.avatar + '</div>' +
|
||
'<div class="card__user-text">由' + t.user + '提交</div>' +
|
||
'</div>' +
|
||
'<div class="card__status">进行中</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>'
|
||
)
|
||
}).join('')
|
||
}
|
||
|
||
function syncChrome() {
|
||
page.classList.toggle('is-manage', state.manage)
|
||
manageBtn.textContent = state.manage ? '完成' : '管理'
|
||
manageBtn.classList.toggle('is-done', state.manage)
|
||
const n = state.selected.size
|
||
batchBar.classList.toggle('is-show', state.manage && n > 0)
|
||
batchHint.textContent = '已选 ' + n + ' 项'
|
||
btnApprove.disabled = n === 0
|
||
btnReturn.disabled = n === 0
|
||
}
|
||
|
||
function toast(msg) {
|
||
toastEl.textContent = msg
|
||
toastEl.classList.add('is-show')
|
||
clearTimeout(toast._t)
|
||
toast._t = setTimeout(() => toastEl.classList.remove('is-show'), 1800)
|
||
}
|
||
|
||
function openSheet(kind) {
|
||
const n = state.selected.size
|
||
if (!n) return
|
||
state.sheetKind = kind
|
||
sheetTitle.textContent = kind === 'approve' ? '批量通过' : '批量打回'
|
||
sheetHint.innerHTML = '将对 <strong>' + n + '</strong> 条待办执行操作'
|
||
sheetInput.value = kind === 'approve' ? '同意' : ''
|
||
sheetInput.placeholder =
|
||
kind === 'approve' ? '审批意见(可选)' : '请填写打回意见(必填)'
|
||
sheetConfirm.textContent = '确定'
|
||
sheetConfirm.className =
|
||
'sheet__btn ' + (kind === 'approve' ? 'is-ok' : 'is-return')
|
||
sheetMask.classList.add('is-show')
|
||
sheet.classList.add('is-show')
|
||
setTimeout(() => sheetInput.focus(), 80)
|
||
}
|
||
|
||
function closeSheet() {
|
||
state.sheetKind = null
|
||
sheetMask.classList.remove('is-show')
|
||
sheet.classList.remove('is-show')
|
||
}
|
||
|
||
function setManage(on) {
|
||
state.manage = on
|
||
if (!on) state.selected.clear()
|
||
renderList()
|
||
syncChrome()
|
||
}
|
||
|
||
function applyScene(scene) {
|
||
document.querySelectorAll('#sceneTabs .tab').forEach((el) => {
|
||
el.classList.toggle('is-active', el.dataset.scene === scene)
|
||
})
|
||
renderSide(scene)
|
||
closeSheet()
|
||
if (scene === 'browse') {
|
||
setManage(false)
|
||
} else if (scene === 'manage') {
|
||
state.selected = new Set(['t1', 't2'])
|
||
setManage(true)
|
||
} else if (scene === 'approve') {
|
||
state.selected = new Set(['t1', 't2'])
|
||
setManage(true)
|
||
openSheet('approve')
|
||
} else if (scene === 'return') {
|
||
state.selected = new Set(['t1', 't3'])
|
||
setManage(true)
|
||
openSheet('return')
|
||
}
|
||
}
|
||
|
||
manageBtn.addEventListener('click', () => setManage(!state.manage))
|
||
|
||
listEl.addEventListener('click', (e) => {
|
||
const check = e.target.closest('[data-check]')
|
||
const card = e.target.closest('[data-card]')
|
||
const id = (check && check.dataset.check) || (card && card.dataset.card)
|
||
if (!id) return
|
||
if (state.manage) {
|
||
if (state.selected.has(id)) state.selected.delete(id)
|
||
else state.selected.add(id)
|
||
renderList()
|
||
syncChrome()
|
||
return
|
||
}
|
||
if (card) toast('进入审批详情(Demo)')
|
||
})
|
||
|
||
btnApprove.addEventListener('click', () => openSheet('approve'))
|
||
btnReturn.addEventListener('click', () => openSheet('return'))
|
||
sheetMask.addEventListener('click', closeSheet)
|
||
document.getElementById('sheetClose').addEventListener('click', closeSheet)
|
||
document.getElementById('sheetCancel').addEventListener('click', closeSheet)
|
||
|
||
sheetConfirm.addEventListener('click', () => {
|
||
const kind = state.sheetKind
|
||
const comment = String(sheetInput.value || '').trim()
|
||
const n = state.selected.size
|
||
if (kind === 'return' && !comment) {
|
||
toast('打回意见必填')
|
||
return
|
||
}
|
||
closeSheet()
|
||
const label = kind === 'approve' ? '通过' : '打回'
|
||
toast('已' + label + ' ' + n + ' 条(Demo)')
|
||
state.selected.clear()
|
||
setManage(false)
|
||
})
|
||
|
||
document.getElementById('sceneTabs').addEventListener('click', (e) => {
|
||
const tab = e.target.closest('.tab')
|
||
if (!tab) return
|
||
applyScene(tab.dataset.scene)
|
||
})
|
||
|
||
renderList()
|
||
syncChrome()
|
||
renderSide('browse')
|
||
</script>
|
||
</body>
|
||
</html>
|