Compare commits
2
Commits
5f41cb05b7
...
072f304cc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
072f304cc2 | ||
|
|
c765755906 |
+643
-21
@@ -187,8 +187,94 @@
|
||||
.empty .ico{width:64px;height:64px;margin:0 auto 12px;border-radius:50%;background:#f2f3f5;display:flex;align-items:center;justify-content:center;color:#c4c8ce;font-size:30px}
|
||||
.empty p{font-size:13px;margin-bottom:12px}
|
||||
|
||||
/* ---------- 表格 ---------- */
|
||||
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e6e8eb;border-radius:10px;overflow:hidden;font-size:13px}
|
||||
/* ---------- 统一主 Tab 头部(V0.2.1 调整:高度减半,标题主色小字) ---------- */
|
||||
.navbar-sm{height:23px;margin-top:28px;flex-shrink:0;display:flex;align-items:center;justify-content:center;
|
||||
font-size:12px;font-weight:600;letter-spacing:1px;color:var(--primary);background:var(--bg)}
|
||||
/* 「我」页:不设头部,头像区直接顶到状态栏下方 */
|
||||
.me-head.flush{padding-top:24px}
|
||||
|
||||
/* ---------- 打卡 · 日历与打卡合并视图(V0.2.1 草案) ---------- */
|
||||
.mv-cal{background:#fff;border-radius:10px;margin:8px 12px 0;padding:6px 6px 8px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
|
||||
.mv-month{display:flex;align-items:center;gap:8px;padding:2px 4px 6px}
|
||||
.mv-month b{font-size:14px}
|
||||
.mv-month .arrow{font-size:17px;color:var(--muted);line-height:1;padding:0 3px}
|
||||
.mv-month .today{margin-left:auto;font-size:11px;color:var(--primary)}
|
||||
.mv-grid{display:grid;grid-template-columns:repeat(7,1fr)}
|
||||
.mv-wd span{text-align:center;font-size:10px;color:var(--muted);padding:1px 0 3px;display:block}
|
||||
.mv-day{height:31px;margin:1px 0;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
border-radius:6px;font-size:12px;line-height:1.15;color:var(--text)}
|
||||
.mv-day.out{color:#c8c8c8}
|
||||
.mv-day.future{color:#b0b0b0}
|
||||
.mv-day.today{color:var(--primary);font-weight:700}
|
||||
.mv-day.sel{background:var(--primary);color:#fff;font-weight:600}
|
||||
.mv-day i{width:4px;height:4px;border-radius:50%;margin-top:2px}
|
||||
.mv-day i.partial{background:var(--warn)} .mv-day i.full{background:var(--success)} .mv-day i.over{background:var(--over)}
|
||||
.mv-day.sel i{background:#fff}
|
||||
.mv-rate{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--muted);padding:7px 6px 0}
|
||||
.mv-rate b{color:var(--primary)}
|
||||
.mv-daybar{display:flex;align-items:center;gap:8px;padding:10px 14px 2px;font-size:13px;font-weight:600}
|
||||
.mv-daybar .badge{font-weight:400}
|
||||
.mv-daybar .sum{margin-left:auto;font-size:11px;color:var(--muted);font-weight:400}
|
||||
.mv-groups{padding-bottom:12px}
|
||||
.mv-ghead{display:flex;align-items:center;gap:6px;padding:9px 14px 5px;font-size:11px;font-weight:600;color:var(--muted)}
|
||||
.mv-ghead::before{content:"";width:3px;height:11px;border-radius:2px;background:var(--primary)}
|
||||
.mv-ghead .cnt{margin-left:auto;font-weight:400;color:var(--weak)}
|
||||
.mv-card{position:relative;display:flex;align-items:center;gap:9px;background:#fff;border-radius:9px;
|
||||
padding:8px 10px;margin:0 12px 7px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
|
||||
.mv-card.done{background:#f7fbf8}
|
||||
.mv-ring{width:42px;height:42px;position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.mv-ring svg{display:block}
|
||||
.mv-ring .c{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}
|
||||
.mv-ring .c em{font-style:normal;font-size:9px;color:var(--muted);font-weight:400}
|
||||
.mv-ring .ov{position:absolute;top:-3px;right:-5px;font-size:9px;color:#fff;background:var(--orange);border-radius:999px;padding:0 4px}
|
||||
.mv-main{flex:1;min-width:0}
|
||||
.mv-name{font-size:14px;font-weight:600;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.mv-meta{font-size:10px;color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.mv-meta .st{color:var(--orange);background:rgba(255,143,31,.12);border-radius:3px;padding:0 4px;margin-left:5px}
|
||||
.mv-btn{font-size:11px;border-radius:999px;padding:4px 12px;background:var(--primary);color:#fff;flex-shrink:0;white-space:nowrap}
|
||||
.mv-btn.grey{background:#f2f3f5;color:var(--muted)}
|
||||
.mv-more{color:#c0c4cc;font-size:15px;line-height:1;padding:4px 2px 4px 6px;flex-shrink:0}
|
||||
/* 操作面板(⋯ 触发) */
|
||||
.mv-sheet .sheet-title{font-size:15px;margin-bottom:8px}
|
||||
.mv-sheet-row{display:flex;align-items:center;justify-content:space-between;padding:12px 4px;border-top:1px solid var(--line);font-size:14px}
|
||||
.mv-sheet-row.danger{color:var(--danger)}
|
||||
.mv-sheet-row .k{font-size:11px;color:var(--weak)}
|
||||
|
||||
/* ---------- 清单 · 模板分类(V0.2.1 草案) ---------- */
|
||||
.tpl-card{background:#fff;border-radius:10px;padding:11px 12px;margin:0 12px 10px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
|
||||
.tpl-row{display:flex;align-items:center;gap:7px}
|
||||
.tpl-name{font-size:14px;font-weight:600}
|
||||
.tpl-badge{font-size:10px;color:var(--muted);background:#f2f3f5;border-radius:4px;padding:1px 5px}
|
||||
.tpl-num{margin-left:auto;font-size:11px;color:var(--muted)}
|
||||
.tpl-chips{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}
|
||||
.tpl-chip{font-size:10px;color:var(--muted);background:#f7f8fa;border-radius:4px;padding:2px 6px}
|
||||
.tpl-chip.more{color:var(--primary);background:#f0f5ff}
|
||||
.tpl-acts{display:flex;gap:14px;margin-top:9px;padding-top:7px;border-top:1px dashed var(--line)}
|
||||
/* 新建清单(无场景,模板从右侧滑出选择) */
|
||||
.pick-row{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:14px;color:var(--weak)}
|
||||
.pick-row .meta{margin-left:auto;display:flex;align-items:center;gap:4px;font-size:11px;color:var(--muted)}
|
||||
.side-panel{position:absolute;top:28px;right:0;bottom:0;width:78%;background:#fff;z-index:11;
|
||||
box-shadow:-6px 0 18px rgba(15,23,42,.12);display:flex;flex-direction:column;padding:14px 0 0}
|
||||
.side-head{display:flex;align-items:center;padding:0 16px 4px}
|
||||
.side-head b{font-size:16px}
|
||||
.side-head .x{margin-left:auto;font-size:20px;color:#c8c8c8;line-height:1}
|
||||
.side-tip{font-size:11px;color:var(--muted);padding:0 16px 8px}
|
||||
.side-item{display:flex;align-items:baseline;gap:8px;padding:11px 16px}
|
||||
.side-item.on{background:#f0f5ff}
|
||||
.side-item .nm{flex:1;font-size:14px;font-weight:600;display:flex;align-items:center;gap:6px}
|
||||
.side-item.on .nm{color:var(--primary)}
|
||||
.side-item .sub{font-size:11px;color:var(--muted)}
|
||||
|
||||
/* ---------- 宏观 · tab 溢出提示(V0.2.1 草案) ---------- */
|
||||
.mtabs-wrap{position:relative;background:#fff;border-bottom:1px solid var(--line)}
|
||||
.mtabs-wrap .mtabs{border-bottom:none}
|
||||
.mtabs-fade{position:absolute;top:0;right:0;bottom:0;width:52px;display:flex;align-items:center;justify-content:flex-end;
|
||||
padding-right:8px;background:linear-gradient(to right,rgba(255,255,255,0),#fff 55%,#fff)}
|
||||
.mtabs-fade .hint{width:19px;height:19px;border-radius:50%;background:#fff;border:1px solid #e3e6ea;
|
||||
display:flex;align-items:center;justify-content:center;color:var(--primary);font-size:13px;font-weight:700;
|
||||
box-shadow:0 1px 4px rgba(0,0,0,.14)}
|
||||
|
||||
/* ---------- 表格 ---------- */ table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e6e8eb;border-radius:10px;overflow:hidden;font-size:13px}
|
||||
th,td{border-bottom:1px solid var(--line);padding:9px 12px;text-align:left;vertical-align:middle}
|
||||
th{background:#fafbfc;font-weight:600;color:#444;white-space:nowrap}
|
||||
tr:last-child td{border-bottom:none}
|
||||
@@ -244,7 +330,7 @@
|
||||
<p>打卡 / 日历 / 清单 —— 移动端静态高保真原型(设计基线 420 × 820,适配 375~430px)</p>
|
||||
<div class="meta">
|
||||
<span>版本 V0.2(0.2.0)</span><span>分支 dev-0.2</span><span>antd-mobile 风格</span>
|
||||
<span>底部 4 Tab(打卡默认)</span><span>时区 Asia/Shanghai</span><span>文档日期 2026-10-22</span>
|
||||
<span>底部 4 Tab(打卡默认)</span><span>时区 Asia/Shanghai</span><span>文档日期 2026-10-22 · 修订 2026-10-23</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -297,14 +383,14 @@
|
||||
</div>
|
||||
|
||||
<!-- ============ 2 主界面 ============ -->
|
||||
<h2>2 主界面<span class="sub">三个子视图(SwipeTabs 左右滑动切换)</span></h2>
|
||||
<h2>2 主界面<span class="sub">三个子视图(V0.2 现状;合并方案见 2.12 ②)</span></h2>
|
||||
|
||||
<div class="stage">
|
||||
|
||||
<!-- ---------- 2.1 打卡视图 ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="habit-head">
|
||||
<div>
|
||||
@@ -423,7 +509,7 @@
|
||||
<!-- ---------- 2.2 日历视图 ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px">
|
||||
<div>打卡</div><div class="on">日历</div><div>清单</div>
|
||||
@@ -525,13 +611,13 @@
|
||||
<!-- ---------- 2.3 清单视图 ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px">
|
||||
<div>打卡</div><div>日历</div><div class="on">清单</div>
|
||||
</div>
|
||||
<div class="list-head">
|
||||
<div class="seg"><div class="on">进行中</div><div>已归档</div></div>
|
||||
<div class="seg"><div class="on">进行中</div><div>已归档</div><div>模板</div></div>
|
||||
<div class="mini-btn solid">+ 新建</div>
|
||||
</div>
|
||||
|
||||
@@ -589,7 +675,7 @@
|
||||
<!-- ---------- 2.4 新建打卡项弹窗 ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="habit-head">
|
||||
<div>
|
||||
@@ -654,7 +740,7 @@
|
||||
<!-- ---------- 2.5 完成特效 ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="habit-head">
|
||||
<div><div class="habit-date">10月22日 周四</div><div class="habit-summary">已完成 <strong>3</strong> / 4</div></div>
|
||||
@@ -726,7 +812,7 @@
|
||||
<!-- ---------- 2.6 日历·明天(计划) ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div>打卡</div><div class="on">日历</div><div>清单</div></div>
|
||||
<div class="cal-month-bar"><span class="cal-arrow">‹</span><span class="cal-month">2026年10月</span><span class="cal-arrow">›</span><span class="cal-today">回到今天</span></div>
|
||||
@@ -792,11 +878,11 @@
|
||||
<!-- ---------- 2.7 清单完成 + 空态 ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div>打卡</div><div>日历</div><div class="on">清单</div></div>
|
||||
<div class="list-head">
|
||||
<div class="seg"><div class="on">进行中</div><div>已归档</div></div>
|
||||
<div class="seg"><div class="on">进行中</div><div>已归档</div><div>模板</div></div>
|
||||
<div class="mini-btn solid">+ 新建</div>
|
||||
</div>
|
||||
<div class="checklist-card done" style="position:relative;overflow:visible">
|
||||
@@ -839,7 +925,7 @@
|
||||
<!-- ---------- 2.8 空态与归档 ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">打卡</div>
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="habit-head">
|
||||
<div><div class="habit-date">10月22日 周四</div><div class="habit-summary">已完成 <strong>0</strong> / 0</div></div>
|
||||
@@ -871,11 +957,14 @@
|
||||
<!-- ---------- 2.9 宏观(原首页报警/新闻并入) ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">宏观</div>
|
||||
<div class="navbar-sm">宏观</div>
|
||||
<div class="body">
|
||||
<div class="mtabs-wrap">
|
||||
<div class="mtabs">
|
||||
<div>油价</div><div>经济数据</div><div>策略研究</div><div>经济文章</div>
|
||||
<div>报警<span class="badge-dot">3</span></div><div class="on">新闻</div>
|
||||
<div>油价</div><div>经济数据</div><div>策略研究</div><div>经济文章</div>
|
||||
</div>
|
||||
<div class="mtabs-fade"><span class="hint">›</span></div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-card">
|
||||
@@ -913,16 +1002,15 @@
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>2.9 宏观 · 原「首页」报警/新闻并入<span class="tag">设计变更</span></b>
|
||||
<p>「首页」Tab 取消,报警与新闻成为宏观的第 5、6 个子 Tab(<code>/macro/alerts</code>、<code>/macro/news</code>),共 6 个子 Tab 可横向滚动,当前项底部有 3px 主色指示条;报警数量以红色角标挂在子 Tab 上。新闻详情改为 <code>/macro/news/:id</code>,底部「宏观」保持高亮。</p>
|
||||
<p>「首页」Tab 取消,报警与新闻并入宏观。<b>2026-10-23 调整</b>:两者移到<b>最左</b>(报警 → 新闻 → 油价 → 经济数据 → 策略研究 → 经济文章),<b>默认选中「新闻」</b>;共 6 个子 Tab 可横向滚动,当前项底部有 3px 主色指示条;报警数量以红色角标挂在子 Tab 上。tab 条右侧增加<b>渐变遮罩 + 箭头</b>,提示还有未展示的 tab(详见 2.12 ④)。新闻详情为 <code>/macro/news/:id</code>,底部「宏观」保持高亮。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ---------- 2.10 我的 · OTA(开发版本 关 = 默认) ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">我</div>
|
||||
<div class="body">
|
||||
<div class="me-head">
|
||||
<div class="me-head flush">
|
||||
<div class="me-avatar">C</div>
|
||||
<div class="me-name">Cheney</div>
|
||||
<div class="me-mail">qian.huang@sunyard.com</div>
|
||||
@@ -964,9 +1052,8 @@
|
||||
<!-- ---------- 2.11 我的 · OTA(开发版本 开) ---------- -->
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar">我</div>
|
||||
<div class="body">
|
||||
<div class="me-head">
|
||||
<div class="me-head flush">
|
||||
<div class="me-avatar">C</div>
|
||||
<div class="me-name">Cheney</div>
|
||||
<div class="me-mail">qian.huang@sunyard.com</div>
|
||||
@@ -1008,9 +1095,544 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ============ 2.12 V0.2.1 界面调整草案 ============ -->
|
||||
<h2>2.12 V0.2.1 界面调整<span class="sub">头部统一 / 日历·打卡合并 / 模板分类 / 宏观 tab 溢出</span></h2>
|
||||
<p class="desc">本节为 2026-10-23 提出的调整方案。其中 <b>①头部统一 / ③模板分类 / ④宏观 tab 溢出</b> 已按本节实现;
|
||||
<b>②日历与打卡合并</b> 为待确认草案,<b>确认后再改代码</b>。</p>
|
||||
|
||||
<h3>① 四个主 Tab 头部统一</h3>
|
||||
<table>
|
||||
<tr><th>页</th><th>调整前</th><th>调整后</th></tr>
|
||||
<tr><td>打卡</td><td>NavBar 45px,标题「打卡」17px 深色</td><td><b>23px</b>,标题「打卡」12px 主色加粗,字距 1px</td></tr>
|
||||
<tr><td>宏观</td><td>NavBar 45px,<b>无标题</b>(空白)</td><td>23px + 标题「宏观」(同打卡规格)</td></tr>
|
||||
<tr><td>监控</td><td>NavBar 45px,<b>无标题</b>(空白)</td><td>23px + 标题「监控」(同打卡规格)</td></tr>
|
||||
<tr><td>我</td><td>NavBar 45px 空白条</td><td><b>整条去掉</b>,蓝色头像区直接顶到状态栏下方</td></tr>
|
||||
</table>
|
||||
<p class="note">实现为独立组件 <code>PageHeader</code>,只被打卡 / 宏观 / 监控 引用;「我」页不引用,因此三者互不影响。
|
||||
头部背景取页面底色 <code>#f5f5f5</code>(不再是卡片白),视觉上更轻。</p>
|
||||
|
||||
<div class="stage">
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="habit-head">
|
||||
<div><div class="habit-date">10月23日 周五</div><div class="habit-summary">已完成 <strong>2</strong> / 10</div></div>
|
||||
<div class="head-right"><div class="mini-btn solid">+ 新建</div></div>
|
||||
</div>
|
||||
<div class="habit-list">
|
||||
<div class="habit-card">
|
||||
<div class="ring">
|
||||
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||
<circle cx="29" cy="29" r="26" fill="none" stroke="#1677ff" stroke-width="6" stroke-linecap="round" stroke-dasharray="163.4" stroke-dashoffset="81.7" transform="rotate(-90 29 29)"/>
|
||||
</svg><div class="ring-center">1<em>/2</em></div>
|
||||
</div>
|
||||
<div class="habit-main"><div class="habit-name">喝水</div><div class="habit-meta">每天 2 次<span class="streak">连续 12</span></div><div class="habit-range">今日</div></div>
|
||||
<div class="habit-actions"><div class="mini-btn solid">打卡</div></div>
|
||||
<div class="habit-foot"><span class="link">撤销</span><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||
</div>
|
||||
<div class="habit-card">
|
||||
<div class="ring">
|
||||
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||
<circle cx="29" cy="29" r="26" fill="none" stroke="#ececf0" stroke-width="6"/>
|
||||
<circle cx="29" cy="29" r="26" fill="none" stroke="#ff8f1f" stroke-width="6" stroke-linecap="round" stroke-dasharray="163.4" stroke-dashoffset="163.4" transform="rotate(-90 29 29)"/>
|
||||
</svg><div class="ring-center">0<em>/1</em></div>
|
||||
</div>
|
||||
<div class="habit-main"><div class="habit-name">俯卧撑</div><div class="habit-meta">每周 1 次</div><div class="habit-range">10-19 ~ 10-25</div></div>
|
||||
<div class="habit-actions"><div class="mini-btn solid">打卡</div></div>
|
||||
<div class="habit-foot"><span class="link">编辑</span><span class="link danger">归档</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>①-a 打卡(头部已缩至 23px)<span class="tag">已实现</span></b>
|
||||
<p>原 45px 白色 NavBar 换成 23px 底色条 + 主色标题,页面上移,首屏多出一张卡片的高度。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">宏观</div>
|
||||
<div class="body">
|
||||
<div class="mtabs-wrap">
|
||||
<div class="mtabs">
|
||||
<div>报警<span class="badge-dot">1</span></div><div class="on">新闻</div><div>油价</div>
|
||||
<div>经济数据</div><div>策略研究</div><div>经济文章</div>
|
||||
</div>
|
||||
<div class="mtabs-fade"><span class="hint">›</span></div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-card">
|
||||
<div class="feed-title">央行开展 5000 亿元 MLF 操作,中标利率持平</div>
|
||||
<div class="feed-sub">本月 MLF 净投放 2000 亿元,市场流动性保持合理充裕…</div>
|
||||
<div class="feed-meta"><span>妙想财经</span><span>2026-10-22</span><span class="rk">#1</span></div>
|
||||
</div>
|
||||
<div class="feed-card">
|
||||
<div class="feed-title">9 月社零同比增长 3.2%,服务消费延续回暖</div>
|
||||
<div class="feed-sub">餐饮收入增速回升至 5.1%,可选消费分化明显…</div>
|
||||
<div class="feed-meta"><span>妙想财经</span><span>2026-10-21</span><span class="rk">#2</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>①-b + ④ 宏观:头部标题化 · 报警/新闻置最左 · 右侧溢出提示<span class="tag">已实现</span></b>
|
||||
<p><b>顺序</b>:报警 → 新闻 → 油价 → 经济数据 → 策略研究 → 经济文章,<b>默认选中「新闻」</b>(<code>/macro</code> 自动跳 <code>/macro/news</code>)。<br>
|
||||
<b>溢出提示</b>:tab 条仍可向右滚动时,右边缘出现白色渐变遮罩 + 主色圆底箭头「›」(1.5s 轻微左右摆动,可点击向右翻页),滚到末尾自动消失;切换 tab 时自动把激活项滚到可视区中间。<br>
|
||||
antd-mobile Tabs 自带的右侧渐变遮罩只有 30px 且无箭头,用户难以察觉,故叠加此提示层。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>② 打卡:日历 + 打卡合并(<span style="color:var(--danger)">待确认草案</span>)</h3>
|
||||
<p class="desc">把原来的「打卡」「日历」两个 tab 合成一个:<b>上半部日历,下半部打卡卡片</b>。取舍按下述规则:</p>
|
||||
<table>
|
||||
<tr><th>项</th><th>规则</th></tr>
|
||||
<tr><td>页面结构</td><td>SwipeTabs 从 3 个减为 <b>2 个</b>:<b>日历(含打卡)</b> / <b>清单</b></td></tr>
|
||||
<tr><td>上方日历</td><td>保留月历网格 + 月份左右切换 + 「回到今天」;<b>去掉</b>「昨天 / 今天 / 明天」分段控件(点日期即可,节省 38px 高度)</td></tr>
|
||||
<tr><td>日历高度</td><td>日期格子 38px → <b>31px</b>,去掉 6 行补位中的空行;整块约 240px</td></tr>
|
||||
<tr><td>下方打卡</td><td>沿用现有打卡卡片样式,但<b>紧凑化</b>:内边距 12px → 8px,环形 58px → <b>42px</b>(环宽 6 → 5),行高压缩,卡片间距 10 → 7px</td></tr>
|
||||
<tr><td>卡片内容</td><td>保留环形进度(含达标对勾 / 超额 +n)、名称、周期与目标、连续天数、打卡按钮;<b>撤销 / 编辑 / 归档收进「⋯」</b>,减少一行高度</td></tr>
|
||||
<tr><td>分组</td><td>按周期类型分组:<b>每天 → 每周 → 每月</b>,每组一个小标题(左侧主色竖条 + 组名 + 条数);<b>去掉「每年」分组</b>(已有年目标项并入「每月」区末尾的「其他周期」小标题,或建议直接隐藏年视图)</td></tr>
|
||||
<tr><td>联动</td><td>点日历某日 → 下方卡片即该日数据(进度仍按该日所属周期算);未来日期只读,按钮变灰显示「计划」</td></tr>
|
||||
<tr><td>空态</td><td>日历下方直接显示「该日没有打卡项」+「+ 新建」引导</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="stage">
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px">
|
||||
<div class="on">日历</div><div>清单</div>
|
||||
</div>
|
||||
|
||||
<div class="mv-cal">
|
||||
<div class="mv-month">
|
||||
<span class="arrow">‹</span><b>2026年10月</b><span class="arrow">›</span>
|
||||
<span class="today">回到今天</span>
|
||||
</div>
|
||||
<div class="mv-grid mv-wd"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span></div>
|
||||
<div class="mv-grid">
|
||||
<div class="mv-day out">29</div><div class="mv-day out">30</div>
|
||||
<div class="mv-day">1<i class="full"></i></div><div class="mv-day">2<i class="full"></i></div>
|
||||
<div class="mv-day">3<i class="partial"></i></div><div class="mv-day">4<i class="full"></i></div><div class="mv-day">5</div>
|
||||
<div class="mv-day">6<i class="full"></i></div><div class="mv-day">7<i class="over"></i></div>
|
||||
<div class="mv-day">8<i class="full"></i></div><div class="mv-day">9<i class="partial"></i></div>
|
||||
<div class="mv-day">10<i class="full"></i></div><div class="mv-day">11<i class="full"></i></div><div class="mv-day">12</div>
|
||||
<div class="mv-day">13</div><div class="mv-day">14<i class="full"></i></div><div class="mv-day">15<i class="full"></i></div>
|
||||
<div class="mv-day">16<i class="partial"></i></div><div class="mv-day">17<i class="full"></i></div><div class="mv-day">18<i class="full"></i></div><div class="mv-day">19<i class="full"></i></div>
|
||||
<div class="mv-day">20<i class="full"></i></div><div class="mv-day">21<i class="full"></i></div>
|
||||
<div class="mv-day today sel">22<i class="partial"></i></div><div class="mv-day future">23</div><div class="mv-day future">24</div><div class="mv-day future">25</div>
|
||||
<div class="mv-day future">26</div><div class="mv-day future">27</div><div class="mv-day future">28</div>
|
||||
<div class="mv-day future">29</div><div class="mv-day future">30</div><div class="mv-day future">31</div><div class="mv-day out">1</div>
|
||||
</div>
|
||||
<div class="mv-rate"><span>本月完成率 <b>82%</b></span><span>今天 · 2026-10-22</span></div>
|
||||
</div>
|
||||
|
||||
<div class="mv-daybar">今天<span class="badge">已完成 2 / 4</span><span class="sum">点环即打卡</span></div>
|
||||
|
||||
<div class="mv-groups">
|
||||
<div class="mv-ghead">每天<span class="cnt">2 项</span></div>
|
||||
|
||||
<div class="mv-card">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#1677ff" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="58.1" transform="rotate(-90 21 21)"/>
|
||||
</svg>
|
||||
<div class="c">1<em>/2</em></div>
|
||||
</div>
|
||||
<div class="mv-main">
|
||||
<div class="mv-name">喝水</div>
|
||||
<div class="mv-meta">每天 2 次 · 今日 1/2<span class="st">连续 12</span></div>
|
||||
</div>
|
||||
<div class="mv-btn">打卡</div>
|
||||
<div class="mv-more">⋯</div>
|
||||
</div>
|
||||
|
||||
<div class="mv-card done">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#a855f7" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="0" transform="rotate(-90 21 21)"/>
|
||||
</svg>
|
||||
<div class="c"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#a855f7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<span class="ov">+1</span>
|
||||
</div>
|
||||
<div class="mv-main">
|
||||
<div class="mv-name">吃药</div>
|
||||
<div class="mv-meta">每天 1 次 · 今日 2/1<span class="st">连续 30</span></div>
|
||||
</div>
|
||||
<div class="mv-btn grey">再打一次</div>
|
||||
<div class="mv-more">⋯</div>
|
||||
</div>
|
||||
|
||||
<div class="mv-ghead">每周<span class="cnt">1 项</span></div>
|
||||
<div class="mv-card">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ff8f1f" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="77.5" transform="rotate(-90 21 21)"/>
|
||||
</svg>
|
||||
<div class="c">1<em>/3</em></div>
|
||||
</div>
|
||||
<div class="mv-main">
|
||||
<div class="mv-name">健身</div>
|
||||
<div class="mv-meta">每周 3 次 · 本周 1/3<span class="st">连续 5</span></div>
|
||||
</div>
|
||||
<div class="mv-btn">打卡</div>
|
||||
<div class="mv-more">⋯</div>
|
||||
</div>
|
||||
|
||||
<div class="mv-ghead">每月<span class="cnt">1 项</span></div>
|
||||
<div class="mv-card">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#00b578" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="116.2" transform="rotate(-90 21 21)"/>
|
||||
</svg>
|
||||
<div class="c">0<em>/12</em></div>
|
||||
</div>
|
||||
<div class="mv-main">
|
||||
<div class="mv-name">读书</div>
|
||||
<div class="mv-meta">每月 12 次 · 本月 0/12</div>
|
||||
</div>
|
||||
<div class="mv-btn">打卡</div>
|
||||
<div class="mv-more">⋯</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>②-a 合并后的主视图(日历在上 · 打卡在下,按天 / 周 / 月分组)<span class="tag" style="background:#fff1f0;color:#ff3141">待确认</span></b>
|
||||
<p>卡片整体压缩:环形 42px、内边距 8px、行距 7px,单卡高度约 58px(原 92px),一屏可看 4~5 项。分组顺序固定为 <b>每天 → 每周 → 每月</b>,
|
||||
组标题左侧 3px 主色竖条;<b>无「每年」分组</b>。日历顶部的「昨天 / 今天 / 明天」分段已去掉,直接点日期切换,选中日靠「今天」主色数字与蓝色实心块识别。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div class="on">日历</div><div>清单</div></div>
|
||||
<div class="mv-cal">
|
||||
<div class="mv-month"><span class="arrow">‹</span><b>2026年10月</b><span class="arrow">›</span><span class="today">回到今天</span></div>
|
||||
<div class="mv-grid mv-wd"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span></div>
|
||||
<div class="mv-grid">
|
||||
<div class="mv-day out">29</div><div class="mv-day out">30</div>
|
||||
<div class="mv-day">1<i class="full"></i></div><div class="mv-day">2<i class="full"></i></div>
|
||||
<div class="mv-day">3<i class="partial"></i></div><div class="mv-day">4<i class="full"></i></div><div class="mv-day">5</div>
|
||||
<div class="mv-day">6<i class="full"></i></div><div class="mv-day">7<i class="over"></i></div>
|
||||
<div class="mv-day">8<i class="full"></i></div><div class="mv-day">9<i class="partial"></i></div>
|
||||
<div class="mv-day">10<i class="full"></i></div><div class="mv-day">11<i class="full"></i></div><div class="mv-day">12</div>
|
||||
<div class="mv-day">13</div><div class="mv-day">14<i class="full"></i></div><div class="mv-day">15<i class="full"></i></div>
|
||||
<div class="mv-day">16<i class="partial"></i></div><div class="mv-day">17<i class="full"></i></div><div class="mv-day">18<i class="full"></i></div><div class="mv-day">19<i class="full"></i></div>
|
||||
<div class="mv-day">20<i class="full"></i></div><div class="mv-day">21<i class="full"></i></div>
|
||||
<div class="mv-day today">22<i class="partial"></i></div><div class="mv-day future sel">23</div><div class="mv-day future">24</div><div class="mv-day future">25</div>
|
||||
<div class="mv-day future">26</div><div class="mv-day future">27</div><div class="mv-day future">28</div>
|
||||
<div class="mv-day future">29</div><div class="mv-day future">30</div><div class="mv-day future">31</div><div class="mv-day out">1</div>
|
||||
</div>
|
||||
<div class="mv-rate"><span>本月完成率 <b>82%</b></span><span>明天 · 2026-10-23</span></div>
|
||||
</div>
|
||||
<div class="mv-daybar">明天<span class="badge">计划(不可打卡)</span></div>
|
||||
<div class="mv-groups">
|
||||
<div class="mv-ghead">每天<span class="cnt">2 项</span></div>
|
||||
<div class="mv-card">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#1677ff" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="116.2" transform="rotate(-90 21 21)"/>
|
||||
</svg><div class="c">0<em>/2</em></div>
|
||||
</div>
|
||||
<div class="mv-main"><div class="mv-name">喝水</div><div class="mv-meta">每天 2 次 · 当日 0/2</div></div>
|
||||
<div class="mv-btn grey">计划</div><div class="mv-more">⋯</div>
|
||||
</div>
|
||||
<div class="mv-card">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#a855f7" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="116.2" transform="rotate(-90 21 21)"/>
|
||||
</svg><div class="c">0<em>/1</em></div>
|
||||
</div>
|
||||
<div class="mv-main"><div class="mv-name">吃药</div><div class="mv-meta">每天 1 次 · 当日 0/1</div></div>
|
||||
<div class="mv-btn grey">计划</div><div class="mv-more">⋯</div>
|
||||
</div>
|
||||
<div class="mv-ghead">每周<span class="cnt">1 项</span></div>
|
||||
<div class="mv-card">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ff8f1f" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="77.5" transform="rotate(-90 21 21)"/>
|
||||
</svg><div class="c">1<em>/3</em></div>
|
||||
</div>
|
||||
<div class="mv-main"><div class="mv-name">健身</div><div class="mv-meta">每周 3 次 · 本周 1/3</div></div>
|
||||
<div class="mv-btn grey">计划</div><div class="mv-more">⋯</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>②-b 未来日期(计划态)</b>
|
||||
<p>选中未来日期时,打卡按钮统一变灰显示「计划」,点击不生效;环形仍显示该日所属周期的既有进度(此处举例为周期初)。
|
||||
补打窗口仍是最近 7 天,超出时同样只读。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div class="on">日历</div><div>清单</div></div>
|
||||
<div class="mv-cal">
|
||||
<div class="mv-month"><span class="arrow">‹</span><b>2026年10月</b><span class="arrow">›</span><span class="today">回到今天</span></div>
|
||||
<div class="mv-grid mv-wd"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span></div>
|
||||
<div class="mv-grid">
|
||||
<div class="mv-day out">29</div><div class="mv-day out">30</div>
|
||||
<div class="mv-day">1<i class="full"></i></div><div class="mv-day">2<i class="full"></i></div>
|
||||
<div class="mv-day">3<i class="partial"></i></div><div class="mv-day">4<i class="full"></i></div><div class="mv-day">5</div>
|
||||
<div class="mv-day">6<i class="full"></i></div><div class="mv-day">7<i class="over"></i></div>
|
||||
<div class="mv-day">8<i class="full"></i></div><div class="mv-day">9<i class="partial"></i></div>
|
||||
<div class="mv-day">10<i class="full"></i></div><div class="mv-day">11<i class="full"></i></div><div class="mv-day">12</div>
|
||||
<div class="mv-day">13</div><div class="mv-day">14<i class="full"></i></div><div class="mv-day">15<i class="full"></i></div>
|
||||
<div class="mv-day">16<i class="partial"></i></div><div class="mv-day">17<i class="full"></i></div><div class="mv-day">18<i class="full"></i></div><div class="mv-day">19<i class="full"></i></div>
|
||||
<div class="mv-day">20<i class="full"></i></div><div class="mv-day">21<i class="full"></i></div>
|
||||
<div class="mv-day today">22<i class="partial"></i></div><div class="mv-day future">23</div><div class="mv-day future">24</div><div class="mv-day future">25</div>
|
||||
<div class="mv-day future">26</div><div class="mv-day future">27</div><div class="mv-day future">28</div>
|
||||
<div class="mv-day future">29</div><div class="mv-day future">30</div><div class="mv-day future">31</div><div class="mv-day out">1</div>
|
||||
</div>
|
||||
<div class="mv-rate"><span>本月完成率 <b>82%</b></span><span>今天 · 2026-10-22</span></div>
|
||||
</div>
|
||||
<div class="mv-daybar">今天<span class="badge">已完成 2 / 4</span><span class="sum">点环即打卡</span></div>
|
||||
<div class="mv-groups">
|
||||
<div class="mv-ghead">每天<span class="cnt">2 项</span></div>
|
||||
<div class="mv-card">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#1677ff" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="58.1" transform="rotate(-90 21 21)"/>
|
||||
</svg><div class="c">1<em>/2</em></div>
|
||||
</div>
|
||||
<div class="mv-main"><div class="mv-name">喝水</div><div class="mv-meta">每天 2 次 · 今日 1/2</div></div>
|
||||
<div class="mv-btn">打卡</div><div class="mv-more">⋯</div>
|
||||
</div>
|
||||
<div class="mv-card done">
|
||||
<div class="mv-ring">
|
||||
<svg width="42" height="42" viewBox="0 0 42 42">
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#ececf0" stroke-width="5"/>
|
||||
<circle cx="21" cy="21" r="18.5" fill="none" stroke="#a855f7" stroke-width="5" stroke-linecap="round" stroke-dasharray="116.2" stroke-dashoffset="0" transform="rotate(-90 21 21)"/>
|
||||
</svg>
|
||||
<div class="c"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M4 12.5l5 5L20 6.5" stroke="#a855f7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<span class="ov">+1</span>
|
||||
</div>
|
||||
<div class="mv-main"><div class="mv-name">吃药</div><div class="mv-meta">每天 1 次 · 今日 2/1</div></div>
|
||||
<div class="mv-btn grey">再打一次</div><div class="mv-more">⋯</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mask" style="inset:28px 0 0 0"></div>
|
||||
<div class="sheet mv-sheet">
|
||||
<div class="sheet-title">吃药 · 每天 1 次</div>
|
||||
<div class="mv-sheet-row"><span>撤销上次打卡</span><span class="k">今日 2 次</span></div>
|
||||
<div class="mv-sheet-row"><span>编辑打卡项</span><span class="k">名称 / 周期 / 目标</span></div>
|
||||
<div class="mv-sheet-row danger"><span>归档</span><span class="k">历史记录保留</span></div>
|
||||
<div class="btn-ghost" style="margin-top:10px">取消</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>②-c 卡片「⋯」操作面板</b>
|
||||
<p>紧凑化后卡片不再常显三个文字链接,改由右上角「⋯」唤出底部面板:撤销 / 编辑 / 归档。面板项 44px 高,
|
||||
「归档」用危险色;撤销仅在当日有记录时可用,否则置灰。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>③ 清单:新增「模板」分类(与进行中 / 已归档并列)<span class="tag">已实现</span></h3>
|
||||
<p class="desc">清单页分段从 2 个变为 <b>3 个</b>:<b>进行中 / 已归档 / 模板</b>。模板拥有独立的增删改查,<b>按名称字母顺序</b>(中文按拼音、数字按数值)排序;
|
||||
新建清单时<b>只保留名称</b>,<b>去掉场景</b>,模板改为点击后在<b>右侧滑出</b>的面板里选择。</p>
|
||||
|
||||
<div class="stage">
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div>日历</div><div class="on">清单</div></div>
|
||||
<div class="list-head">
|
||||
<div class="seg"><div>进行中</div><div>已归档</div><div class="on">模板</div></div>
|
||||
<div class="mini-btn solid">+ 新建模板</div>
|
||||
</div>
|
||||
|
||||
<div class="tpl-card">
|
||||
<div class="tpl-row"><span class="tpl-name">出差</span><span class="tpl-badge">预置</span><span class="tpl-num">11 项</span></div>
|
||||
<div class="tpl-chips">
|
||||
<span class="tpl-chip">身份证 / 证件</span><span class="tpl-chip">手机充电器 + 充电宝</span>
|
||||
<span class="tpl-chip">笔记本电脑 + 电源</span><span class="tpl-chip">耳机</span><span class="tpl-chip more">+7</span>
|
||||
</div>
|
||||
<div class="tpl-acts"><span class="link">由此新建</span></div>
|
||||
</div>
|
||||
|
||||
<div class="tpl-card">
|
||||
<div class="tpl-row"><span class="tpl-name">回老家</span><span class="tpl-badge">预置</span><span class="tpl-num">12 项</span></div>
|
||||
<div class="tpl-chips">
|
||||
<span class="tpl-chip">身份证 / 证件</span><span class="tpl-chip">钥匙</span>
|
||||
<span class="tpl-chip">手机充电器 + 充电宝</span><span class="tpl-chip">换洗衣物</span><span class="tpl-chip more">+8</span>
|
||||
</div>
|
||||
<div class="tpl-acts"><span class="link">由此新建</span></div>
|
||||
</div>
|
||||
|
||||
<div class="tpl-card">
|
||||
<div class="tpl-row"><span class="tpl-name">露营装备</span><span class="tpl-num">2 项</span></div>
|
||||
<div class="tpl-chips"><span class="tpl-chip">帐篷</span><span class="tpl-chip">睡袋</span></div>
|
||||
<div class="tpl-acts"><span class="link">由此新建</span><span class="link">编辑</span><span class="link danger">删除</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>③-a 模板分类<span class="tag">已实现</span></b>
|
||||
<p>模板卡片:名称 + 「预置」标记 + 项数;下方最多展示 4 个检查项摘要(超出折叠为 <code>+n</code>);底部操作「由此新建 / 编辑 / 删除」。
|
||||
<b>系统预置模板不可编辑、不可删除</b>(只保留「由此新建」)。右上角按钮随分段切换:模板页为「+ 新建模板」,其余为「+ 新建」。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div>日历</div><div class="on">清单</div></div>
|
||||
<div class="mask" style="inset:28px 0 0 0"></div>
|
||||
<div class="sheet">
|
||||
<div class="sheet-title">新建检查清单</div>
|
||||
<div class="field"><label>名称</label><div class="inp">周末露营</div></div>
|
||||
<div class="field">
|
||||
<label>模板</label>
|
||||
<div class="pick-row">露营装备<span class="meta">2 项 <span style="color:#c8c8c8;font-size:15px">›</span></span></div>
|
||||
</div>
|
||||
<div class="btn-primary">创建</div>
|
||||
<div class="btn-ghost">取消</div>
|
||||
</div>
|
||||
<div class="side-panel">
|
||||
<div class="side-head"><b>选择模板</b><span class="x">×</span></div>
|
||||
<div class="side-tip">按名称字母顺序排列</div>
|
||||
<div class="side-item"><span class="nm">不使用模板</span><span class="sub">空白清单</span></div>
|
||||
<div class="side-item"><span class="nm">出差<span class="tpl-badge">预置</span></span><span class="sub">11 项</span></div>
|
||||
<div class="side-item"><span class="nm">回老家<span class="tpl-badge">预置</span></span><span class="sub">12 项</span></div>
|
||||
<div class="side-item on"><span class="nm">露营装备</span><span class="sub">2 项</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>③-b 新建清单 + 模板右侧滑出面板<span class="tag">已实现</span></b>
|
||||
<p>表单只剩两项:<b>名称</b>(≤20 字,必填)与 <b>模板</b>(可空)。<b>场景选择器已移除</b>——场景不再由用户填写,新建时沿用所选模板自带的场景(无模板则为空),历史数据仍正常展示场景标签。<br>
|
||||
点模板行从<b>右侧滑出 78% 宽面板</b>:首项固定为「不使用模板」,其后按字母顺序列出全部模板(预置带标记、右侧显示项数),选中项主色高亮,点遮罩或 × 关闭。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shot">
|
||||
<div class="phone">
|
||||
<div class="navbar-sm">打卡</div>
|
||||
<div class="body">
|
||||
<div class="seg" style="width:calc(100% - 28px);margin-left:14px"><div>日历</div><div class="on">清单</div></div>
|
||||
<div class="mask" style="inset:28px 0 0 0"></div>
|
||||
<div class="sheet">
|
||||
<div class="sheet-title">编辑模板</div>
|
||||
<div class="field"><label>模板名称</label><div class="inp">露营装备</div></div>
|
||||
<div class="field">
|
||||
<label>检查项(3)</label>
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
<div style="display:flex;align-items:center;gap:8px"><span style="width:16px;text-align:right;font-size:11px;color:#999">1</span><div class="inp" style="flex:1">帐篷</div><span style="color:#ccc;font-size:17px">×</span></div>
|
||||
<div style="display:flex;align-items:center;gap:8px"><span style="width:16px;text-align:right;font-size:11px;color:#999">2</span><div class="inp" style="flex:1">睡袋</div><span style="color:#ccc;font-size:17px">×</span></div>
|
||||
<div style="display:flex;align-items:center;gap:8px"><span style="width:16px;text-align:right;font-size:11px;color:#999">3</span><div class="inp ph" style="flex:1">检查项内容</div><span style="color:#ccc;font-size:17px">×</span></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;border:1px solid var(--line);border-radius:7px;text-align:center;padding:6px 0;font-size:12px;color:var(--muted)">+ 添加检查项</div>
|
||||
</div>
|
||||
<div class="btn-primary">保存</div>
|
||||
<div class="btn-ghost">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbar">
|
||||
<div class="tab on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8.5"/><path d="M8 12.4l2.6 2.6L16.5 9.5" stroke-linecap="round" stroke-linejoin="round"/></svg>打卡</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 19V9m5 10V5m5 14v-7m5 7V8"/></svg>宏观</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg>监控</div>
|
||||
<div class="tab"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.6"/><path d="M4.5 20c1.4-3.6 4.1-5.4 7.5-5.4S18.1 16.4 19.5 20"/></svg>我</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<b>③-c 模板编辑(增删改)<span class="tag">已实现</span></b>
|
||||
<p>新建 / 编辑共用同一弹层:模板名称(≤20 字)+ 检查项列表(逐条 ≤40 字,可删除、可追加)。保存时至少需 1 条有效检查项,名称为空或全空则拦截并 Toast 提示。
|
||||
删除模板需二次确认,且不影响已用该模板创建过的清单。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>④ 补充:模板接口</h3>
|
||||
<table>
|
||||
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
|
||||
<tr><td>GET</td><td><code>/api/checklists/templates</code></td><td>模板列表(含预置),<b>服务端按名称字母顺序排序</b></td></tr>
|
||||
<tr><td>GET</td><td><code>/api/checklists/templates/:id</code></td><td>模板详情</td></tr>
|
||||
<tr><td>POST</td><td><code>/api/checklists/templates</code></td><td>新建模板 <code>{ name, items[] }</code></td></tr>
|
||||
<tr><td>PUT</td><td><code>/api/checklists/templates/:id</code></td><td>改名称 / 检查项(预置模板拒绝)</td></tr>
|
||||
<tr><td>DELETE</td><td><code>/api/checklists/templates/:id</code></td><td>删自建模板(预置模板拒绝)</td></tr>
|
||||
</table>
|
||||
<p class="note">排序口径:<code>Intl.Collator('zh-Hans-CN', { numeric: true, sensitivity: 'base' })</code>,中文按拼音、数字按数值比较,前后端一致。</p>
|
||||
|
||||
<!-- ============ 3 组件状态 ============ -->
|
||||
<h2>3 组件状态定义<span class="sub">Component States</span></h2>
|
||||
|
||||
<h3>3.0 页面头部(PageHeader)</h3>
|
||||
<table>
|
||||
<tr><th>项</th><th>取值</th></tr>
|
||||
<tr><td>高度</td><td>23px(原 NavBar 45px 的一半),<code>flex: 0 0 23px</code> 不参与压缩</td></tr>
|
||||
<tr><td>标题字号 / 字重</td><td>12px / 600</td></tr>
|
||||
<tr><td>标题颜色</td><td>主色 <code>#1677ff</code></td></tr>
|
||||
<tr><td>背景</td><td>页面底色 <code>#f5f5f5</code></td></tr>
|
||||
<tr><td>适用范围</td><td>打卡 / 宏观 / 监控;<b>「我」页不使用</b></td></tr>
|
||||
</table>
|
||||
<p class="note">实现为独立组件 <code>src/components/PageHeader.jsx</code>,三页各自引用、互不影响;「我」页移除 NavBar 后头像区直接贴顶。</p>
|
||||
|
||||
<h3>3.1 环形进度(尺寸 58,环宽 6,r=26,周长 163.4)</h3>
|
||||
<table>
|
||||
<tr><th>状态</th><th>条件</th><th>环形</th><th>中心</th><th>卡片底</th><th>按钮</th></tr>
|
||||
|
||||
@@ -442,3 +442,75 @@ compareVersion('0.2','0.2.0') // → 0 (缺位补 0)
|
||||
### 10.5 界面设计同步
|
||||
|
||||
`界面设计.html` 已同步:底部 Tab 改为 4 项且「打卡」高亮在首位、打卡页去掉特效开关按钮、新增「我的 · OTA 开发版本开关」屏、宏观新增「报警 / 新闻」子 Tab 与合并后的 6 Tab 导航。
|
||||
|
||||
---
|
||||
|
||||
## 11. 界面调整(2026-10-23)
|
||||
|
||||
### 11.1 变更清单
|
||||
|
||||
| # | 范围 | 变更 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 页面头部 | 打卡 NavBar 高度减半(45 → 23px)、标题改主色 12px;宏观 / 监控补标题;「我」页整条去掉 | **已实现** |
|
||||
| 2 | 打卡页 | 日历与打卡合并为一个视图(上日历、下打卡,按天/周/月分组、去掉年分组、卡片紧凑化) | **待确认**(设计稿已出,见《界面设计.html》2.12 ②节) |
|
||||
| 3 | 清单页 | 新增「模板」分类(与进行中 / 已归档并列),独立 CRUD + 字母序;新建清单去掉场景、模板改右侧滑出选择 | **已实现** |
|
||||
| 4 | 宏观页 | 报警 / 新闻移到最左并默认选中新闻;tab 条增加右侧溢出提示 | **已实现** |
|
||||
|
||||
### 11.2 页面头部统一
|
||||
|
||||
新增组件 `src/components/PageHeader.jsx`(高度 23px、标题 12px/600 主色、字距 1px、背景取页面底色 `#f5f5f5`),只被 **打卡 / 宏观 / 监控** 三页引用:
|
||||
|
||||
| 页 | 变更前 | 变更后 |
|
||||
| --- | --- | --- |
|
||||
| 打卡 | `NavBar` 45px,标题 17px 深色 | `PageHeader` 23px,标题主色 |
|
||||
| 宏观 | `NavBar` 45px,空白 | `PageHeader` + 标题「宏观」 |
|
||||
| 监控 | `NavBar` 45px,空白 | `PageHeader` + 标题「监控」 |
|
||||
| 我 | `NavBar` 45px 空白条 | **移除**,`me-profile` 直接贴顶 |
|
||||
|
||||
> 「我」页不引用该组件,因此调整打卡/宏观/监控的头部不会波及「我」;反之亦然。三个页面之间也各自持有独立实例,互不影响。
|
||||
|
||||
### 11.3 清单模板分类
|
||||
|
||||
**接口**(`server/routes/checklists.js`):
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/checklists/templates` | 列表,**服务端按名称字母顺序排序**(原为 `builtin desc, id asc`) |
|
||||
| GET | `/api/checklists/templates/:id` | 详情(新增) |
|
||||
| POST | `/api/checklists/templates` | 新建(名称 ≤20 字,≥1 条检查项) |
|
||||
| PUT | `/api/checklists/templates/:id` | 改名称 / 检查项(新增;`builtin=true` 拒绝,404) |
|
||||
| DELETE | `/api/checklists/templates/:id` | 删自建模板(`builtin=true` 拒绝) |
|
||||
|
||||
排序口径:`Intl.Collator('zh-Hans-CN', { numeric: true, sensitivity: 'base' })` —— 中文按拼音、数字按数值。前端 `ListView` 拿到列表后再排一次,保证与后端一致(网络返回顺序不可控时也不乱)。
|
||||
|
||||
**前端**(`src/pages/checkin/ListView.jsx`):
|
||||
|
||||
- 分段由 2 项扩为 3 项:`active`(进行中)/ `archived`(已归档)/ `template`(模板);右上角按钮随分段切换为「+ 新建」/「+ 新建模板」。
|
||||
- 模板卡片:名称 + 「预置」标记 + 项数;最多展示 4 条检查项摘要,超出折叠为 `+n`;操作「由此新建 / 编辑 / 删除」,预置模板只保留「由此新建」。
|
||||
- 新建 / 编辑模板共用底部弹层:名称 + 检查项逐条编辑(≤40 字,可删可加)。
|
||||
- **新建清单表单**:仅「名称」+「模板」两项 —— **场景选择器已移除**(`scene` 改为沿用所选模板自带的场景,无模板则为空;数据库字段保留,历史数据照常展示)。
|
||||
- **模板选择**:点击模板行从**右侧滑出** `78vw` 面板(antd-mobile `Popup position="right"`),首项固定「不使用模板」,其后为字母序模板列表(预置带标记 + 项数),选中项主色高亮。
|
||||
|
||||
### 11.4 宏观 tab 顺序与溢出提示
|
||||
|
||||
- Tab 顺序改为 **报警 → 新闻 → 油价 → 经济数据 → 策略研究 → 经济文章**,默认选中 **新闻**;`/macro` 由路由直接 `Navigate` 到 `/macro/news`(`App.jsx`),`activeFromPath` 兜底同样返回 `news`。
|
||||
- `SwipeTabs` 增加溢出提示层:测量 `.adm-tabs-tab-list` 的 `scrollLeft + clientWidth < scrollWidth`,为真时在顶部右侧渲染 `.tab-more-hint`(白色渐变 + 主色圆底箭头「›」,1.5s 轻微摆动动画,点击向右翻页 60% 视宽),滚到末尾自动消失。
|
||||
- 切换 tab 时把激活项平滑滚到可视区中央,避免选中项藏在屏幕外。
|
||||
- 为什么不用框架自带的:antd-mobile Tabs 自带 `.adm-tabs-header-mask-right` 只有 30px 纯渐变、无箭头、无交互,用户难以判断"右侧还有内容",故叠加此提示层。
|
||||
|
||||
### 11.5 待确认项:日历与打卡合并
|
||||
|
||||
方案已写入《界面设计.html》2.12 ②节(含主视图、未来日期计划态、卡片「⋯」操作面板三屏)。要点:
|
||||
|
||||
| 项 | 方案 |
|
||||
| --- | --- |
|
||||
| 结构 | SwipeTabs 由 3 个减为 2 个:`日历(含打卡)` / `清单` |
|
||||
| 日历 | 月历 + 月份切换 + 回到今天;**去掉「昨天/今天/明天」分段**;日期格 38 → 31px |
|
||||
| 打卡 | 沿用现有卡片样式,紧凑化:内边距 12→8px、环形 58→42px(环宽 6→5)、间距 10→7px |
|
||||
| 操作 | 撤销 / 编辑 / 归档收进「⋯」底部面板(原为卡片底部三个文字链接) |
|
||||
| 分组 | **每天 → 每周 → 每月**,组标题左侧主色竖条;**去掉「每年」分组** |
|
||||
| 联动 | 点日期即切换当日数据;未来日期只读,按钮灰显「计划」 |
|
||||
|
||||
确认后再落地:`CheckinPage` 收敛为 2 个 tab、`CalendarView` 与 `HabitView` 合成新视图组件、`HabitView` 的循环/编辑逻辑抽出复用、样式新增 `.mv-*` 段。
|
||||
|
||||
|
||||
|
||||
@@ -73,25 +73,40 @@ function withProgress(list) {
|
||||
|
||||
// ---------- 模板 ----------
|
||||
|
||||
// GET /api/checklists/templates
|
||||
// 模板按名称字母顺序 (中文按拼音) 排序, 前后端共用同一比较器语义
|
||||
const NAME_COLLATOR = new Intl.Collator('zh-Hans-CN', { numeric: true, sensitivity: 'base' })
|
||||
const byName = (a, b) => NAME_COLLATOR.compare(String(a.name || ''), String(b.name || ''))
|
||||
|
||||
// GET /api/checklists/templates 模板列表 (含系统预置, 按字母顺序)
|
||||
router.get('/templates', async (_req, res, next) => {
|
||||
try {
|
||||
await ensureBuiltin()
|
||||
const rows = await prisma.checkListTemplate.findMany({
|
||||
where: { OR: [{ builtin: true }, { userId: _req.user.sub }] },
|
||||
orderBy: [{ builtin: 'desc' }, { id: 'asc' }],
|
||||
})
|
||||
res.json({ items: rows })
|
||||
res.json({ items: rows.sort(byName) })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// POST /api/checklists/templates 另存为模板 { name, scene, items: [{title, note}] }
|
||||
// GET /api/checklists/templates/:id 模板详情
|
||||
router.get('/templates/:id', async (req, res, next) => {
|
||||
try {
|
||||
const row = await prisma.checkListTemplate.findFirst({
|
||||
where: { id: Number(req.params.id), OR: [{ builtin: true }, { userId: req.user.sub }] },
|
||||
})
|
||||
if (!row) return res.status(404).json({ error: '模板不存在' })
|
||||
res.json({ item: row })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// POST /api/checklists/templates 另存为模板 { name, scene?, items: [{title, note}] }
|
||||
router.post('/templates', async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.sub
|
||||
const { name, scene = '', items = [] } = req.body || {}
|
||||
const cleanName = String(name || '').trim()
|
||||
if (!cleanName) return res.status(400).json({ error: '模板名称不能为空' })
|
||||
if (cleanName.length > 20) return res.status(400).json({ error: '模板名称不能超过 20 个字' })
|
||||
const list = (Array.isArray(items) ? items : [])
|
||||
.map((i) => ({ title: String(i?.title || '').trim(), note: String(i?.note || '') }))
|
||||
.filter((i) => i.title)
|
||||
@@ -103,12 +118,40 @@ router.post('/templates', async (req, res, next) => {
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// DELETE /api/checklists/templates/:id
|
||||
// PUT /api/checklists/templates/:id 改名称 / 检查项 (系统预置模板不可改)
|
||||
router.put('/templates/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.sub
|
||||
const id = Number(req.params.id)
|
||||
const exist = await prisma.checkListTemplate.findFirst({ where: { id, userId, builtin: false } })
|
||||
if (!exist) return res.status(404).json({ error: '模板不存在、无权限修改或是系统预置模板' })
|
||||
const { name, scene, items } = req.body || {}
|
||||
const data = {}
|
||||
if (name !== undefined) {
|
||||
const n = String(name).trim()
|
||||
if (!n) return res.status(400).json({ error: '模板名称不能为空' })
|
||||
if (n.length > 20) return res.status(400).json({ error: '模板名称不能超过 20 个字' })
|
||||
data.name = n
|
||||
}
|
||||
if (scene !== undefined) data.scene = String(scene)
|
||||
if (items !== undefined) {
|
||||
const list = (Array.isArray(items) ? items : [])
|
||||
.map((i) => ({ title: String(i?.title || '').trim(), note: String(i?.note || '') }))
|
||||
.filter((i) => i.title)
|
||||
if (!list.length) return res.status(400).json({ error: '模板至少需要一个检查项' })
|
||||
data.items = list
|
||||
}
|
||||
const row = await prisma.checkListTemplate.update({ where: { id }, data })
|
||||
res.json({ item: row })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// DELETE /api/checklists/templates/:id 仅自建模板可删
|
||||
router.delete('/templates/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.sub
|
||||
const id = Number(req.params.id)
|
||||
const exist = await prisma.checkListTemplate.findFirst({ where: { id, userId } })
|
||||
const exist = await prisma.checkListTemplate.findFirst({ where: { id, userId, builtin: false } })
|
||||
if (!exist) return res.status(404).json({ error: '模板不存在或无权删除' })
|
||||
await prisma.checkListTemplate.delete({ where: { id } })
|
||||
res.json({ ok: true })
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@ export default function App() {
|
||||
{/* 打卡为默认首页 */}
|
||||
<Route path="/" element={<Navigate to={HOME_PATH} replace />} />
|
||||
<Route path="/checkin" element={<CheckinPage />} />
|
||||
{/* 宏观: 油价 / 经济数据 / 策略研究 / 经济文章 / 报警 / 新闻 (V0.2 原首页并入) */}
|
||||
{/* 宏观: 报警 / 新闻 / 油价 / 经济数据 / 策略研究 / 经济文章 (V0.2 原首页并入, 默认新闻) */}
|
||||
<Route path="/macro" element={<Navigate to="/macro/news" replace />} />
|
||||
<Route path="/macro/*" element={<MacroPage />} />
|
||||
<Route path="/monitor" element={<MonitorPage />} />
|
||||
<Route path="/monitor/server/:uuid" element={<ServerDetail />} />
|
||||
|
||||
@@ -34,6 +34,8 @@ export const checklistApi = {
|
||||
removeItem: (itemId) => api(`/checklists/items/${itemId}`, { method: 'DELETE' }),
|
||||
checkAll: (id, done) => api(`/checklists/${id}/check-all`, { method: 'POST', body: { done } }),
|
||||
templates: () => api('/checklists/templates'),
|
||||
template: (id) => api(`/checklists/templates/${id}`),
|
||||
createTemplate: (body) => api('/checklists/templates', { method: 'POST', body }),
|
||||
updateTemplate: (id, body) => api(`/checklists/templates/${id}`, { method: 'PUT', body }),
|
||||
removeTemplate: (id) => api(`/checklists/templates/${id}`, { method: 'DELETE' }),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 四个主 Tab 的统一头部 (V0.2.1 界面调整)
|
||||
// - 高度为原 antd-mobile NavBar 的一半 (23px)
|
||||
// - 标题小字 + 主色, 仅作定位用, 不抢内容视觉
|
||||
// - 「我」页不使用该头部 (页面顶部直接是头像区), 因此本组件只被
|
||||
// 打卡 / 宏观 / 监控 三页引用, 互不影响
|
||||
export default function PageHeader({ title }) {
|
||||
return (
|
||||
<header className="page-header">
|
||||
<span className="page-header-title">{title}</span>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,83 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Tabs } from 'antd-mobile'
|
||||
|
||||
// 顶部分类 + 下方仅渲染当前激活 tab 的内容
|
||||
// - 顶部: antd-mobile Tabs (点击切换)
|
||||
// - 下方: 同时挂载所有 tab, 通过 [hidden] 仅显示激活的那个 (保留各 tab 内部状态)
|
||||
// - 右侧溢出提示: 当 tab 条还能向右滚动时, 右边缘显示渐变 + 箭头,
|
||||
// 提示"右侧还有更多 tab"; 滚动到末尾后自动消失
|
||||
// props:
|
||||
// tabs: [{ key, label, render }]
|
||||
// active: 当前激活 key
|
||||
// onChange(key): 切换回调
|
||||
export default function SwipeTabs({ tabs, active, onChange }) {
|
||||
const wrapRef = useRef(null)
|
||||
const [more, setMore] = useState(false)
|
||||
const [headerH, setHeaderH] = useState(45)
|
||||
|
||||
// 判断右侧是否还有未展示的 tab (顺带记录 tab 条高度, 让提示层只覆盖页签行)
|
||||
const measure = useCallback(() => {
|
||||
const wrap = wrapRef.current
|
||||
if (!wrap) return
|
||||
const list = wrap.querySelector('.adm-tabs-tab-list')
|
||||
const header = wrap.querySelector('.adm-tabs-header')
|
||||
if (header && header.offsetHeight > 0) setHeaderH(header.offsetHeight)
|
||||
if (!list) return
|
||||
setMore(list.scrollLeft + list.clientWidth < list.scrollWidth - 2)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const wrap = wrapRef.current
|
||||
if (!wrap) return
|
||||
const list = wrap.querySelector('.adm-tabs-tab-list')
|
||||
const header = wrap.querySelector('.adm-tabs-header')
|
||||
if (!list) return
|
||||
|
||||
// 首次布局未稳定 (字体/宽度), 下一帧 + 300ms 各补测一次
|
||||
const raf = requestAnimationFrame(measure)
|
||||
const timer = setTimeout(measure, 300)
|
||||
list.addEventListener('scroll', measure, { passive: true })
|
||||
const ro = new ResizeObserver(measure)
|
||||
ro.observe(list)
|
||||
if (header) ro.observe(header)
|
||||
return () => {
|
||||
cancelAnimationFrame(raf)
|
||||
clearTimeout(timer)
|
||||
list.removeEventListener('scroll', measure)
|
||||
ro.disconnect()
|
||||
}
|
||||
}, [measure, tabs.length])
|
||||
|
||||
// 切换 tab 后把激活项滚到可见区域中间, 避免选中项被藏在屏幕外
|
||||
useEffect(() => {
|
||||
const wrap = wrapRef.current
|
||||
if (!wrap) return
|
||||
const list = wrap.querySelector('.adm-tabs-tab-list')
|
||||
const activeEl = wrap.querySelector('.adm-tabs-tab-active')
|
||||
if (!list || !activeEl) return
|
||||
const target = activeEl.offsetLeft - (list.clientWidth - activeEl.offsetWidth) / 2
|
||||
list.scrollTo({ left: Math.max(0, target), behavior: 'smooth' })
|
||||
}, [active, tabs.length])
|
||||
|
||||
return (
|
||||
<div className="swipe-tabs-wrap">
|
||||
<div className="swipe-tabs-wrap" ref={wrapRef}>
|
||||
<Tabs activeKey={active} onChange={onChange}>
|
||||
{tabs.map((t) => (
|
||||
<Tabs.Tab key={t.key} title={t.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
{more && (
|
||||
<div
|
||||
className="tab-more-hint"
|
||||
style={{ height: headerH }}
|
||||
onClick={() => {
|
||||
const list = wrapRef.current?.querySelector('.adm-tabs-tab-list')
|
||||
if (list) list.scrollTo({ left: list.scrollLeft + list.clientWidth * 0.6, behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
<span className="tab-more-arrow">›</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="tab-content">
|
||||
{tabs.map((t) => (
|
||||
<div key={t.key} className="tab-pane" hidden={t.key !== active}>
|
||||
|
||||
+16
-14
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useLocation, Routes, Route } from 'react-router-dom'
|
||||
import { Badge, NavBar } from 'antd-mobile'
|
||||
import { Badge } from 'antd-mobile'
|
||||
import PageHeader from '../components/PageHeader.jsx'
|
||||
import SwipeTabs from '../components/SwipeTabs.jsx'
|
||||
import DataView from './macro/DataView.jsx'
|
||||
import StrategyView from './macro/StrategyView.jsx'
|
||||
@@ -11,33 +12,34 @@ import AlertsView from './macro/AlertsView.jsx'
|
||||
import NewsView from './macro/NewsView.jsx'
|
||||
import NewsDetail from './news/NewsDetail.jsx'
|
||||
|
||||
// 宏观: 顶栏 6 tab, 默认 油价
|
||||
// /macro → 显示 oil tab (本组件 activeFromPath 决定)
|
||||
// 宏观: 顶栏 6 tab, 默认 新闻
|
||||
// /macro → 显示 news tab (activeFromPath 决定)
|
||||
// /macro/alerts → 报警 (V0.2: 原「首页」tab 合并进来, 置最左)
|
||||
// /macro/news → 新闻 (V0.2: 原「首页」tab 合并进来, 置第二并作为默认)
|
||||
// /macro/oil → 油价
|
||||
// /macro/data → 经济数据
|
||||
// /macro/strategy → 策略研究
|
||||
// /macro/articles → 经济文章
|
||||
// /macro/alerts → 报警 (V0.2: 原「首页」tab 合并进来)
|
||||
// /macro/news → 新闻 (V0.2: 原「首页」tab 合并进来)
|
||||
// 详情: /macro/data/:name · /macro/strategy/:name · /macro/articles/:slug · /macro/news/:id
|
||||
// 说明: tab 数量已超出屏宽, SwipeTabs 会在右侧显示渐变 + 箭头提示可继续滑动
|
||||
const TABS = [
|
||||
{ key: 'alerts', label: '报警', path: '/macro/alerts' },
|
||||
{ key: 'news', label: '新闻', path: '/macro/news' },
|
||||
{ key: 'oil', label: '油价', path: '/macro/oil' },
|
||||
{ key: 'data', label: '经济数据', path: '/macro/data' },
|
||||
{ key: 'strategy', label: '策略研究', path: '/macro/strategy' },
|
||||
{ key: 'articles', label: '经济文章', path: '/macro/articles' },
|
||||
{ key: 'alerts', label: '报警', path: '/macro/alerts' },
|
||||
{ key: 'news', label: '新闻', path: '/macro/news' },
|
||||
]
|
||||
|
||||
function activeFromPath(pathname) {
|
||||
if (pathname.startsWith('/macro/oil')) return 'oil'
|
||||
if (pathname.startsWith('/macro/articles')) return 'articles'
|
||||
if (pathname.startsWith('/macro/strategy')) return 'strategy'
|
||||
if (pathname.startsWith('/macro/data')) return 'data'
|
||||
if (pathname.startsWith('/macro/alerts')) return 'alerts'
|
||||
if (pathname.startsWith('/macro/news')) return 'news'
|
||||
// 父路由 /macro 走 Tab 列表第一个 (油价)
|
||||
return 'oil'
|
||||
if (pathname.startsWith('/macro/oil')) return 'oil'
|
||||
if (pathname.startsWith('/macro/data')) return 'data'
|
||||
if (pathname.startsWith('/macro/strategy')) return 'strategy'
|
||||
if (pathname.startsWith('/macro/articles')) return 'articles'
|
||||
// 父路由 /macro 走默认 tab (新闻)
|
||||
return 'news'
|
||||
}
|
||||
|
||||
export default function MacroPage() {
|
||||
@@ -82,7 +84,7 @@ export default function MacroPage() {
|
||||
|
||||
return (
|
||||
<div className="page-with-navbar">
|
||||
<NavBar back={null} />
|
||||
<PageHeader title="宏观" />
|
||||
<SwipeTabs tabs={tabs} active={active} onChange={onChange} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Avatar, Button, List, NavBar, ProgressBar, Switch, Toast } from 'antd-mobile'
|
||||
import { Avatar, Button, List, ProgressBar, Switch, Toast } from 'antd-mobile'
|
||||
import { useAuth } from '../auth/AuthContext.jsx'
|
||||
import { APP_VERSION, BUILD_ID } from '../version.js'
|
||||
import {
|
||||
@@ -115,7 +115,6 @@ export default function MePage() {
|
||||
|
||||
return (
|
||||
<div className="me-page">
|
||||
<NavBar back={null} className="me-navbar" />
|
||||
<div className="me-profile">
|
||||
<Avatar
|
||||
className="me-avatar"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { NavBar, Card, Empty, DotLoading } from 'antd-mobile'
|
||||
import { Card, Empty, DotLoading } from 'antd-mobile'
|
||||
import { api } from '../api/client.js'
|
||||
import PageHeader from '../components/PageHeader.jsx'
|
||||
import SwipeTabs from '../components/SwipeTabs.jsx'
|
||||
import useRefreshOnVisible from '../hooks/useRefreshOnVisible.js'
|
||||
import EnvView from './monitor/EnvView.jsx'
|
||||
@@ -46,7 +47,7 @@ export default function MonitorPage() {
|
||||
]
|
||||
return (
|
||||
<div className="page-with-navbar">
|
||||
<NavBar back={null} />
|
||||
<PageHeader title="监控" />
|
||||
<div className="monitor-content">
|
||||
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from 'react'
|
||||
import { NavBar } from 'antd-mobile'
|
||||
import PageHeader from '../../components/PageHeader.jsx'
|
||||
import SwipeTabs from '../../components/SwipeTabs.jsx'
|
||||
import HabitView from './HabitView.jsx'
|
||||
import CalendarView from './CalendarView.jsx'
|
||||
import ListView from './ListView.jsx'
|
||||
import { toDateStr } from './date.js'
|
||||
|
||||
// 打卡页: 一个页面三个视图 —— 打卡(环形进度) / 日历(今天·昨天·明天) / 清单(场景检查)
|
||||
// 打卡页: 三个视图 —— 打卡(环形进度) / 日历(今天·昨天·明天) / 清单(检查清单 + 模板)
|
||||
// 注: 打卡与日历的合并视图正在设计确认中, 确认后此处收敛为 日历(含打卡) / 清单 两个视图
|
||||
export default function CheckinPage() {
|
||||
const [active, setActive] = useState('habit')
|
||||
const [date, setDate] = useState(() => toDateStr())
|
||||
@@ -19,7 +20,7 @@ export default function CheckinPage() {
|
||||
|
||||
return (
|
||||
<div className="page-with-navbar">
|
||||
<NavBar back={null}>打卡</NavBar>
|
||||
<PageHeader title="打卡" />
|
||||
<div className="checkin-content">
|
||||
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
||||
</div>
|
||||
|
||||
+276
-39
@@ -1,24 +1,33 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Button, Dialog, DotLoading, Empty, Input, Popup, Segmented, Selector, Toast,
|
||||
Button, Dialog, DotLoading, Empty, Input, Popup, Segmented, Toast,
|
||||
} from 'antd-mobile'
|
||||
import { Celebrate } from '../../components/RingProgress.jsx'
|
||||
import { checklistApi } from '../../api/checkin.js'
|
||||
import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js'
|
||||
|
||||
const SCENES = ['出差', '回老家', '其他']
|
||||
// 清单视图: 进行中 / 已归档 / 模板 三类并列 (F-LS-*)
|
||||
// - 进行中 / 已归档: 清单本体, 勾选即完成
|
||||
// - 模板: 独立 CRUD (新建 / 编辑 / 删除), 按名称字母顺序排序 (中文按拼音)
|
||||
// - 新建清单: 只填名称, 模板从右侧滑出面板中选择 (不再使用场景选择器)
|
||||
const NAME_COLLATOR = new Intl.Collator('zh-Hans-CN', { numeric: true, sensitivity: 'base' })
|
||||
const byName = (a, b) => NAME_COLLATOR.compare(String(a.name || a.title || ''), String(b.name || b.title || ''))
|
||||
|
||||
// 清单视图: 出差/回老家等场景的一次性检查清单 (F-LS-*)
|
||||
export default function ListView() {
|
||||
const [archived, setArchived] = useState(false)
|
||||
const [tab, setTab] = useState('active') // active | archived | template
|
||||
const [lists, setLists] = useState(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [openId, setOpenId] = useState(null)
|
||||
const [newItem, setNewItem] = useState({})
|
||||
const [bursts, setBursts] = useState({})
|
||||
const [templates, setTemplates] = useState([])
|
||||
const [templates, setTemplates] = useState(null)
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [form, setForm] = useState({ title: '', scene: '出差', templateId: '' })
|
||||
const [form, setForm] = useState({ title: '', templateId: '' })
|
||||
const [pickerOpen, setPickerOpen] = useState(false) // 右侧模板选择面板
|
||||
const [tplEdit, setTplEdit] = useState(null) // { id, name, items[] } | null
|
||||
const [tplSaving, setTplSaving] = useState(false)
|
||||
|
||||
const archived = tab === 'archived'
|
||||
|
||||
const load = useCallback((silent) => {
|
||||
if (!silent) setErr('')
|
||||
@@ -27,13 +36,16 @@ export default function ListView() {
|
||||
.catch((e) => { if (!silent) setErr(e.message) })
|
||||
}, [archived])
|
||||
|
||||
useEffect(() => { setLists(null); load(false) }, [load])
|
||||
useRefreshOnVisible(() => load(true))
|
||||
|
||||
useEffect(() => {
|
||||
checklistApi.templates().then((d) => setTemplates(d.items || [])).catch(() => {})
|
||||
const loadTemplates = useCallback(() => {
|
||||
checklistApi.templates()
|
||||
.then((d) => setTemplates((d.items || []).slice().sort(byName)))
|
||||
.catch(() => setTemplates([]))
|
||||
}, [])
|
||||
|
||||
useEffect(() => { setLists(null); load(false) }, [load])
|
||||
useRefreshOnVisible(() => { load(true); if (tab === 'template') loadTemplates() })
|
||||
useEffect(() => { loadTemplates() }, [loadTemplates])
|
||||
|
||||
const withProgress = useCallback((list) => {
|
||||
const items = list.items || []
|
||||
const done = items.filter((i) => i.done).length
|
||||
@@ -107,13 +119,27 @@ export default function ListView() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 清单 ----------
|
||||
|
||||
function openCreateForm(templateId = '') {
|
||||
setForm({ title: '', templateId })
|
||||
setShowForm(true)
|
||||
}
|
||||
|
||||
async function createList() {
|
||||
const title = String(form.title || '').trim()
|
||||
if (!title) return Toast.show({ content: '请填写清单名称' })
|
||||
const tpl = templateById(form.templateId)
|
||||
try {
|
||||
await checklistApi.create({ title, scene: form.scene, templateId: form.templateId || undefined })
|
||||
await checklistApi.create({
|
||||
title,
|
||||
// 场景不再由用户选择: 沿用模板自带的场景 (无模板则为空), 兼容历史数据展示
|
||||
scene: tpl?.scene || '',
|
||||
templateId: form.templateId || undefined,
|
||||
})
|
||||
setShowForm(false)
|
||||
setForm({ title: '', scene: '出差', templateId: '' })
|
||||
setForm({ title: '', templateId: '' })
|
||||
setTab('active')
|
||||
load(true)
|
||||
} catch (e) {
|
||||
Toast.show({ content: e.message || '创建失败' })
|
||||
@@ -131,15 +157,12 @@ export default function ListView() {
|
||||
}
|
||||
|
||||
async function saveAsTemplate(list) {
|
||||
const items = (list.items || []).map((i) => ({ title: i.title, note: i.note }))
|
||||
if (!items.length) return Toast.show({ content: '空清单无法存为模板' })
|
||||
try {
|
||||
await checklistApi.createTemplate({
|
||||
name: list.title,
|
||||
scene: list.scene || '其他',
|
||||
items: (list.items || []).map((i) => ({ title: i.title, note: i.note })),
|
||||
})
|
||||
await checklistApi.createTemplate({ name: list.title, scene: list.scene || '', items })
|
||||
Toast.show({ content: '已保存为模板' })
|
||||
const d = await checklistApi.templates()
|
||||
setTemplates(d.items || [])
|
||||
loadTemplates()
|
||||
} catch (e) {
|
||||
Toast.show({ content: e.message || '保存失败' })
|
||||
}
|
||||
@@ -166,23 +189,148 @@ export default function ListView() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 模板 CRUD ----------
|
||||
|
||||
function templateById(id) {
|
||||
if (!id) return null
|
||||
return (templates || []).find((t) => String(t.id) === String(id)) || null
|
||||
}
|
||||
|
||||
function newTemplate() {
|
||||
setTplEdit({ id: null, name: '', items: [{ title: '' }], builtin: false })
|
||||
}
|
||||
|
||||
function editTemplate(tpl) {
|
||||
if (tpl.builtin) return Toast.show({ content: '系统预置模板不可编辑' })
|
||||
setTplEdit({
|
||||
id: tpl.id,
|
||||
name: tpl.name,
|
||||
builtin: false,
|
||||
items: (Array.isArray(tpl.items) ? tpl.items : []).map((i) => ({
|
||||
title: String(i?.title || ''),
|
||||
note: String(i?.note || ''),
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
function setTplItem(idx, title) {
|
||||
setTplEdit((t) => ({ ...t, items: t.items.map((it, i) => (i === idx ? { ...it, title } : it)) }))
|
||||
}
|
||||
|
||||
function addTplItem() {
|
||||
setTplEdit((t) => ({ ...t, items: [...t.items, { title: '' }] }))
|
||||
}
|
||||
|
||||
function removeTplItem(idx) {
|
||||
setTplEdit((t) => ({ ...t, items: t.items.filter((_, i) => i !== idx) }))
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!tplEdit) return
|
||||
const name = String(tplEdit.name || '').trim()
|
||||
const items = tplEdit.items
|
||||
.map((i) => ({ title: String(i.title || '').trim(), note: String(i.note || '') }))
|
||||
.filter((i) => i.title)
|
||||
if (!name) return Toast.show({ content: '请填写模板名称' })
|
||||
if (name.length > 20) return Toast.show({ content: '模板名称不能超过 20 个字' })
|
||||
if (!items.length) return Toast.show({ content: '模板至少需要一个检查项' })
|
||||
setTplSaving(true)
|
||||
try {
|
||||
if (tplEdit.id) await checklistApi.updateTemplate(tplEdit.id, { name, items })
|
||||
else await checklistApi.createTemplate({ name, items })
|
||||
Toast.show({ content: tplEdit.id ? '已保存' : '模板已创建' })
|
||||
setTplEdit(null)
|
||||
loadTemplates()
|
||||
} catch (e) {
|
||||
Toast.show({ content: e.message || '保存失败' })
|
||||
} finally {
|
||||
setTplSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTemplate(tpl) {
|
||||
if (tpl.builtin) return Toast.show({ content: '系统预置模板不可删除' })
|
||||
const ok = await Dialog.confirm({
|
||||
content: `删除模板「${tpl.name}」?已创建的清单不受影响。`,
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
})
|
||||
if (!ok) return
|
||||
try {
|
||||
await checklistApi.removeTemplate(tpl.id)
|
||||
Toast.show({ content: '模板已删除' })
|
||||
loadTemplates()
|
||||
} catch (e) {
|
||||
Toast.show({ content: e.message || '删除失败' })
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = useMemo(() => lists || [], [lists])
|
||||
const templateCount = (t) => (Array.isArray(t.items) ? t.items.length : 0)
|
||||
const selectedTpl = templateById(form.templateId)
|
||||
|
||||
return (
|
||||
<div className="list-view">
|
||||
<div className="list-view-head">
|
||||
<Segmented
|
||||
options={[{ label: '进行中', value: 'active' }, { label: '已归档', value: 'archived' }]}
|
||||
value={archived ? 'archived' : 'active'}
|
||||
onChange={(v) => setArchived(v === 'archived')}
|
||||
options={[
|
||||
{ label: '进行中', value: 'active' },
|
||||
{ label: '已归档', value: 'archived' },
|
||||
{ label: '模板', value: 'template' },
|
||||
]}
|
||||
value={tab}
|
||||
onChange={(v) => setTab(v)}
|
||||
/>
|
||||
<Button size="mini" color="primary" onClick={() => setShowForm(true)}>+ 新建</Button>
|
||||
{tab === 'template'
|
||||
? <Button size="mini" color="primary" onClick={newTemplate}>+ 新建模板</Button>
|
||||
: <Button size="mini" color="primary" onClick={() => openCreateForm()}>+ 新建</Button>}
|
||||
</div>
|
||||
|
||||
{/* ---------- 模板分类 ---------- */}
|
||||
{tab === 'template' && (
|
||||
<>
|
||||
{templates === null && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||||
{templates !== null && templates.length === 0 && (
|
||||
<Empty description="还没有模板">
|
||||
<Button color="primary" size="small" onClick={newTemplate}>创建第一个模板</Button>
|
||||
</Empty>
|
||||
)}
|
||||
<div className="template-wrap">
|
||||
{(templates || []).map((tpl) => (
|
||||
<div key={tpl.id} className="template-card">
|
||||
<div className="template-head">
|
||||
<span className="template-name">{tpl.name}</span>
|
||||
{tpl.builtin && <span className="template-badge">预置</span>}
|
||||
<span className="template-count">{templateCount(tpl)} 项</span>
|
||||
</div>
|
||||
<div className="template-items">
|
||||
{(Array.isArray(tpl.items) ? tpl.items : []).slice(0, 4).map((it, i) => (
|
||||
<span key={i} className="template-item-chip">{it.title}</span>
|
||||
))}
|
||||
{templateCount(tpl) > 4 && (
|
||||
<span className="template-item-chip more">+{templateCount(tpl) - 4}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="template-actions">
|
||||
<span className="habit-link" onClick={() => openCreateForm(String(tpl.id))}>由此新建</span>
|
||||
{!tpl.builtin && <span className="habit-link" onClick={() => editTemplate(tpl)}>编辑</span>}
|
||||
{!tpl.builtin && <span className="habit-link danger" onClick={() => removeTemplate(tpl)}>删除</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ---------- 清单列表 ---------- */}
|
||||
{tab !== 'template' && (
|
||||
<>
|
||||
{err && <div className="error-banner">{err}<span className="retry-link" onClick={() => load(false)}>重试</span></div>}
|
||||
{lists === null && !err && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||||
{lists !== null && !err && sorted.length === 0 && (
|
||||
<Empty description={archived ? '暂无归档清单' : '还没有检查清单'} />
|
||||
<Empty description={archived ? '暂无归档清单' : '还没有检查清单'}>
|
||||
{!archived && <Button color="primary" size="small" onClick={() => openCreateForm()}>新建清单</Button>}
|
||||
</Empty>
|
||||
)}
|
||||
|
||||
<div className="checklist-wrap">
|
||||
@@ -252,7 +400,10 @@ export default function ListView() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ---------- 新建清单 (名称 + 模板, 无场景) ---------- */}
|
||||
<Popup
|
||||
visible={showForm}
|
||||
onMaskClick={() => setShowForm(false)}
|
||||
@@ -262,23 +413,24 @@ export default function ListView() {
|
||||
<div className="form-title">新建检查清单</div>
|
||||
<div className="form-field">
|
||||
<label>名称</label>
|
||||
<Input placeholder="例如:出差 · 杭州" value={form.title} onChange={(v) => setForm((f) => ({ ...f, title: v }))} />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label>场景</label>
|
||||
<Selector
|
||||
options={SCENES.map((s) => ({ label: s, value: s }))}
|
||||
value={[form.scene]}
|
||||
onChange={(v) => setForm((f) => ({ ...f, scene: v[0] }))}
|
||||
<Input
|
||||
placeholder="例如:出差 · 杭州"
|
||||
value={form.title}
|
||||
maxLength={20}
|
||||
onChange={(v) => setForm((f) => ({ ...f, title: v }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label>从模板生成(可选)</label>
|
||||
<Selector
|
||||
options={[{ label: '不使用模板', value: '' }, ...templates.map((t) => ({ label: t.name, value: String(t.id) }))]}
|
||||
value={[form.templateId]}
|
||||
onChange={(v) => setForm((f) => ({ ...f, templateId: v[0] }))}
|
||||
/>
|
||||
<label>模板</label>
|
||||
<div className="template-pick-row" onClick={() => setPickerOpen(true)}>
|
||||
<span className={selectedTpl ? 'template-pick-name' : 'template-pick-name placeholder'}>
|
||||
{selectedTpl ? selectedTpl.name : '不使用模板(空白清单)'}
|
||||
</span>
|
||||
<span className="template-pick-meta">
|
||||
{selectedTpl ? `${templateCount(selectedTpl)} 项` : ''}
|
||||
<span className="template-pick-arrow">›</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<Button block color="primary" onClick={createList}>创建</Button>
|
||||
@@ -286,6 +438,91 @@ export default function ListView() {
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* ---------- 模板选择: 右侧滑出面板 ---------- */}
|
||||
<Popup
|
||||
visible={pickerOpen}
|
||||
position="right"
|
||||
onMaskClick={() => setPickerOpen(false)}
|
||||
bodyStyle={{ width: '78vw', maxWidth: 340, height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<div className="tpl-picker">
|
||||
<div className="tpl-picker-head">
|
||||
<span className="tpl-picker-title">选择模板</span>
|
||||
<span className="tpl-picker-close" onClick={() => setPickerOpen(false)}>×</span>
|
||||
</div>
|
||||
<div className="tpl-picker-tip">按名称字母顺序排列</div>
|
||||
<div className="tpl-picker-list">
|
||||
<div
|
||||
className={'tpl-picker-item' + (!form.templateId ? ' active' : '')}
|
||||
onClick={() => { setForm((f) => ({ ...f, templateId: '' })); setPickerOpen(false) }}
|
||||
>
|
||||
<span className="tpl-picker-name">不使用模板</span>
|
||||
<span className="tpl-picker-sub">空白清单</span>
|
||||
</div>
|
||||
{(templates || []).map((tpl) => (
|
||||
<div
|
||||
key={tpl.id}
|
||||
className={'tpl-picker-item' + (String(tpl.id) === String(form.templateId) ? ' active' : '')}
|
||||
onClick={() => { setForm((f) => ({ ...f, templateId: String(tpl.id) })); setPickerOpen(false) }}
|
||||
>
|
||||
<span className="tpl-picker-name">
|
||||
{tpl.name}
|
||||
{tpl.builtin && <span className="template-badge">预置</span>}
|
||||
</span>
|
||||
<span className="tpl-picker-sub">{templateCount(tpl)} 项</span>
|
||||
</div>
|
||||
))}
|
||||
{templates !== null && templates.length === 0 && (
|
||||
<div className="tpl-picker-empty">还没有可用模板</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* ---------- 模板编辑 ---------- */}
|
||||
<Popup
|
||||
visible={!!tplEdit}
|
||||
onMaskClick={() => setTplEdit(null)}
|
||||
bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, paddingBottom: 16, maxHeight: '82vh', overflowY: 'auto' }}
|
||||
>
|
||||
{tplEdit && (
|
||||
<div className="form-popup">
|
||||
<div className="form-title">{tplEdit.id ? '编辑模板' : '新建模板'}</div>
|
||||
<div className="form-field">
|
||||
<label>模板名称</label>
|
||||
<Input
|
||||
placeholder="例如:出差 / 露营"
|
||||
value={tplEdit.name}
|
||||
maxLength={20}
|
||||
onChange={(v) => setTplEdit((t) => ({ ...t, name: v }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label>检查项({tplEdit.items.length})</label>
|
||||
<div className="tpl-edit-items">
|
||||
{tplEdit.items.map((it, idx) => (
|
||||
<div key={idx} className="tpl-edit-item">
|
||||
<span className="tpl-edit-index">{idx + 1}</span>
|
||||
<Input
|
||||
placeholder="检查项内容"
|
||||
value={it.title}
|
||||
maxLength={40}
|
||||
onChange={(v) => setTplItem(idx, v)}
|
||||
/>
|
||||
<span className="checklist-item-del" onClick={() => removeTplItem(idx)}>×</span>
|
||||
</div>
|
||||
))}
|
||||
<Button size="mini" fill="outline" onClick={addTplItem} style={{ marginTop: 8 }}>+ 添加检查项</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<Button block color="primary" loading={tplSaving} onClick={saveTemplate}>保存</Button>
|
||||
<Button block fill="outline" onClick={() => setTplEdit(null)} style={{ marginTop: 8 }}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Popup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+129
-3
@@ -67,12 +67,31 @@ input, textarea, select { font: inherit; }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 带有 antd-mobile NavBar 的页面 (Home/Monitor/Macro/Article) */
|
||||
/* 带有页面头部的页面 (Checkin/Monitor/Macro) */
|
||||
/* height:100% 让页面占满 app-main 高度, 内部 tab-content 独立滚动,
|
||||
头部 NavBar 与分类 tab 固定在顶部不上滚 */
|
||||
头部与分类 tab 固定在顶部不上滚 */
|
||||
.page-with-navbar { display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
||||
.page-with-navbar .adm-error-block { flex: 1; }
|
||||
|
||||
/* 统一主 Tab 头部 (打卡 / 宏观 / 监控): 原 NavBar 45px → 23px, 标题主色小字
|
||||
注意: 「我」页不使用此头部, 见 MePage (顶部直接是头像区) */
|
||||
.page-header {
|
||||
height: 23px;
|
||||
flex: 0 0 23px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-header-title {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 底部 antd-mobile TabBar 定位 */
|
||||
.app-tabbar.adm-tab-bar {
|
||||
position: fixed;
|
||||
@@ -92,11 +111,53 @@ input, textarea, select { font: inherit; }
|
||||
|
||||
/* 宏观页 SwipeTabs */
|
||||
.swipe-tabs-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
/* tab 条右侧溢出提示: 渐变遮罩 + 箭头, 表示右边还有未展示的 tab
|
||||
遮罩宽度留足, 让被截断的 tab 文字淡出而不是被箭头压住 */
|
||||
.swipe-tabs-wrap .adm-tabs-header {
|
||||
position: relative;
|
||||
background: var(--color-card);
|
||||
}
|
||||
.swipe-tabs-wrap .adm-tabs-tab-wrapper { padding: 0 10px; }
|
||||
.tab-more-hint {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 45px;
|
||||
width: 58px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 6px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(to right, rgba(255,255,255,0) 0%, #fff 62%, #fff 100%);
|
||||
}
|
||||
.tab-more-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 1px solid #e3e6ea;
|
||||
color: var(--color-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.14);
|
||||
animation: tab-more-nudge 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes tab-more-nudge {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
50% { transform: translateX(3px); }
|
||||
}
|
||||
.tab-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -200,8 +261,8 @@ input, textarea, select { font: inherit; }
|
||||
.login-error { background: #fef2f2; color: var(--color-danger); padding: 10px; border-radius: 6px; font-size: 13px; margin-bottom: 12px; }
|
||||
|
||||
/* ================== 我的页 ================== */
|
||||
/* 「我」页不设顶部头部条: 头像区直接贴顶 (2026-10-23 界面调整) */
|
||||
.me-page { display: flex; flex-direction: column; min-height: 100%; background: var(--color-bg); }
|
||||
.me-navbar.adm-nav-bar { background: #fff; border-bottom: 1px solid var(--color-border); }
|
||||
.me-profile { background: linear-gradient(135deg, #1677ff, #4096ff); color: #fff; padding: 24px 16px 28px; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||
.me-avatar { background: rgba(255,255,255,.2); color: #fff; font-size: 28px; }
|
||||
.me-name { font-size: 18px; font-weight: 600; }
|
||||
@@ -652,6 +713,71 @@ input, textarea, select { font: inherit; }
|
||||
.checklist-add .adm-input { flex: 1; border: 1px solid var(--color-border); border-radius: 6px; padding: 6px 8px; }
|
||||
.checklist-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; }
|
||||
|
||||
/* ----- 模板分类 (清单的第三个分段: 进行中 / 已归档 / 模板) ----- */
|
||||
.template-wrap { display: flex; flex-direction: column; gap: 10px; }
|
||||
.template-card {
|
||||
background: #fff; border-radius: 10px; padding: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||||
}
|
||||
.template-head { display: flex; align-items: center; gap: 8px; }
|
||||
.template-name { font-size: 15px; font-weight: 600; color: var(--color-text); word-break: break-all; }
|
||||
.template-badge {
|
||||
font-size: 10px; color: var(--color-muted); background: #f2f3f5;
|
||||
border-radius: 4px; padding: 1px 5px; flex-shrink: 0;
|
||||
}
|
||||
.template-count { margin-left: auto; font-size: 11px; color: var(--color-muted); flex-shrink: 0; }
|
||||
.template-items { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
.template-item-chip {
|
||||
font-size: 11px; color: var(--color-muted); background: #f7f8fa;
|
||||
border-radius: 4px; padding: 2px 7px; max-width: 100%;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.template-item-chip.more { color: var(--color-primary); background: #f0f5ff; }
|
||||
.template-actions {
|
||||
display: flex; flex-wrap: wrap; gap: 14px; margin-top: 10px;
|
||||
padding-top: 8px; border-top: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
/* 新建清单: 模板行 (点击后从右侧滑出选择面板) */
|
||||
.template-pick-row {
|
||||
display: flex; align-items: center; gap: 8px; cursor: pointer;
|
||||
border: 1px solid var(--color-border); border-radius: 8px; padding: 8px 10px;
|
||||
}
|
||||
.template-pick-row:active { background: #fafafa; }
|
||||
.template-pick-name { flex: 1; font-size: 14px; color: var(--color-text); }
|
||||
.template-pick-name.placeholder { color: #b8bcc4; }
|
||||
.template-pick-meta { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--color-muted); }
|
||||
.template-pick-arrow { font-size: 16px; line-height: 1; color: #c8c8c8; }
|
||||
|
||||
/* 模板选择面板 (右侧滑出) */
|
||||
.tpl-picker { display: flex; flex-direction: column; height: 100%; padding: 14px 0 0; }
|
||||
.tpl-picker-head { display: flex; align-items: center; padding: 0 16px 6px; }
|
||||
.tpl-picker-title { font-size: 16px; font-weight: 700; color: var(--color-text); }
|
||||
.tpl-picker-close { margin-left: auto; font-size: 22px; line-height: 1; color: #c8c8c8; cursor: pointer; padding: 0 4px; }
|
||||
.tpl-picker-tip { font-size: 11px; color: var(--color-muted); padding: 0 16px 8px; }
|
||||
.tpl-picker-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0 10px 16px; }
|
||||
.tpl-picker-item {
|
||||
display: flex; align-items: baseline; gap: 8px; padding: 11px 10px;
|
||||
border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
.tpl-picker-item:active { background: #f5f6f8; }
|
||||
.tpl-picker-item.active { background: #f0f5ff; }
|
||||
.tpl-picker-name {
|
||||
flex: 1; font-size: 14px; font-weight: 600; color: var(--color-text);
|
||||
display: inline-flex; align-items: center; gap: 6px; word-break: break-all;
|
||||
}
|
||||
.tpl-picker-item.active .tpl-picker-name { color: var(--color-primary); }
|
||||
.tpl-picker-sub { font-size: 11px; color: var(--color-muted); flex-shrink: 0; }
|
||||
.tpl-picker-empty { padding: 40px 0; text-align: center; font-size: 13px; color: var(--color-muted); }
|
||||
|
||||
/* 模板编辑弹层 */
|
||||
.tpl-edit-items { display: flex; flex-direction: column; gap: 6px; }
|
||||
.tpl-edit-item { display: flex; align-items: center; gap: 8px; }
|
||||
.tpl-edit-index { width: 18px; text-align: right; font-size: 11px; color: var(--color-muted); flex-shrink: 0; }
|
||||
.tpl-edit-item .adm-input {
|
||||
flex: 1; border: 1px solid var(--color-border); border-radius: 6px; padding: 6px 8px;
|
||||
}
|
||||
|
||||
/* ----- 表单弹窗 (打卡项 / 清单) ----- */
|
||||
.form-popup { padding: 16px 16px 8px; }
|
||||
.form-title { font-size: 16px; font-weight: 700; color: var(--color-text); margin-bottom: 12px; }
|
||||
|
||||
Reference in New Issue
Block a user