Files
zkxw/mobile-ui/zkxw-mobile-vant-ui-demo.html
T
zlwuandCursor 031e456614 docs: record mobile-h5 Vant UI design, plan, and demo
Link the workspace UI demo and implementation plan from the
miniprogram UI spec for the unified mobile workbench.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 14:03:36 +08:00

3677 lines
107 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>中科兴皖 · 移动端 UI Demo(Vant)</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vant@4.9.21/lib/index.css" />
<style>
:root {
--brand-green: #0a9f6d;
--brand-green-dark: #087f57;
--brand-green-soft: #e9f9f2;
--barley-yellow: #d6a343;
--barley-yellow-soft: #fff7e8;
--bg-page: #f5f8f7;
--surface: #ffffff;
--text-primary: #1f2a26;
--text-muted: #6b7b74;
--border: #dcebe4;
--danger: #d14343;
--warning: #cf8a19;
--success: #189b65;
--radius-card: 14px;
--radius-lg: 20px;
--shadow-soft: 0 4px 16px rgba(8, 127, 87, 0.08);
--van-primary-color: #0a9f6d;
--van-success-color: #189b65;
--van-warning-color: #cf8a19;
--van-danger-color: #d14343;
--van-border-color: #dcebe4;
--van-background: #f5f8f7;
--van-background-2: #ffffff;
--van-text-color: #1f2a26;
--van-text-color-2: #6b7b74;
--van-button-primary-background: #0a9f6d;
--van-button-primary-border-color: #0a9f6d;
--van-button-radius: 10px;
--van-cell-group-inset-padding: 12px;
--van-cell-group-inset-radius: 14px;
--van-dialog-confirm-button-text-color: #0a9f6d;
--van-nav-bar-icon-color: #ffffff;
--van-nav-bar-text-color: #ffffff;
--van-nav-bar-title-text-color: #ffffff;
--van-nav-bar-background: transparent;
--van-tabbar-item-active-color: #0a9f6d;
--van-tag-radius: 6px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
color: var(--text-primary);
background:
radial-gradient(900px 420px at 10% -10%, rgba(10, 159, 109, 0.14), transparent 55%),
radial-gradient(700px 380px at 90% 0%, rgba(214, 163, 67, 0.12), transparent 50%),
linear-gradient(180deg, #eef6f2 0%, #f7faf9 40%, #f3f6f5 100%);
}
body {
display: flex;
justify-content: center;
padding: 24px 16px 40px;
}
#app {
width: 100%;
max-width: 1120px;
display: grid;
grid-template-columns: 280px 1fr;
gap: 24px;
align-items: start;
}
.doc-panel {
position: sticky;
top: 24px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid var(--border);
border-radius: 18px;
padding: 20px 18px;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(8px);
}
.doc-panel h1 {
margin: 0 0 6px;
font-size: 20px;
line-height: 1.3;
color: var(--brand-green-dark);
}
.doc-panel .sub {
margin: 0 0 16px;
font-size: 12px;
color: var(--text-muted);
line-height: 1.5;
}
.doc-panel .section-title {
margin: 16px 0 8px;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.screen-nav {
display: flex;
flex-direction: column;
gap: 8px;
}
.screen-nav button {
appearance: none;
border: 1px solid var(--border);
background: #fff;
border-radius: 12px;
padding: 12px 14px;
text-align: left;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
transition: border-color .2s, background .2s, color .2s;
}
.screen-nav button:hover {
border-color: #a7e0c8;
background: var(--brand-green-soft);
}
.screen-nav button.active {
border-color: var(--brand-green);
background: var(--brand-green-soft);
color: var(--brand-green-dark);
font-weight: 600;
}
.token-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.token-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-muted);
background: #fff;
border: 1px solid var(--border);
border-radius: 999px;
padding: 4px 8px 4px 4px;
}
.token-chip i {
width: 14px;
height: 14px;
border-radius: 50%;
display: block;
}
.notes {
margin: 12px 0 0;
padding: 0;
list-style: none;
font-size: 12px;
color: var(--text-muted);
line-height: 1.55;
}
.notes li {
position: relative;
padding-left: 12px;
margin-bottom: 6px;
}
.notes li::before {
content: "";
position: absolute;
left: 0;
top: 7px;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--brand-green);
}
.phone-stage {
display: flex;
justify-content: center;
}
.phone {
width: 390px;
height: 844px;
background: #111;
border-radius: 42px;
padding: 12px;
box-shadow:
0 24px 60px rgba(15, 40, 30, 0.18),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
position: relative;
}
.phone::before {
content: "";
position: absolute;
top: 18px;
left: 50%;
transform: translateX(-50%);
width: 118px;
height: 28px;
background: #0a0a0a;
border-radius: 20px;
z-index: 5;
}
.phone-screen {
width: 100%;
height: 100%;
border-radius: 32px;
overflow: hidden;
background: var(--bg-page);
position: relative;
display: flex;
flex-direction: column;
}
.screen-scroll {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 16px;
}
/* ========== 首页 ========== */
.home-header {
background: linear-gradient(145deg, var(--brand-green-dark), var(--brand-green));
color: #fff;
padding: 54px 16px 28px;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
position: relative;
overflow: hidden;
}
.home-header::after {
content: "";
position: absolute;
right: -40px;
top: -30px;
width: 160px;
height: 160px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
}
.home-header .brand {
font-size: 12px;
opacity: 0.88;
letter-spacing: 0.04em;
margin-bottom: 8px;
}
.home-header h2 {
margin: 0;
font-size: 22px;
font-weight: 700;
letter-spacing: 0.02em;
}
.home-header .meta {
margin-top: 10px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
opacity: 0.92;
}
.home-header .meta .dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(255,255,255,.7);
}
.home-body {
padding: 14px 12px 0;
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-card);
box-shadow: var(--shadow-soft);
padding: 14px 12px;
margin-bottom: 12px;
}
.panel-hd {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding: 0 4px;
}
.panel-hd h3 {
margin: 0;
font-size: 15px;
font-weight: 650;
}
.panel-hd .hint {
font-size: 11px;
color: var(--text-muted);
}
.menu-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px 6px;
}
.menu-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 8px;
min-height: 84px;
padding: 10px 4px 6px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 12px;
transition: background .18s, transform .18s;
-webkit-tap-highlight-color: transparent;
}
.menu-card:active {
background: var(--brand-green-soft);
transform: scale(0.97);
}
.menu-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
background: var(--brand-green-soft);
color: var(--brand-green-dark);
font-size: 24px;
box-shadow: inset 0 0 0 1px rgba(10, 159, 109, 0.08);
}
.menu-icon.tone-yellow {
background: var(--barley-yellow-soft);
color: #a67b20;
}
.menu-icon.tone-slate {
background: #eef2f1;
color: #3d524a;
}
.menu-title {
font-size: 12px;
line-height: 1.25;
color: var(--text-primary);
text-align: center;
max-width: 72px;
word-break: break-all;
}
.perm-banner {
display: flex;
gap: 8px;
align-items: flex-start;
padding: 10px 12px;
border-radius: 12px;
background: var(--barley-yellow-soft);
border: 1px solid #f0e0b8;
font-size: 12px;
color: #8a6520;
margin-bottom: 12px;
line-height: 1.45;
}
/* ========== 待办摘要 ========== */
.todo-panel {
padding: 14px 12px 12px;
background:
linear-gradient(165deg, rgba(233, 249, 242, 0.95) 0%, #fff 42%),
#fff;
}
.todo-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.todo-stat {
position: relative;
border: none;
border-radius: 12px;
padding: 12px 10px 10px;
text-align: left;
cursor: pointer;
background: #fff;
box-shadow: 0 2px 10px rgba(8, 127, 87, 0.06);
border: 1px solid rgba(220, 235, 228, 0.9);
transition: transform .18s, box-shadow .18s;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
}
.todo-stat:active {
transform: scale(0.97);
}
.todo-stat::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
border-radius: 3px 0 0 3px;
}
.todo-stat.tone-warn::before { background: var(--warning); }
.todo-stat.tone-pay::before { background: var(--barley-yellow); }
.todo-stat.tone-danger::before { background: var(--danger); }
.todo-stat.tone-warn { background: linear-gradient(180deg, #fffaf0 0%, #fff 70%); }
.todo-stat.tone-pay { background: linear-gradient(180deg, #fff8ec 0%, #fff 70%); }
.todo-stat.tone-danger { background: linear-gradient(180deg, #fff5f5 0%, #fff 70%); }
.todo-stat .num {
display: block;
font-size: 22px;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.todo-stat.tone-warn .num { color: #b77412; }
.todo-stat.tone-pay .num { color: #9a7424; }
.todo-stat.tone-danger .num { color: var(--danger); }
.todo-stat .label {
display: block;
margin-top: 4px;
font-size: 11px;
color: var(--text-muted);
}
.todo-stat .stat-icon {
position: absolute;
right: 8px;
top: 8px;
width: 26px;
height: 26px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
opacity: 0.9;
}
.todo-stat.tone-warn .stat-icon {
background: rgba(207, 138, 25, 0.12);
color: var(--warning);
}
.todo-stat.tone-pay .stat-icon {
background: rgba(214, 163, 67, 0.16);
color: #a67b20;
}
.todo-stat.tone-danger .stat-icon {
background: rgba(209, 67, 67, 0.12);
color: var(--danger);
}
.todo-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.todo-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 10px;
border: 1px solid var(--border);
border-radius: 12px;
background: #fff;
cursor: pointer;
text-align: left;
transition: background .15s, border-color .15s;
}
.todo-item:active {
background: var(--brand-green-soft);
border-color: #b7e5d0;
}
.todo-item .ti-icon {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 18px;
}
.todo-item .ti-icon.i-approve {
background: #fff3e0;
color: var(--warning);
}
.todo-item .ti-icon.i-pay {
background: var(--barley-yellow-soft);
color: #a67b20;
}
.todo-item .ti-icon.i-alert {
background: #fdecec;
color: var(--danger);
}
.todo-item .ti-body {
flex: 1;
min-width: 0;
}
.todo-item .ti-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.3;
}
.todo-item .ti-desc {
margin-top: 2px;
font-size: 11px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.todo-item .ti-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
}
.todo-item .ti-time {
font-size: 10px;
color: #9aada4;
}
/* ========== 列表 ========== */
.page-nav {
background: linear-gradient(145deg, var(--brand-green-dark), var(--brand-green));
padding: 44px 8px 10px;
}
.page-nav .van-nav-bar {
background: transparent;
}
.page-nav .van-hairline--bottom::after {
display: none;
}
.list-page {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background:
radial-gradient(420px 220px at 100% 0%, rgba(214, 163, 67, 0.16), transparent 55%),
radial-gradient(380px 240px at 0% 30%, rgba(10, 159, 109, 0.10), transparent 50%),
linear-gradient(180deg, #e8f5ef 0%, #f3f7f5 38%, #f7f9f8 100%);
}
.list-page .screen-scroll {
background: transparent;
}
.list-hero {
background:
radial-gradient(220px 140px at 92% 18%, rgba(255, 255, 255, 0.18), transparent 60%),
linear-gradient(155deg, #066647 0%, #0a9f6d 48%, #12b37a 100%);
padding: 44px 14px 22px;
border-radius: 0 0 28px 28px;
position: relative;
overflow: hidden;
}
.list-hero::after {
content: "";
position: absolute;
width: 180px;
height: 180px;
right: -50px;
bottom: -70px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.list-hero .van-nav-bar {
background: transparent;
margin: 0 -6px;
}
.list-hero .van-hairline--bottom::after {
display: none;
}
.list-hero-copy {
position: relative;
z-index: 1;
margin: 4px 2px 14px;
color: #fff;
}
.list-hero-copy .eyebrow {
font-size: 11px;
letter-spacing: 0.08em;
opacity: 0.82;
margin-bottom: 4px;
}
.list-hero-copy h2 {
margin: 0;
font-size: 22px;
font-weight: 700;
letter-spacing: 0.01em;
}
.list-hero-copy .subline {
margin-top: 6px;
font-size: 12px;
opacity: 0.88;
display: flex;
align-items: center;
gap: 8px;
}
.list-hero-copy .subline em {
font-style: normal;
font-weight: 700;
background: rgba(255, 255, 255, 0.18);
border-radius: 999px;
padding: 1px 8px;
}
.list-search {
position: relative;
z-index: 1;
}
.list-search .van-search {
padding: 0;
}
.list-search .van-search__content {
background: rgba(255, 255, 255, 0.96);
border-radius: 14px;
box-shadow: 0 8px 24px rgba(4, 70, 48, 0.18);
}
.list-filter-dock {
margin: -14px 12px 0;
position: relative;
z-index: 3;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(220, 235, 228, 0.95);
border-radius: 18px;
padding: 10px;
box-shadow: 0 10px 28px rgba(15, 55, 40, 0.08);
backdrop-filter: blur(10px);
}
.seg-tabs {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
padding: 4px;
border-radius: 14px;
background: #eef6f2;
}
.seg-tab {
border: none;
background: transparent;
border-radius: 11px;
padding: 8px 4px;
cursor: pointer;
color: #6b7b74;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
transition: all .2s;
-webkit-tap-highlight-color: transparent;
}
.seg-tab .lab {
font-size: 12px;
line-height: 1.2;
}
.seg-tab .count {
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: #9aada4;
}
.seg-tab.active {
background: #fff;
color: var(--brand-green-dark);
box-shadow: 0 4px 12px rgba(8, 127, 87, 0.14);
font-weight: 650;
}
.seg-tab.active .count {
color: var(--brand-green);
}
.list-wrap {
padding: 16px 12px 0;
}
.biz-card {
position: relative;
border: none;
border-radius: 20px;
padding: 0;
margin-bottom: 14px;
overflow: hidden;
box-shadow: 0 10px 28px rgba(20, 55, 40, 0.08);
transition: transform .16s, box-shadow .16s;
isolation: isolate;
}
.biz-card:active {
transform: translateY(1px) scale(0.995);
box-shadow: 0 6px 16px rgba(20, 55, 40, 0.07);
}
.biz-card::before {
content: "";
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
}
.biz-card::after {
content: "";
position: absolute;
width: 140px;
height: 140px;
right: -36px;
top: -48px;
border-radius: 50%;
z-index: 0;
pointer-events: none;
opacity: 0.55;
}
.biz-card.status-warning {
background: linear-gradient(145deg, #fff9ef 0%, #ffffff 46%, #fffaf3 100%);
}
.biz-card.status-warning::before {
background: linear-gradient(90deg, rgba(214, 163, 67, 0.22), transparent 28%);
}
.biz-card.status-warning::after {
background: radial-gradient(circle, rgba(214, 163, 67, 0.28), transparent 68%);
}
.biz-card.status-primary {
background: linear-gradient(145deg, #eaf8f1 0%, #ffffff 48%, #f3fbf7 100%);
}
.biz-card.status-primary::before {
background: linear-gradient(90deg, rgba(10, 159, 109, 0.18), transparent 28%);
}
.biz-card.status-primary::after {
background: radial-gradient(circle, rgba(10, 159, 109, 0.22), transparent 68%);
}
.biz-card.status-danger {
background: linear-gradient(145deg, #fff1f1 0%, #ffffff 48%, #fff7f7 100%);
}
.biz-card.status-danger::before {
background: linear-gradient(90deg, rgba(209, 67, 67, 0.16), transparent 28%);
}
.biz-card.status-danger::after {
background: radial-gradient(circle, rgba(209, 67, 67, 0.18), transparent 68%);
}
.biz-card.status-success {
background: linear-gradient(145deg, #eef8f3 0%, #ffffff 50%, #f6fbf8 100%);
}
.biz-card.status-success::before {
background: linear-gradient(90deg, rgba(24, 155, 101, 0.16), transparent 28%);
}
.biz-card.status-success::after {
background: radial-gradient(circle, rgba(24, 155, 101, 0.18), transparent 68%);
}
.biz-card .card-inner {
position: relative;
z-index: 1;
padding: 14px 14px 12px;
}
.biz-card .row-top {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 12px;
}
.biz-card .type-icon {
width: 44px;
height: 44px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 22px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.65);
}
.biz-card.status-warning .type-icon {
background: linear-gradient(145deg, #ffe8b8, #f6d089);
color: #7a5610;
}
.biz-card.status-primary .type-icon {
background: linear-gradient(145deg, #8fe0bf, #0a9f6d);
color: #fff;
}
.biz-card.status-danger .type-icon {
background: linear-gradient(145deg, #ffb4b4, #e85a5a);
color: #fff;
}
.biz-card.status-success .type-icon {
background: linear-gradient(145deg, #b8efd4, #189b65);
color: #fff;
}
.biz-card .title-wrap {
flex: 1;
min-width: 0;
padding-top: 1px;
}
.biz-card .type-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-weight: 650;
letter-spacing: 0.04em;
padding: 2px 7px;
border-radius: 999px;
margin-bottom: 5px;
}
.biz-card.status-warning .type-chip {
background: rgba(214, 163, 67, 0.16);
color: #8f6718;
}
.biz-card.status-primary .type-chip {
background: rgba(10, 159, 109, 0.12);
color: #087f57;
}
.biz-card.status-danger .type-chip {
background: rgba(209, 67, 67, 0.12);
color: #b13535;
}
.biz-card.status-success .type-chip {
background: rgba(24, 155, 101, 0.12);
color: #147a50;
}
.biz-card .title {
font-size: 15px;
font-weight: 700;
line-height: 1.35;
color: #173128;
letter-spacing: -0.01em;
}
.biz-card .status-pill {
flex-shrink: 0;
margin-top: 2px;
font-size: 11px;
font-weight: 700;
padding: 4px 9px;
border-radius: 999px;
letter-spacing: 0.02em;
}
.biz-card.status-warning .status-pill {
background: #fff;
color: #a67b20;
box-shadow: 0 2px 8px rgba(214, 163, 67, 0.22);
}
.biz-card.status-primary .status-pill {
background: #0a9f6d;
color: #fff;
box-shadow: 0 4px 12px rgba(10, 159, 109, 0.28);
}
.biz-card.status-danger .status-pill {
background: #d14343;
color: #fff;
box-shadow: 0 4px 12px rgba(209, 67, 67, 0.25);
}
.biz-card.status-success .status-pill {
background: rgba(24, 155, 101, 0.12);
color: #147a50;
}
.biz-card .body-row {
display: flex;
gap: 12px;
align-items: stretch;
}
.biz-card .desc-box {
flex: 1;
margin: 0;
padding: 10px 11px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.9);
font-size: 12px;
color: #3f564c;
line-height: 1.5;
backdrop-filter: blur(2px);
}
.biz-card .side-metric {
width: 86px;
flex-shrink: 0;
border-radius: 12px;
padding: 8px 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
text-align: right;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 0.95);
}
.biz-card .side-metric .amt-label {
font-size: 10px;
color: #8aa198;
}
.biz-card .side-metric .amt {
margin-top: 2px;
font-size: 15px;
font-weight: 800;
color: #173128;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1.15;
}
.biz-card.status-warning .side-metric .amt { color: #9a7424; }
.biz-card.status-primary .side-metric .amt { color: #087f57; }
.biz-card.status-danger .side-metric .amt { color: #b13535; }
.biz-card .side-metric .time {
margin-top: 6px;
font-size: 10px;
color: #8aa198;
}
.biz-card .meta-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.biz-card .meta-row .mi {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #5d746a;
background: rgba(255, 255, 255, 0.7);
border-radius: 999px;
padding: 3px 8px;
}
.biz-card .meta-row .van-icon {
font-size: 12px;
color: #7f968c;
}
.biz-card .code-pill {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
letter-spacing: 0.02em;
}
.biz-card .footer {
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed rgba(140, 170, 155, 0.28);
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.biz-card .footer .left-hint {
font-size: 11px;
color: #7f968c;
display: inline-flex;
align-items: center;
gap: 4px;
}
.biz-card .footer .left-hint i {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
background: currentColor;
opacity: 0.75;
}
.biz-card.status-warning .footer .left-hint { color: #a67b20; }
.biz-card.status-primary .footer .left-hint { color: #0a9f6d; }
.biz-card.status-danger .footer .left-hint { color: #d14343; }
.biz-card.status-success .footer .left-hint { color: #189b65; }
.biz-card .footer .ops {
display: flex;
gap: 8px;
}
.biz-card .footer .van-button {
min-width: 64px;
border-radius: 999px;
height: 30px;
}
.biz-card .footer .van-button--primary {
background: linear-gradient(135deg, #12b37a, #087f57);
border: none;
box-shadow: 0 4px 12px rgba(8, 127, 87, 0.28);
}
.list-end {
padding: 8px 0 28px;
text-align: center;
font-size: 11px;
color: #8aa198;
letter-spacing: 0.08em;
}
.list-end span {
display: inline-flex;
align-items: center;
gap: 10px;
}
.list-end span::before,
.list-end span::after {
content: "";
width: 28px;
height: 1px;
background: linear-gradient(90deg, transparent, #c5d8ce);
}
.list-end span::after {
background: linear-gradient(90deg, #c5d8ce, transparent);
}
.list-empty-pad {
padding: 40px 0;
}
/* ========== 详情 ========== */
.detail-page {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background:
radial-gradient(380px 200px at 100% 8%, rgba(214, 163, 67, 0.14), transparent 55%),
linear-gradient(180deg, #e8f5ef 0%, #f4f7f6 42%, #f7f9f8 100%);
}
.detail-page .screen-scroll {
background: transparent;
padding-bottom: 88px;
}
.detail-hero {
position: relative;
overflow: hidden;
padding: 44px 14px 28px;
border-radius: 0 0 28px 28px;
color: #fff;
}
.detail-hero.status-warning {
background:
radial-gradient(220px 140px at 90% 10%, rgba(255, 255, 255, 0.2), transparent 60%),
linear-gradient(155deg, #a67b20 0%, #d6a343 45%, #e8c06a 100%);
}
.detail-hero.status-primary {
background:
radial-gradient(220px 140px at 90% 10%, rgba(255, 255, 255, 0.18), transparent 60%),
linear-gradient(155deg, #066647 0%, #0a9f6d 48%, #12b37a 100%);
}
.detail-hero.status-danger {
background:
radial-gradient(220px 140px at 90% 10%, rgba(255, 255, 255, 0.16), transparent 60%),
linear-gradient(155deg, #9c2f2f 0%, #d14343 50%, #e36a6a 100%);
}
.detail-hero.status-success {
background:
radial-gradient(220px 140px at 90% 10%, rgba(255, 255, 255, 0.16), transparent 60%),
linear-gradient(155deg, #0f6b48 0%, #189b65 52%, #3bb883 100%);
}
.detail-hero::after {
content: "";
position: absolute;
width: 180px;
height: 180px;
right: -48px;
bottom: -70px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.detail-hero .van-nav-bar {
background: transparent;
margin: 0 -6px;
}
.detail-hero .van-hairline--bottom::after {
display: none;
}
.detail-hero-body {
position: relative;
z-index: 1;
margin-top: 6px;
}
.detail-hero .type-line {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.detail-hero .type-chip {
font-size: 11px;
font-weight: 650;
letter-spacing: 0.04em;
padding: 3px 9px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.detail-hero .status-chip {
font-size: 11px;
font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
background: #fff;
color: #173128;
}
.detail-hero.status-warning .status-chip { color: #8f6718; }
.detail-hero.status-primary .status-chip { color: #087f57; }
.detail-hero.status-danger .status-chip { color: #b13535; }
.detail-hero.status-success .status-chip { color: #147a50; }
.detail-hero h2 {
margin: 0;
font-size: 20px;
font-weight: 750;
line-height: 1.35;
letter-spacing: -0.01em;
}
.detail-hero .code {
margin-top: 8px;
font-size: 12px;
opacity: 0.92;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.02em;
}
.detail-metric-dock {
margin: -18px 12px 0;
position: relative;
z-index: 3;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 12px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(220, 235, 228, 0.95);
box-shadow: 0 12px 30px rgba(15, 55, 40, 0.1);
backdrop-filter: blur(10px);
}
.detail-metric {
text-align: center;
padding: 4px 2px;
}
.detail-metric .lab {
display: block;
font-size: 10px;
color: #8aa198;
margin-bottom: 4px;
}
.detail-metric .val {
display: block;
font-size: 14px;
font-weight: 750;
color: #173128;
line-height: 1.25;
word-break: break-all;
}
.detail-metric .val.money {
color: #9a7424;
font-variant-numeric: tabular-nums;
}
.detail-body {
padding: 16px 12px 0;
}
.detail-section {
background: #fff;
border-radius: 18px;
border: 1px solid rgba(220, 235, 228, 0.9);
box-shadow: 0 8px 22px rgba(20, 55, 40, 0.05);
padding: 14px 14px 12px;
margin-bottom: 12px;
overflow: hidden;
}
.detail-section .sec-hd {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.detail-section .sec-hd h3 {
margin: 0;
font-size: 14px;
font-weight: 700;
color: #173128;
display: inline-flex;
align-items: center;
gap: 8px;
}
.detail-section .sec-hd h3::before {
content: "";
width: 4px;
height: 14px;
border-radius: 999px;
background: linear-gradient(180deg, #12b37a, #087f57);
}
.detail-section .sec-hd .more {
border: none;
background: transparent;
color: var(--brand-green);
font-size: 12px;
cursor: pointer;
padding: 0;
}
.kv-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.kv-row {
display: flex;
align-items: flex-start;
gap: 12px;
font-size: 13px;
line-height: 1.45;
}
.kv-row .k {
width: 72px;
flex-shrink: 0;
color: #8aa198;
}
.kv-row .v {
flex: 1;
color: #173128;
font-weight: 550;
word-break: break-all;
}
.detail-note {
margin: 0;
padding: 10px 12px;
border-radius: 12px;
background: linear-gradient(135deg, #fff8ec, #fffdf8);
border: 1px solid #f0e0b8;
color: #8a6520;
font-size: 12px;
line-height: 1.5;
}
/* 详情 · 流转记录 */
.flow-list {
display: flex;
flex-direction: column;
gap: 0;
position: relative;
}
.flow-item {
position: relative;
display: flex;
gap: 12px;
padding-bottom: 14px;
}
.flow-item:last-child {
padding-bottom: 0;
}
.flow-item:not(:last-child) .flow-rail::after {
content: "";
position: absolute;
left: 50%;
top: 36px;
bottom: -14px;
width: 2px;
transform: translateX(-50%);
background: linear-gradient(180deg, rgba(10, 159, 109, 0.45), rgba(220, 235, 228, 0.9));
border-radius: 2px;
}
.flow-rail {
position: relative;
width: 36px;
flex-shrink: 0;
display: flex;
justify-content: center;
}
.flow-dot {
width: 36px;
height: 36px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
box-shadow: 0 6px 14px rgba(8, 127, 87, 0.2);
position: relative;
z-index: 1;
}
.flow-item.done .flow-dot {
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
}
.flow-item.warn .flow-dot {
background: linear-gradient(145deg, #ecc06a, #d6a343);
box-shadow: 0 6px 14px rgba(214, 163, 67, 0.28);
}
.flow-item.danger .flow-dot {
background: linear-gradient(145deg, #ef7a7a, #d14343);
box-shadow: 0 6px 14px rgba(209, 67, 67, 0.25);
}
.flow-card {
flex: 1;
min-width: 0;
border-radius: 14px;
padding: 11px 12px;
background: linear-gradient(160deg, #f7fbf9 0%, #ffffff 55%);
border: 1px solid #e4eee9;
box-shadow: 0 4px 12px rgba(20, 55, 40, 0.04);
}
.flow-item.warn .flow-card {
background: linear-gradient(160deg, #fff9ef 0%, #ffffff 60%);
border-color: #f0e0b8;
}
.flow-item.danger .flow-card {
background: linear-gradient(160deg, #fff4f4 0%, #ffffff 60%);
border-color: #f3d0d0;
}
.flow-card .flow-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
margin-bottom: 4px;
}
.flow-card .flow-title {
font-size: 13px;
font-weight: 700;
color: #173128;
line-height: 1.35;
}
.flow-card .flow-badge {
flex-shrink: 0;
font-size: 10px;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
letter-spacing: 0.02em;
}
.flow-item.done .flow-badge {
background: rgba(10, 159, 109, 0.12);
color: #087f57;
}
.flow-item.warn .flow-badge {
background: rgba(214, 163, 67, 0.16);
color: #8f6718;
}
.flow-item.danger .flow-badge {
background: rgba(209, 67, 67, 0.12);
color: #b13535;
}
.flow-card .flow-desc {
font-size: 12px;
color: #5d746a;
line-height: 1.5;
}
.flow-card .flow-meta {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.flow-card .flow-meta .chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #6b7b74;
background: rgba(255, 255, 255, 0.9);
border: 1px solid #e8f0ec;
border-radius: 999px;
padding: 2px 8px;
}
.flow-card .flow-meta .van-icon {
font-size: 12px;
color: #8aa198;
}
.pay-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
background: #f7faf9;
margin-bottom: 8px;
}
.pay-row:last-child {
margin-bottom: 0;
}
.pay-row .left .name {
font-size: 13px;
font-weight: 650;
color: #173128;
}
.pay-row .left .sub {
margin-top: 2px;
font-size: 11px;
color: #8aa198;
}
.pay-row .right {
text-align: right;
}
.pay-row .right .amt {
font-size: 14px;
font-weight: 750;
color: #9a7424;
font-variant-numeric: tabular-nums;
}
/* 详情 · 附件 */
.d-attach-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.d-attach-upload {
width: 100%;
border: 1.5px dashed #9ad9be;
border-radius: 14px;
background:
linear-gradient(135deg, rgba(233, 249, 242, 0.95), rgba(255, 255, 255, 0.95)),
#e9f9f2;
padding: 14px 12px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
text-align: left;
transition: transform .15s, border-color .15s;
}
.d-attach-upload:active {
transform: scale(0.99);
border-color: #0a9f6d;
}
.d-attach-upload .up-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
box-shadow: 0 6px 14px rgba(10, 159, 109, 0.28);
flex-shrink: 0;
}
.d-attach-upload .up-text {
flex: 1;
min-width: 0;
}
.d-attach-upload .up-title {
font-size: 13px;
font-weight: 700;
color: #087f57;
}
.d-attach-upload .up-sub {
margin-top: 2px;
font-size: 11px;
color: #6b7b74;
line-height: 1.4;
}
.d-attach-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 14px;
background: #fff;
border: 1px solid #e4eee9;
box-shadow: 0 4px 14px rgba(20, 55, 40, 0.05);
position: relative;
overflow: hidden;
}
.d-attach-card::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--file-accent, #0a9f6d);
}
.d-attach-card.ft-pdf { --file-accent: #d14343; }
.d-attach-card.ft-img { --file-accent: #0a9f6d; }
.d-attach-card.ft-xls { --file-accent: #189b65; }
.d-attach-card.ft-doc { --file-accent: #3b82f6; }
.d-attach-card.ft-other { --file-accent: #d6a343; }
.d-attach-card .file-icon {
width: 46px;
height: 46px;
border-radius: 13px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
flex-shrink: 0;
color: #fff;
font-weight: 800;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.d-attach-card.ft-pdf .file-icon {
background: linear-gradient(145deg, #f07171, #d14343);
}
.d-attach-card.ft-img .file-icon {
background: linear-gradient(145deg, #3dca96, #0a9f6d);
}
.d-attach-card.ft-xls .file-icon {
background: linear-gradient(145deg, #4fbf8a, #189b65);
}
.d-attach-card.ft-doc .file-icon {
background: linear-gradient(145deg, #6ea8ff, #3b82f6);
}
.d-attach-card.ft-other .file-icon {
background: linear-gradient(145deg, #e4bc6a, #d6a343);
}
.d-attach-card .file-icon .ext {
font-size: 10px;
letter-spacing: 0.04em;
line-height: 1;
text-transform: uppercase;
}
.d-attach-card .file-icon .van-icon {
font-size: 16px;
opacity: 0.95;
}
.d-attach-card .file-body {
flex: 1;
min-width: 0;
}
.d-attach-card .file-name {
font-size: 13px;
font-weight: 650;
color: #173128;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.d-attach-card .file-meta {
margin-top: 4px;
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 11px;
color: #8aa198;
}
.d-attach-card .file-ops {
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.d-attach-card .file-ops button {
border: none;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: transform .12s;
}
.d-attach-card .file-ops button:active {
transform: scale(0.94);
}
.d-attach-card .file-ops .btn-preview {
background: var(--brand-green-soft);
color: var(--brand-green-dark);
}
.d-attach-card .file-ops .btn-download {
background: #f0f5f3;
color: #4d635a;
}
.d-attach-card .file-ops .icon-download {
width: 14px;
height: 14px;
display: block;
}
.detail-footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 10px;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
border-top: 1px solid var(--border);
backdrop-filter: blur(10px);
z-index: 4;
}
.detail-footer .van-button {
flex: 1;
height: 44px;
border-radius: 999px;
}
.detail-footer .van-button--primary {
background: linear-gradient(135deg, #12b37a, #087f57);
border: none;
box-shadow: 0 6px 16px rgba(8, 127, 87, 0.28);
}
/* ========== 表单 ========== */
.form-page {
padding-bottom: 88px;
}
.form-section-title {
margin: 14px 16px 8px;
font-size: 13px;
font-weight: 600;
color: var(--text-muted);
}
.form-tip {
margin: 8px 16px 0;
font-size: 11px;
color: var(--text-muted);
line-height: 1.45;
}
.form-footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 10px;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
border-top: 1px solid var(--border);
backdrop-filter: blur(8px);
z-index: 3;
}
.form-footer .van-button {
flex: 1;
height: 44px;
}
/* ========== 组件展示 ========== */
.comps-page {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background:
radial-gradient(400px 220px at 100% 0%, rgba(214, 163, 67, 0.14), transparent 55%),
radial-gradient(360px 200px at 0% 20%, rgba(10, 159, 109, 0.1), transparent 50%),
linear-gradient(180deg, #e8f5ef 0%, #f4f7f6 40%, #f7f9f8 100%);
}
.comps-page .screen-scroll {
background: transparent;
}
.comps-hero {
background:
radial-gradient(220px 140px at 90% 15%, rgba(255, 255, 255, 0.18), transparent 60%),
linear-gradient(155deg, #066647 0%, #0a9f6d 48%, #12b37a 100%);
padding: 44px 14px 22px;
border-radius: 0 0 28px 28px;
color: #fff;
position: relative;
overflow: hidden;
}
.comps-hero::after {
content: "";
position: absolute;
width: 160px;
height: 160px;
right: -40px;
bottom: -60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.comps-hero .van-nav-bar {
background: transparent;
margin: 0 -6px;
}
.comps-hero .van-hairline--bottom::after {
display: none;
}
.comps-hero-copy {
position: relative;
z-index: 1;
margin: 4px 2px 0;
}
.comps-hero-copy .eyebrow {
font-size: 11px;
letter-spacing: 0.08em;
opacity: 0.82;
margin-bottom: 4px;
}
.comps-hero-copy h2 {
margin: 0;
font-size: 22px;
font-weight: 750;
}
.comps-hero-copy .subline {
margin-top: 6px;
font-size: 12px;
opacity: 0.9;
line-height: 1.45;
}
.comp-section {
padding: 14px 12px 28px;
}
.comp-block {
position: relative;
background: #fff;
border: 1px solid rgba(220, 235, 228, 0.95);
border-radius: 18px;
padding: 0;
margin-bottom: 12px;
box-shadow: 0 8px 22px rgba(20, 55, 40, 0.05);
overflow: hidden;
}
.comp-block .comp-hd {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: linear-gradient(135deg, #f3faf6 0%, #ffffff 70%);
border-bottom: 1px solid #eef4f1;
}
.comp-block .comp-hd .hd-icon {
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
box-shadow: 0 4px 10px rgba(10, 159, 109, 0.22);
flex-shrink: 0;
}
.comp-block .comp-hd .hd-icon.tone-amber {
background: linear-gradient(145deg, #ecc06a, #d6a343);
box-shadow: 0 4px 10px rgba(214, 163, 67, 0.25);
}
.comp-block .comp-hd .hd-icon.tone-rose {
background: linear-gradient(145deg, #f07171, #d14343);
box-shadow: 0 4px 10px rgba(209, 67, 67, 0.22);
}
.comp-block .comp-hd .hd-icon.tone-slate {
background: linear-gradient(145deg, #8aa198, #5d746a);
box-shadow: 0 4px 10px rgba(93, 116, 106, 0.2);
}
.comp-block .comp-hd h4 {
margin: 0;
font-size: 14px;
font-weight: 750;
color: #173128;
line-height: 1.2;
}
.comp-block .comp-hd .hd-sub {
margin-top: 2px;
font-size: 11px;
color: #8aa198;
}
.comp-block .comp-bd {
padding: 14px;
}
.comp-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
.comp-row:last-child { margin-bottom: 0; }
.label-muted {
font-size: 11px;
color: #8aa198;
width: 100%;
margin-bottom: 2px;
}
.comps-page .van-button--primary {
background: linear-gradient(135deg, #12b37a, #087f57);
border: none;
box-shadow: 0 4px 12px rgba(8, 127, 87, 0.22);
}
.comps-page .van-button--primary.van-button--plain {
background: #fff;
box-shadow: none;
color: #0a9f6d;
border-color: #0a9f6d;
}
.comps-page .van-button--round {
border-radius: 999px;
}
.tag-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.demo-tag {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 650;
}
.demo-tag i {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.demo-tag.t-warn {
background: #fff3e0;
color: #a67b20;
}
.demo-tag.t-primary {
background: #0a9f6d;
color: #fff;
}
.demo-tag.t-success {
background: rgba(24, 155, 101, 0.12);
color: #147a50;
}
.demo-tag.t-danger {
background: #fdecec;
color: #d14343;
}
.demo-tag.t-muted {
background: #eef2f1;
color: #6b7b74;
}
.comp-cell-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.comp-cell-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 12px;
border-radius: 14px;
background: linear-gradient(160deg, #f7fbf9 0%, #ffffff 55%);
border: 1px solid #e4eee9;
box-shadow: 0 4px 12px rgba(20, 55, 40, 0.04);
cursor: pointer;
text-align: left;
width: 100%;
transition: transform .15s, border-color .15s;
}
.comp-cell-item:active {
transform: scale(0.99);
border-color: #b7e5d0;
}
.comp-cell-item .ci-icon {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #fff;
flex-shrink: 0;
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
box-shadow: 0 4px 10px rgba(10, 159, 109, 0.2);
}
.comp-cell-item .ci-icon.tone-amber {
background: linear-gradient(145deg, #ecc06a, #d6a343);
box-shadow: 0 4px 10px rgba(214, 163, 67, 0.22);
}
.comp-cell-item .ci-icon.tone-slate {
background: linear-gradient(145deg, #8aa198, #5d746a);
box-shadow: 0 4px 10px rgba(93, 116, 106, 0.18);
}
.comp-cell-item .ci-body {
flex: 1;
min-width: 0;
}
.comp-cell-item .ci-title {
font-size: 14px;
font-weight: 700;
color: #173128;
line-height: 1.3;
}
.comp-cell-item .ci-label {
margin-top: 2px;
font-size: 11px;
color: #8aa198;
line-height: 1.4;
}
.comp-cell-item .ci-right {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
color: #8aa198;
font-size: 13px;
font-weight: 550;
}
.comp-cell-item .ci-right .val {
color: #4d635a;
}
.comp-feedback-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.comp-feedback-grid .fb-btn {
border: 1px solid #e4eee9;
background: linear-gradient(160deg, #f7fbf9, #fff);
border-radius: 14px;
padding: 14px 8px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
color: #173128;
font-size: 12px;
font-weight: 650;
transition: transform .15s;
}
.comp-feedback-grid .fb-btn:active {
transform: scale(0.97);
}
.comp-feedback-grid .fb-btn .fb-icon {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #fff;
}
.comp-feedback-grid .fb-btn.fb-toast .fb-icon {
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
}
.comp-feedback-grid .fb-btn.fb-dialog .fb-icon {
background: linear-gradient(145deg, #ecc06a, #d6a343);
}
.comp-feedback-grid .fb-btn.fb-sheet .fb-icon {
background: linear-gradient(145deg, #8aa198, #5d746a);
}
.comp-steps-wrap {
padding: 8px 4px 0;
border-radius: 12px;
background: #f7faf9;
}
.comp-seg {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 4px;
border-radius: 14px;
background: #eef6f2;
margin-bottom: 12px;
}
.comp-seg button {
border: none;
background: transparent;
border-radius: 11px;
padding: 8px 4px;
font-size: 12px;
color: #6b7b74;
cursor: pointer;
}
.comp-seg button.active {
background: #fff;
color: #087f57;
font-weight: 700;
box-shadow: 0 4px 12px rgba(8, 127, 87, 0.12);
}
.comp-notice-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.comp-notice-bar {
border-radius: 14px;
overflow: hidden;
border: 1px solid #c7e6d6;
box-shadow: 0 4px 12px rgba(10, 159, 109, 0.08);
min-height: 44px;
}
.comp-empty-box {
border-radius: 16px;
background: linear-gradient(165deg, #eef9f3 0%, #ffffff 55%);
border: 1.5px dashed #b7e5d0;
padding: 28px 16px 24px;
min-height: 220px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8);
}
.comp-empty-box .van-empty {
padding: 0;
}
.comp-empty-box .van-empty__description {
margin-top: 12px;
color: #5d746a;
font-size: 13px;
}
.comp-empty-box .empty-tip {
margin-top: 4px;
font-size: 11px;
color: #8aa198;
text-align: center;
line-height: 1.45;
}
.comp-loading-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 28px 16px;
min-height: 120px;
border-radius: 16px;
background: linear-gradient(165deg, #f3faf6 0%, #ffffff 60%);
border: 1px solid #e4eee9;
color: #087f57;
font-size: 13px;
font-weight: 650;
box-shadow: 0 4px 12px rgba(20, 55, 40, 0.04);
}
.comp-loading-box .load-sub {
font-size: 11px;
font-weight: 500;
color: #8aa198;
}
.comps-page .comp-attach-list {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.comps-page .comp-upload-tile {
width: 100%;
border: 1.5px dashed #9ad9be;
border-radius: 16px;
background:
radial-gradient(120px 80px at 12% 30%, rgba(46, 207, 143, 0.16), transparent 70%),
linear-gradient(145deg, #e9f9f2 0%, #ffffff 55%);
padding: 16px 14px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
text-align: left;
transition: transform .15s, border-color .15s, box-shadow .15s;
box-shadow: 0 6px 16px rgba(10, 159, 109, 0.08);
}
.comps-page .comp-upload-tile:active {
transform: scale(0.99);
border-color: #0a9f6d;
box-shadow: 0 4px 12px rgba(10, 159, 109, 0.12);
}
.comps-page .comp-upload-tile .up-icon {
width: 48px;
height: 48px;
border-radius: 14px;
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
box-shadow: 0 6px 14px rgba(10, 159, 109, 0.28);
flex-shrink: 0;
}
.comps-page .comp-upload-tile .up-text {
flex: 1;
min-width: 0;
}
.comps-page .comp-upload-tile .up-title {
display: block;
font-size: 14px;
font-weight: 750;
color: #087f57;
}
.comps-page .comp-upload-tile .up-sub {
display: block;
margin-top: 3px;
font-size: 11px;
color: #6b7b74;
line-height: 1.45;
}
.comps-page .comp-file-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 14px;
background: #fff;
border: 1px solid #e4eee9;
box-shadow: 0 4px 14px rgba(20, 55, 40, 0.05);
position: relative;
overflow: hidden;
}
.comps-page .comp-file-card::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: #3b82f6;
}
.comps-page .comp-file-card .file-icon {
width: 46px;
height: 46px;
border-radius: 13px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
flex-shrink: 0;
color: #fff;
font-weight: 800;
background: linear-gradient(145deg, #6ea8ff, #3b82f6);
}
.comps-page .comp-file-card .file-icon .ext {
font-size: 10px;
letter-spacing: 0.04em;
line-height: 1;
text-transform: uppercase;
}
.comps-page .comp-file-card .file-icon .van-icon {
font-size: 16px;
}
.comps-page .comp-file-card .file-body {
flex: 1;
min-width: 0;
}
.comps-page .comp-file-card .file-name {
font-size: 13px;
font-weight: 650;
color: #173128;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.comps-page .comp-file-card .file-meta {
margin-top: 4px;
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 11px;
color: #8aa198;
}
.comps-page .comp-file-card .file-ops {
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.comps-page .comp-file-card .file-ops button {
border: none;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.comps-page .comp-file-card .file-ops .btn-preview {
background: var(--brand-green-soft);
color: var(--brand-green-dark);
}
.comps-page .comp-file-card .file-ops .btn-del {
background: #fdecec;
color: #d14343;
}
/* 表单页等仍可能用到的旧附件样式 */
.upload-card {
width: 88px;
height: 88px;
border: 1.5px dashed #9ad9be;
border-radius: 14px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: #087f57;
background: linear-gradient(145deg, #e9f9f2, #fff);
font-size: 11px;
font-weight: 650;
}
.attach-card {
width: 140px;
min-height: 88px;
border: 1px solid #e4eee9;
border-radius: 14px;
padding: 10px 12px;
background: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 11px;
}
.attach-card .name {
color: #173128;
font-weight: 650;
word-break: break-all;
line-height: 1.35;
}
.attach-card .ops {
display: flex;
gap: 10px;
color: #0a9f6d;
font-weight: 650;
}
.demo-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
background: var(--brand-green-soft);
color: var(--brand-green-dark);
}
@media (max-width: 900px) {
#app {
grid-template-columns: 1fr;
}
.doc-panel {
position: static;
}
.phone {
width: min(390px, 100%);
height: min(844px, calc(100vh - 120px));
min-height: 640px;
}
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}
</style>
</head>
<body>
<div id="app">
<aside class="doc-panel">
<h1>移动端 UI Demo</h1>
<p class="sub">
Vant 4 + 三墙 Token(#0a9f6d)。首页为权限菜单卡片;列表 / 新增编辑 / 组件风格可在下方切换预览。
</p>
<div class="section-title">页面预览</div>
<div class="screen-nav">
<button
v-for="s in screens"
:key="s.key"
:class="{ active: screen === s.key }"
type="button"
@click="goScreen(s.key)"
>{{ s.label }}</button>
</div>
<div class="section-title">设计 Token</div>
<div class="token-row">
<span class="token-chip"><i style="background:#0a9f6d"></i>#0a9f6d</span>
<span class="token-chip"><i style="background:#087f57"></i>#087f57</span>
<span class="token-chip"><i style="background:#e9f9f2"></i>#e9f9f2</span>
<span class="token-chip"><i style="background:#d6a343"></i>#d6a343</span>
<span class="token-chip"><i style="background:#f5f8f7"></i>#f5f8f7</span>
</div>
<div class="section-title">权限说明</div>
<ul class="notes">
<li>菜单来自「菜单管理 → 移动端菜单」树</li>
<li>仅展示当前登录人角色/职位拥有的菜单权限</li>
<li>卡片:上 Icon、下标题;建议 4 列宫格</li>
<li>本 Demo 用右侧开关模拟「无某菜单权限」</li>
</ul>
<div class="section-title" v-if="screen === 'home'">权限模拟</div>
<div v-if="screen === 'home'" style="display:flex;flex-direction:column;gap:8px;">
<van-checkbox
v-for="m in allMenus"
:key="m.id"
v-model="grantedMap[m.id]"
shape="square"
icon-size="16px"
>{{ m.title }}</van-checkbox>
</div>
</aside>
<div class="phone-stage">
<div class="phone">
<div class="phone-screen">
<!-- ===== 首页 ===== -->
<template v-if="screen === 'home'">
<div class="screen-scroll">
<header class="home-header">
<div class="brand">中科兴皖 · 移动工作台</div>
<h2>你好,{{ userName }}</h2>
<div class="meta">
<span>{{ roleName }}</span>
<span class="dot"></span>
<span>{{ deptName }}</span>
</div>
</header>
<div class="home-body">
<div class="perm-banner">
菜单卡片按角色权限动态渲染;无权限的功能入口不会出现在宫格中。
</div>
<section class="panel">
<div class="panel-hd">
<h3>功能菜单</h3>
<span class="hint">共 {{ visibleMenus.length }} 项</span>
</div>
<div v-if="visibleMenus.length" class="menu-grid">
<button
v-for="m in visibleMenus"
:key="m.id"
type="button"
class="menu-card"
@click="onMenuTap(m)"
>
<div class="menu-icon" :class="m.tone">
<van-icon :name="m.icon" />
</div>
<span class="menu-title">{{ m.title }}</span>
</button>
</div>
<div v-else class="list-empty-pad">
<van-empty description="暂无可用菜单权限" image-size="80" />
</div>
</section>
<section class="panel todo-panel">
<div class="panel-hd">
<h3>待办摘要</h3>
<button type="button" class="demo-badge" style="border:none;cursor:pointer;" @click="screen = 'list'">
查看全部
<van-icon name="arrow" style="font-size:11px;" />
</button>
</div>
<div class="todo-stats">
<button type="button" class="todo-stat tone-warn" @click="goListTab(2)">
<span class="stat-icon"><van-icon name="passed" /></span>
<span class="num">{{ todoStats.approve }}</span>
<span class="label">待审批</span>
</button>
<button type="button" class="todo-stat tone-pay" @click="goListTab(2)">
<span class="stat-icon"><van-icon name="balance-pay" /></span>
<span class="num">{{ todoStats.pay }}</span>
<span class="label">待确认缴费</span>
</button>
<button type="button" class="todo-stat tone-danger" @click="goListTab(0)">
<span class="stat-icon"><van-icon name="warning-o" /></span>
<span class="num">{{ todoStats.alert }}</span>
<span class="label">异常提醒</span>
</button>
</div>
<div class="todo-list">
<button
v-for="t in todoItems"
:key="t.id"
type="button"
class="todo-item"
@click="screen = 'list'"
>
<div class="ti-icon" :class="t.iconClass">
<van-icon :name="t.icon" />
</div>
<div class="ti-body">
<div class="ti-title">{{ t.title }}</div>
<div class="ti-desc">{{ t.desc }}</div>
</div>
<div class="ti-right">
<van-tag :type="t.tagType" plain size="medium">{{ t.tag }}</van-tag>
<span class="ti-time">{{ t.time }}</span>
</div>
</button>
</div>
</section>
</div>
</div>
</template>
<!-- ===== 列表 ===== -->
<template v-else-if="screen === 'list'">
<div class="list-page">
<div class="screen-scroll">
<header class="list-hero">
<van-nav-bar left-arrow @click-left="screen = 'home'">
<template #title>
<span style="font-weight:600;">业务列表</span>
</template>
<template #right>
<van-icon name="plus" size="20" @click="openForm('create')" />
</template>
</van-nav-bar>
<div class="list-hero-copy">
<div class="eyebrow">WORKSPACE</div>
<h2>今日待办一览</h2>
<div class="subline">
<span>当前展示</span>
<em>{{ filteredList.length }} 条</em>
<span v-if="keyword">· 已筛选</span>
</div>
</div>
<div class="list-search">
<van-search
v-model="keyword"
shape="round"
background="transparent"
placeholder="搜索单号 / 名称 / 负责人"
/>
</div>
</header>
<div class="list-filter-dock">
<div class="seg-tabs">
<button
v-for="(tab, idx) in listTabs"
:key="tab.key"
type="button"
class="seg-tab"
:class="{ active: listTab === idx }"
@click="listTab = idx"
>
<span class="lab">{{ tab.label }}</span>
<span class="count">{{ tabCount(idx) }}</span>
</button>
</div>
</div>
<div class="list-wrap">
<article
v-for="item in filteredList"
:key="item.id"
class="biz-card"
:class="'status-' + item.tagType"
>
<div class="card-inner">
<div class="row-top">
<div class="type-icon">
<van-icon :name="item.icon" />
</div>
<div class="title-wrap">
<div class="type-chip">{{ item.typeLabel }}</div>
<div class="title">{{ item.title }}</div>
</div>
<span class="status-pill">{{ item.status }}</span>
</div>
<div class="body-row">
<p class="desc-box">{{ item.desc }}</p>
<div class="side-metric">
<template v-if="item.amount">
<span class="amt-label">金额</span>
<span class="amt">¥{{ item.amount }}</span>
</template>
<template v-else>
<span class="amt-label">进度</span>
<span class="amt" style="font-size:13px;">{{ item.progress || '—' }}</span>
</template>
<span class="time">{{ item.time }}</span>
</div>
</div>
<div class="meta-row">
<span class="mi code-pill">{{ item.code }}</span>
<span class="mi"><van-icon name="manager-o" />{{ item.owner }}</span>
</div>
<div class="footer">
<span class="left-hint"><i></i>{{ item.hint || '点击操作处理' }}</span>
<div class="ops">
<van-button size="small" plain hairline type="default" @click="openDetail(item)">查看</van-button>
<van-button size="small" type="primary" @click="openForm('edit', item)">处理</van-button>
</div>
</div>
</div>
</article>
<div v-if="!filteredList.length" class="list-empty-pad">
<van-empty description="暂无数据" />
</div>
<div v-else class="list-end"><span>已经到底了</span></div>
</div>
</div>
</div>
</template>
<!-- ===== 详情 ===== -->
<template v-else-if="screen === 'detail'">
<div class="detail-page" v-if="detail">
<div class="screen-scroll">
<header class="detail-hero" :class="'status-' + detail.tagType">
<van-nav-bar left-arrow @click-left="screen = 'list'">
<template #title>
<span style="font-weight:600;">业务详情</span>
</template>
<template #right>
<van-icon name="ellipsis" size="20" @click="showAction = true" />
</template>
</van-nav-bar>
<div class="detail-hero-body">
<div class="type-line">
<span class="type-chip">{{ detail.typeLabel }}</span>
<span class="status-chip">{{ detail.status }}</span>
</div>
<h2>{{ detail.title }}</h2>
<div class="code">{{ detail.code }}</div>
</div>
</header>
<div class="detail-metric-dock">
<div class="detail-metric">
<span class="lab">{{ detail.amount ? '金额' : '进度' }}</span>
<span class="val" :class="{ money: !!detail.amount }">
{{ detail.amount ? ('¥' + detail.amount) : (detail.progress || '—') }}
</span>
</div>
<div class="detail-metric">
<span class="lab">负责人</span>
<span class="val">{{ detail.owner }}</span>
</div>
<div class="detail-metric">
<span class="lab">更新时间</span>
<span class="val">{{ detail.time }}</span>
</div>
</div>
<div class="detail-body">
<section class="detail-section">
<div class="sec-hd"><h3>基础信息</h3></div>
<div class="kv-list">
<div class="kv-row"><span class="k">业务类型</span><span class="v">{{ detail.typeLabel }}</span></div>
<div class="kv-row"><span class="k">所属部门</span><span class="v">{{ detail.dept || deptName }}</span></div>
<div class="kv-row"><span class="k">客户/对象</span><span class="v">{{ detail.customer || '—' }}</span></div>
<div class="kv-row"><span class="k">创建时间</span><span class="v">{{ detail.createdAt || '—' }}</span></div>
</div>
<p class="detail-note" style="margin-top:12px;">{{ detail.desc }}</p>
</section>
<section class="detail-section">
<div class="sec-hd"><h3>业务进度</h3></div>
<van-steps :active="detail.stepActive" active-color="#0a9f6d" inactive-color="#dcebe4">
<van-step v-for="s in detail.steps" :key="s">{{ s }}</van-step>
</van-steps>
</section>
<section class="detail-section" v-if="detail.payments && detail.payments.length">
<div class="sec-hd">
<h3>缴费明细</h3>
<button type="button" class="more" @click="toast('查看全部缴费')">全部</button>
</div>
<div
v-for="p in detail.payments"
:key="p.id"
class="pay-row"
>
<div class="left">
<div class="name">{{ p.name }}</div>
<div class="sub">{{ p.sub }}</div>
</div>
<div class="right">
<div class="amt">¥{{ p.amount }}</div>
<van-tag :type="p.tagType" plain size="medium" style="margin-top:4px;">{{ p.status }}</van-tag>
</div>
</div>
</section>
<section class="detail-section">
<div class="sec-hd">
<h3>附件</h3>
<span style="font-size:11px;color:#8aa198;">{{ detail.attachments.length }} 个文件</span>
</div>
<div class="d-attach-list">
<button type="button" class="d-attach-upload" @click="toast('上传附件')">
<span class="up-icon"><van-icon name="plus" /></span>
<span class="up-text">
<span class="up-title">添加附件</span>
<span class="up-sub">支持 PDF / 图片 / Office,可多选上传</span>
</span>
<van-icon name="arrow" color="#0a9f6d" />
</button>
<div
v-for="a in detail.attachments"
:key="a.name"
class="d-attach-card"
:class="fileTypeClass(a.name)"
>
<div class="file-icon">
<van-icon :name="fileTypeIcon(a.name)" />
<span class="ext">{{ fileExt(a.name) }}</span>
</div>
<div class="file-body">
<div class="file-name">{{ a.name }}</div>
<div class="file-meta">
<span>{{ a.size || '—' }}</span>
<span>{{ a.time || '—' }}</span>
</div>
</div>
<div class="file-ops">
<button type="button" class="btn-preview" title="预览" @click="toast('预览 ' + a.name)">
<van-icon name="eye-o" />
</button>
<button type="button" class="btn-download" title="下载" @click="toast('下载 ' + a.name)">
<svg class="icon-download" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M12 3a1 1 0 0 1 1 1v8.59l2.3-2.3a1 1 0 1 1 1.4 1.42l-4 4a1 1 0 0 1-1.4 0l-4-4a1 1 0 1 1 1.4-1.42L11 12.59V4a1 1 0 0 1 1-1Z"/>
<path fill="currentColor" d="M5 15a1 1 0 0 1 1 1v2h12v-2a1 1 0 1 1 2 0v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2a1 1 0 0 1 1-1Z"/>
</svg>
</button>
</div>
</div>
</div>
</section>
<section class="detail-section">
<div class="sec-hd">
<h3>流转记录</h3>
<span style="font-size:11px;color:#8aa198;">{{ detail.logs.length }} 步</span>
</div>
<div class="flow-list">
<div
v-for="(log, idx) in detail.logs"
:key="idx"
class="flow-item"
:class="log.tone"
>
<div class="flow-rail">
<div class="flow-dot">
<van-icon :name="log.icon || 'checked'" />
</div>
</div>
<div class="flow-card">
<div class="flow-top">
<div class="flow-title">{{ log.title }}</div>
<span class="flow-badge">{{ log.badge || (log.tone === 'warn' ? '进行中' : '已完成') }}</span>
</div>
<div class="flow-desc">{{ log.desc }}</div>
<div class="flow-meta">
<span class="chip" v-if="log.actor">
<van-icon name="manager-o" />{{ log.actor }}
</span>
<span class="chip">
<van-icon name="clock-o" />{{ log.time }}
</span>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="detail-footer">
<van-button plain hairline type="default" @click="screen = 'list'">返回</van-button>
<van-button type="primary" @click="openForm('edit', detail)">编辑处理</van-button>
</div>
</div>
</template>
<!-- ===== 新增 / 编辑 ===== -->
<template v-else-if="screen === 'form'">
<div class="page-nav">
<van-nav-bar
:title="formMode === 'create' ? '新增业务' : '编辑业务'"
left-arrow
@click-left="screen = 'list'"
/>
</div>
<div class="screen-scroll form-page">
<div class="form-section-title">基础信息</div>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="form.name"
name="name"
label="名称"
placeholder="请输入名称"
:rules="[{ required: true, message: '请填写名称' }]"
/>
<van-field
v-model="form.code"
name="code"
label="业务单号"
placeholder="系统自动生成或手工录入"
/>
<van-field
v-model="form.typeLabel"
is-link
readonly
name="type"
label="类型"
placeholder="请选择类型"
@click="showTypePicker = true"
/>
<van-field name="urgent" label="加急">
<template #input>
<van-switch v-model="form.urgent" size="20px" active-color="#0a9f6d" />
</template>
</van-field>
</van-cell-group>
<div class="form-section-title">业务详情</div>
<van-cell-group inset>
<van-field
v-model="form.owner"
name="owner"
label="负责人"
placeholder="请输入负责人"
/>
<van-field
v-model="form.date"
is-link
readonly
name="date"
label="业务日期"
placeholder="选择日期"
@click="showDate = true"
/>
<van-field
v-model="form.amount"
type="number"
name="amount"
label="金额"
placeholder="0.00"
>
<template #right-icon>元</template>
</van-field>
<van-field
v-model="form.remark"
rows="3"
autosize
type="textarea"
maxlength="200"
name="remark"
label="备注"
placeholder="选填,最多 200 字"
show-word-limit
/>
</van-cell-group>
<div class="form-section-title">附件</div>
<van-cell-group inset>
<van-cell title="协议附件" label="支持多附件,逗号分隔存储">
<template #value>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<div class="upload-card">
<van-icon name="plus" size="20" />
<span>上传</span>
</div>
</div>
</template>
</van-cell>
<van-cell>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<div class="attach-card">
<div class="name">协议扫描件.pdf</div>
<div class="ops">
<span>预览</span>
<span>删除</span>
</div>
</div>
</div>
</van-cell>
</van-cell-group>
<p class="form-tip">表单:label 在左、控件在右(Vant Field 默认);必填项校验失败时 Toast / 字段红字提示。底栏固定「取消 / 保存」。</p>
<div class="form-footer">
<van-button plain hairline type="default" @click="screen = 'list'">取消</van-button>
<van-button round block type="primary" native-type="submit">保存</van-button>
</div>
</van-form>
</div>
<van-popup v-model:show="showTypePicker" position="bottom" round>
<van-picker
:columns="typeColumns"
@confirm="onTypeConfirm"
@cancel="showTypePicker = false"
/>
</van-popup>
<van-calendar
v-model:show="showDate"
@confirm="onDateConfirm"
color="#0a9f6d"
/>
</template>
<!-- ===== 组件风格 ===== -->
<template v-else>
<div class="comps-page">
<div class="screen-scroll">
<header class="comps-hero">
<van-nav-bar left-arrow @click-left="screen = 'home'">
<template #title>
<span style="font-weight:600;">组件风格</span>
</template>
</van-nav-bar>
<div class="comps-hero-copy">
<div class="eyebrow">DESIGN SYSTEM</div>
<h2>组件合集</h2>
<div class="subline">按钮 · 状态 · 反馈 · 步骤 · 附件等基础样式规范</div>
</div>
</header>
<div class="comp-section">
<section class="comp-block">
<div class="comp-hd">
<div class="hd-icon"><van-icon name="font-o" /></div>
<div>
<h4>按钮 Button</h4>
<div class="hd-sub">每组仅一个实心主色行动</div>
</div>
</div>
<div class="comp-bd">
<div class="comp-row">
<span class="label-muted">常用尺寸</span>
<van-button type="primary" size="small" round>主按钮</van-button>
<van-button type="primary" size="small" plain hairline round>描边主色</van-button>
<van-button type="default" size="small" plain hairline round>次要</van-button>
<van-button type="danger" size="small" plain hairline round>危险</van-button>
</div>
<div class="comp-row">
<van-button type="primary" block round style="height:44px;">通栏主按钮(44px)</van-button>
</div>
</div>
</section>
<section class="comp-block status-demo">
<div class="comp-hd">
<div class="hd-icon tone-amber"><van-icon name="label-o" /></div>
<div>
<h4>状态 Tag</h4>
<div class="hd-sub">语义色:待处理 / 进行中 / 完成 / 异常</div>
</div>
</div>
<div class="comp-bd">
<div class="tag-grid">
<span class="demo-tag t-warn"><i></i>待处理</span>
<span class="demo-tag t-primary"><i></i>进行中</span>
<span class="demo-tag t-success"><i></i>已完成</span>
<span class="demo-tag t-danger"><i></i>异常</span>
<span class="demo-tag t-muted"><i></i>已作废</span>
</div>
<div class="comp-row" style="margin-top:12px;">
<span class="label-muted">Vant Tag 兼容</span>
<van-tag type="warning" plain round>待处理</van-tag>
<van-tag type="primary" round>进行中</van-tag>
<van-tag type="success" plain round>已完成</van-tag>
<van-tag type="danger" plain round>异常</van-tag>
</div>
</div>
</section>
<section class="comp-block">
<div class="comp-hd">
<div class="hd-icon tone-slate"><van-icon name="orders-o" /></div>
<div>
<h4>单元格 Cell</h4>
<div class="hd-sub">分组列表与跳转行</div>
</div>
</div>
<div class="comp-bd">
<div class="comp-cell-list">
<button type="button" class="comp-cell-item" @click="toast('查看内容')">
<span class="ci-icon"><van-icon name="orders-o" /></span>
<span class="ci-body">
<span class="ci-title">标题</span>
<span class="ci-label">基础信息展示</span>
</span>
<span class="ci-right"><span class="val">内容</span></span>
</button>
<button type="button" class="comp-cell-item" @click="toast('跳转详情')">
<span class="ci-icon tone-amber"><van-icon name="guide-o" /></span>
<span class="ci-body">
<span class="ci-title">可跳转</span>
<span class="ci-label">点击进入下一页</span>
</span>
<span class="ci-right">
<span class="val">详情</span>
<van-icon name="arrow" />
</span>
</button>
<button type="button" class="comp-cell-item" @click="toast('状态:启用')">
<span class="ci-icon tone-slate"><van-icon name="manager-o" /></span>
<span class="ci-body">
<span class="ci-title">带标签</span>
<span class="ci-label">辅助说明文字</span>
</span>
<span class="ci-right">
<span class="demo-tag t-success" style="padding:3px 8px;font-size:11px;"><i></i>启用</span>
</span>
</button>
</div>
</div>
</section>
<section class="comp-block">
<div class="comp-hd">
<div class="hd-icon"><van-icon name="volume-o" /></div>
<div>
<h4>通知 / 空态 / 加载</h4>
<div class="hd-sub">提示条与占位反馈</div>
</div>
</div>
<div class="comp-bd">
<div class="comp-notice-stack">
<div class="comp-notice-bar">
<van-notice-bar
left-icon="volume-o"
text="移动端菜单在「菜单管理 → 移动端菜单」下配置,按角色授权后展示。"
background="#e9f9f2"
color="#087f57"
/>
</div>
<div class="comp-empty-box">
<van-empty description="暂无相关数据" image-size="110" />
<div class="empty-tip">可下拉刷新,或点击上方按钮新增一条记录</div>
</div>
<div class="comp-loading-box">
<van-loading type="spinner" color="#0a9f6d" size="28px" />
<div>内容加载中…</div>
<div class="load-sub">正在拉取最新业务数据</div>
</div>
</div>
</div>
</section>
<section class="comp-block">
<div class="comp-hd">
<div class="hd-icon tone-amber"><van-icon name="comment-o" /></div>
<div>
<h4>反馈 Feedback</h4>
<div class="hd-sub">Toast / Dialog / ActionSheet</div>
</div>
</div>
<div class="comp-bd">
<div class="comp-feedback-grid">
<button type="button" class="fb-btn fb-toast" @click="toast('操作成功')">
<span class="fb-icon"><van-icon name="comment-o" /></span>
Toast
</button>
<button type="button" class="fb-btn fb-dialog" @click="showDialog">
<span class="fb-icon"><van-icon name="info-o" /></span>
Dialog
</button>
<button type="button" class="fb-btn fb-sheet" @click="showAction = true">
<span class="fb-icon"><van-icon name="wap-nav" /></span>
Sheet
</button>
</div>
</div>
</section>
<section class="comp-block">
<div class="comp-hd">
<div class="hd-icon"><van-icon name="cluster-o" /></div>
<div>
<h4>分段 / 步骤</h4>
<div class="hd-sub">筛选分段与流程进度</div>
</div>
</div>
<div class="comp-bd">
<div class="comp-seg">
<button type="button" :class="{ active: compTab === 0 }" @click="compTab = 0">资料</button>
<button type="button" :class="{ active: compTab === 1 }" @click="compTab = 1">进度</button>
<button type="button" :class="{ active: compTab === 2 }" @click="compTab = 2">附件</button>
</div>
<div class="comp-steps-wrap">
<van-steps :active="compTab === 2 ? 2 : 1" active-color="#0a9f6d" inactive-color="#dcebe4">
<van-step>提交</van-step>
<van-step>审批中</van-step>
<van-step>完成</van-step>
</van-steps>
</div>
</div>
</section>
<section class="comp-block">
<div class="comp-hd">
<div class="hd-icon tone-rose"><van-icon name="description" /></div>
<div>
<h4>附件卡片</h4>
<div class="hd-sub">上传入口与文件摘要卡</div>
</div>
</div>
<div class="comp-bd">
<div class="comp-attach-list">
<button type="button" class="comp-upload-tile" @click="toast('上传附件')">
<span class="up-icon"><van-icon name="plus" /></span>
<span class="up-text">
<span class="up-title">添加附件</span>
<span class="up-sub">支持 PDF / 图片 / Office,点按即可上传</span>
</span>
<van-icon name="arrow" color="#0a9f6d" />
</button>
<div class="comp-file-card">
<div class="file-icon">
<van-icon name="records" />
<span class="ext">DOCX</span>
</div>
<div class="file-body">
<div class="file-name">质检报告.docx</div>
<div class="file-meta">
<span>1.2 MB</span>
<span>08-10 10:20</span>
</div>
</div>
<div class="file-ops">
<button type="button" class="btn-preview" @click="toast('预览')">
<van-icon name="eye-o" />
</button>
<button type="button" class="btn-del" @click="toast('删除')">
<van-icon name="delete-o" />
</button>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<van-action-sheet
v-model:show="showAction"
:actions="actions"
cancel-text="取消"
close-on-click-action
@select="onActionSelect"
/>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.13/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vant@4.9.21/lib/vant.min.js"></script>
<script>
const { createApp, computed, reactive, ref } = Vue
createApp({
setup() {
const screen = ref('home')
const screens = [
{ key: 'home', label: '1. 首页 · 权限菜单卡片' },
{ key: 'list', label: '2. 列表界面' },
{ key: 'detail', label: '3. 详情界面' },
{ key: 'form', label: '4. 新增 / 编辑表单' },
{ key: 'comps', label: '5. 组件风格合集' }
]
const userName = '张三'
const roleName = '业务专员'
const deptName = '样本运营部'
const allMenus = [
{ id: 'm1', title: '待办审批', icon: 'todo-list-o', tone: '', path: 'list' },
{ id: 'm2', title: '采集协议', icon: 'orders-o', tone: '', path: 'list' },
{ id: 'm3', title: '缴费确认', icon: 'balance-pay', tone: 'tone-yellow', path: 'list' },
{ id: 'm4', title: '样本接收', icon: 'logistics', tone: '', path: 'list' },
{ id: 'm5', title: '质检工单', icon: 'certificate', tone: 'tone-slate', path: 'list' },
{ id: 'm6', title: '生产任务', icon: 'cluster-o', tone: '', path: 'list' },
{ id: 'm7', title: '库位出库', icon: 'send-gift-o', tone: 'tone-yellow', path: 'list' },
{ id: 'm8', title: '我的', icon: 'user-o', tone: 'tone-slate', path: 'comps' }
]
const grantedMap = reactive({
m1: true, m2: true, m3: true, m4: true,
m5: true, m6: false, m7: true, m8: true
})
const visibleMenus = computed(() =>
allMenus.filter((m) => grantedMap[m.id])
)
const todoStats = { approve: 3, pay: 1, alert: 1 }
const todoItems = [
{
id: 't1',
title: '采集协议审批',
desc: '王女士 · 分期首期待复核',
tag: '待审批',
tagType: 'warning',
time: '09:20',
icon: 'todo-list-o',
iconClass: 'i-approve'
},
{
id: 't2',
title: '缴费确认',
desc: 'XY20260810001 · ¥12,800',
tag: '待确认',
tagType: 'warning',
time: '昨天',
icon: 'balance-pay',
iconClass: 'i-pay'
},
{
id: 't3',
title: '质检异常',
desc: 'QC-8821 · 结论待录入',
tag: '异常',
tagType: 'danger',
time: '08-08',
icon: 'warning-o',
iconClass: 'i-alert'
}
]
const keyword = ref('')
const listTab = ref(0)
const listTabs = [
{ key: 'all', label: '全部' },
{ key: 'doing', label: '进行中' },
{ key: 'pending', label: '待处理' },
{ key: 'done', label: '已完成' }
]
const listData = [
{
id: 1, title: '脐带血采集协议-王女士', code: 'XY20260810001',
typeLabel: '采集协议', icon: 'orders-o',
owner: '李四', time: '08-10 09:20', amount: '12,800', progress: '',
desc: '分期缴费 · 待财务确认首期款项',
status: '待处理', tagType: 'warning', hint: '需尽快确认',
customer: '王女士', dept: '样本运营部', createdAt: '2026-08-08 14:20',
stepActive: 1,
steps: ['建档', '缴费确认', '样本采集', '入库'],
payments: [
{ id: 'p1', name: '第 1 期(首期)', sub: '应缴 08-10 · 微信转账', amount: '6,400', status: '待确认', tagType: 'warning' },
{ id: 'p2', name: '第 2 期', sub: '应缴 09-10 · 未到期', amount: '6,400', status: '待缴费', tagType: 'primary' }
],
attachments: [
{ name: '协议扫描件.pdf', size: '2.4 MB', time: '08-08 14:18' },
{ name: '身份证明.jpg', size: '860 KB', time: '08-08 14:19' }
],
logs: [
{ title: '提交审批', desc: '提交采集协议,等待财务确认首期', time: '08-08 14:22', tone: 'done', actor: '李四', icon: 'edit', badge: '已完成' },
{ title: '流转至财务', desc: '系统自动分配至财务确认岗', time: '08-08 14:23', tone: 'done', actor: '系统', icon: 'cluster-o', badge: '已完成' },
{ title: '待确认缴费', desc: '首期 ¥6,400 等待财务核对附件', time: '08-10 09:20', tone: 'warn', actor: '财务岗', icon: 'balance-pay', badge: '进行中' }
]
},
{
id: 2, title: '样本接收单-批次 A12', code: 'YS20260809018',
typeLabel: '样本接收', icon: 'logistics',
owner: '王五', time: '08-09 16:40', amount: '', progress: '在途',
desc: '冷链在途 · 预计今日 18:00 前到达库房',
status: '进行中', tagType: 'primary', hint: '跟踪物流',
customer: '批次 A12', dept: '仓储物流部', createdAt: '2026-08-09 09:10',
stepActive: 2,
steps: ['发运', '在途', '到库签收', '入库'],
payments: [],
attachments: [{ name: '冷链温度记录.pdf', size: '1.1 MB', time: '08-09 11:28' }],
logs: [
{ title: '创建接收单', desc: '创建样本接收单并绑定批次 A12', time: '08-09 09:10', tone: 'done', actor: '王五', icon: 'orders-o', badge: '已完成' },
{ title: '冷链发出', desc: '承运方已取件,全程温控开启', time: '08-09 11:30', tone: 'done', actor: '承运方', icon: 'logistics', badge: '已完成' },
{ title: '运输中', desc: '当前位置:城东中转站', time: '08-09 16:40', tone: 'warn', actor: '物流跟踪', icon: 'location-o', badge: '进行中' }
]
},
{
id: 3, title: '质检工单-QC-8821', code: 'ZJ20260808007',
typeLabel: '质检工单', icon: 'certificate',
owner: '赵六', time: '08-08 11:05', amount: '', progress: '阻塞',
desc: '检测结论待录入,阻塞下游入库流程',
status: '异常', tagType: 'danger', hint: '优先处理',
customer: '样本 S-8821', dept: '质检中心', createdAt: '2026-08-07 16:40',
stepActive: 1,
steps: ['取样', '检测', '结论录入', '关闭'],
payments: [],
attachments: [{ name: '初检原始数据.xlsx', size: '428 KB', time: '08-08 10:48' }],
logs: [
{ title: '工单创建', desc: '系统根据接收单自动生成质检工单', time: '08-07 16:40', tone: 'done', actor: '系统', icon: 'add-o', badge: '已完成' },
{ title: '检测完成', desc: '完成实验操作,待录入结论', time: '08-08 10:50', tone: 'done', actor: '赵六', icon: 'certificate', badge: '已完成' },
{ title: '异常挂起', desc: '结论超时未录入,已阻塞入库', time: '08-08 11:05', tone: 'danger', actor: '质控规则', icon: 'warning-o', badge: '异常' }
]
},
{
id: 4, title: '出库申请-库位 B-03', code: 'CK20260807002',
typeLabel: '库位出库', icon: 'send-gift-o',
owner: '张三', time: '08-07 14:22', amount: '', progress: '完结',
desc: '复核已完成,可归档关闭',
status: '已完成', tagType: 'success', hint: '已完结',
customer: '库位 B-03', dept: '仓储物流部', createdAt: '2026-08-06 10:00',
stepActive: 3,
steps: ['申请', '复核', '出库', '归档'],
payments: [],
attachments: [{ name: '出库单签收.pdf', size: '980 KB', time: '08-07 14:18' }],
logs: [
{ title: '提交申请', desc: '发起出库申请,指定库位 B-03', time: '08-06 10:00', tone: 'done', actor: '张三', icon: 'edit', badge: '已完成' },
{ title: '复核通过', desc: '库管复核通过,允许出库', time: '08-07 11:10', tone: 'done', actor: '库管', icon: 'passed', badge: '已完成' },
{ title: '出库完成', desc: '实物已交接,单据归档', time: '08-07 14:22', tone: 'done', actor: '张三', icon: 'checked', badge: '已完成' }
]
}
]
const detail = ref(null)
function openDetail(item) {
detail.value = item || listData[0]
screen.value = 'detail'
}
function goScreen(key) {
if (key === 'detail' && !detail.value) detail.value = listData[0]
screen.value = key
}
function fileExt(name) {
const i = (name || '').lastIndexOf('.')
return i >= 0 ? name.slice(i + 1).slice(0, 4) : 'FILE'
}
function fileTypeClass(name) {
const ext = fileExt(name).toLowerCase()
if (ext === 'pdf') return 'ft-pdf'
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) return 'ft-img'
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'ft-xls'
if (['doc', 'docx'].includes(ext)) return 'ft-doc'
return 'ft-other'
}
function fileTypeIcon(name) {
const cls = fileTypeClass(name)
if (cls === 'ft-pdf') return 'description'
if (cls === 'ft-img') return 'photo-o'
if (cls === 'ft-xls') return 'bar-chart-o'
if (cls === 'ft-doc') return 'records'
return 'bag-o'
}
function rowsByTab(idx) {
if (idx === 1) return listData.filter((r) => r.status === '进行中')
if (idx === 2) return listData.filter((r) => r.status === '待处理')
if (idx === 3) return listData.filter((r) => r.status === '已完成')
return listData
}
function tabCount(idx) {
return rowsByTab(idx).length
}
const filteredList = computed(() => {
const kw = keyword.value.trim()
let rows = rowsByTab(listTab.value)
if (!kw) return rows
return rows.filter((r) =>
r.title.includes(kw) || r.code.includes(kw) || r.owner.includes(kw)
)
})
function goListTab(idx) {
listTab.value = idx
screen.value = 'list'
}
const formMode = ref('create')
const form = reactive({
name: '',
code: '',
typeLabel: '',
urgent: false,
owner: '',
date: '',
amount: '',
remark: ''
})
const showTypePicker = ref(false)
const showDate = ref(false)
const typeColumns = [
{ text: '采集协议', value: 'protocol' },
{ text: '样本接收', value: 'sample' },
{ text: '质检工单', value: 'qc' },
{ text: '出库申请', value: 'outbound' }
]
function resetForm() {
form.name = ''
form.code = ''
form.typeLabel = ''
form.urgent = false
form.owner = userName
form.date = ''
form.amount = ''
form.remark = ''
}
function openForm(mode, item) {
formMode.value = mode
if (mode === 'edit' && item) {
form.name = item.title
form.code = item.code
form.typeLabel = '采集协议'
form.urgent = item.tagType === 'danger'
form.owner = item.owner
form.date = '2026-08-10'
form.amount = '12800'
form.remark = item.desc
} else {
resetForm()
}
screen.value = 'form'
}
function onTypeConfirm({ selectedOptions }) {
form.typeLabel = selectedOptions[0]?.text || ''
showTypePicker.value = false
}
function onDateConfirm(val) {
const d = val
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
form.date = `${y}-${m}-${day}`
showDate.value = false
}
function onSubmit() {
vant.showToast({ message: formMode.value === 'create' ? '已保存(新增)' : '已保存(编辑)', type: 'success' })
screen.value = 'list'
}
function onMenuTap(m) {
if (m.path === 'list') screen.value = 'list'
else if (m.path === 'comps') screen.value = 'comps'
else vant.showToast(m.title)
}
function toast(msg) {
vant.showToast(msg)
}
function showDialog() {
vant.showConfirmDialog({
title: '确认提交',
message: '提交后将进入审批流程,是否继续?',
confirmButtonText: '确认',
confirmButtonColor: '#0a9f6d'
}).then(() => {
vant.showToast('已确认')
}).catch(() => {})
}
const showAction = ref(false)
const actions = [
{ name: '查看', color: '#1f2a26' },
{ name: '编辑', color: '#0a9f6d' },
{ name: '作废', color: '#d14343' }
]
const compTab = ref(0)
function onActionSelect(action) {
vant.showToast(action.name)
}
return {
screen, screens, goScreen, userName, roleName, deptName,
allMenus, grantedMap, visibleMenus,
todoStats, todoItems, goListTab,
keyword, listTab, listTabs, tabCount, filteredList,
detail, openDetail, fileExt, fileTypeClass, fileTypeIcon,
formMode, form, showTypePicker, showDate, typeColumns,
openForm, onTypeConfirm, onDateConfirm, onSubmit, onMenuTap,
toast, showDialog, showAction, actions, onActionSelect, compTab
}
}
}).use(vant).mount('#app')
</script>
</body>
</html>