|
|
@@ -0,0 +1,767 @@
|
|
|
+<!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" />
|
|
|
+ <title>报销申请 · 发票缩略图选择 Demo</title>
|
|
|
+ <style>
|
|
|
+ :root {
|
|
|
+ --primary: #00c98d;
|
|
|
+ --primary-dark: #00a874;
|
|
|
+ --primary-bg: #eafff6;
|
|
|
+ --bg: #f4f7f6;
|
|
|
+ --surface: #fff;
|
|
|
+ --text: #1e293b;
|
|
|
+ --text-sec: #64748b;
|
|
|
+ --text-muted: #94a3b8;
|
|
|
+ --border: #e2e8f0;
|
|
|
+ --danger: #ef4444;
|
|
|
+ --radius: 12px;
|
|
|
+ --phone-w: 375px;
|
|
|
+ }
|
|
|
+
|
|
|
+ * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+ body {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #dfe8e4;
|
|
|
+ color: var(--text);
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ padding: 24px 16px 48px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-intro {
|
|
|
+ max-width: 980px;
|
|
|
+ margin: 0 auto 20px;
|
|
|
+ color: #334155;
|
|
|
+ }
|
|
|
+ .page-intro h1 { font-size: 18px; margin-bottom: 6px; }
|
|
|
+ .page-intro p { font-size: 13px; color: #64748b; margin-bottom: 4px; }
|
|
|
+ .page-intro code {
|
|
|
+ font-size: 12px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #cbd5e1;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 1px 5px;
|
|
|
+ }
|
|
|
+ .legend {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 12px;
|
|
|
+ }
|
|
|
+ .legend span {
|
|
|
+ font-size: 12px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #cbd5e1;
|
|
|
+ border-radius: 999px;
|
|
|
+ padding: 4px 10px;
|
|
|
+ color: #475569;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 28px;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: flex-start;
|
|
|
+ max-width: 980px;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .phone-wrap { width: var(--phone-w); }
|
|
|
+ .phone-label {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #334155;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+ .phone-label small {
|
|
|
+ display: block;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #64748b;
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .phone {
|
|
|
+ width: var(--phone-w);
|
|
|
+ background: var(--bg);
|
|
|
+ border-radius: 28px;
|
|
|
+ box-shadow: 0 16px 40px rgba(15, 23, 42, 0.12);
|
|
|
+ overflow: hidden;
|
|
|
+ border: 8px solid #1e293b;
|
|
|
+ position: relative;
|
|
|
+ height: 740px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header {
|
|
|
+ background: linear-gradient(145deg, #008f62 0%, #00c98d 70%, #34d399 160%);
|
|
|
+ color: #fff;
|
|
|
+ padding: 14px 16px 18px;
|
|
|
+ border-radius: 0 0 20px 20px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .header__row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+ .header__back { font-size: 18px; opacity: 0.95; }
|
|
|
+ .header__title { font-size: 17px; font-weight: 700; }
|
|
|
+
|
|
|
+ .body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 12px 12px 88px;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card {
|
|
|
+ background: var(--surface);
|
|
|
+ border-radius: var(--radius);
|
|
|
+ padding: 14px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
|
+ }
|
|
|
+ .card-h {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+ .card-h h3 { font-size: 15px; font-weight: 600; }
|
|
|
+ .badge {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--primary-dark);
|
|
|
+ background: var(--primary-bg);
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .field { margin-bottom: 12px; }
|
|
|
+ .field:last-child { margin-bottom: 0; }
|
|
|
+ .field-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-sec);
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+ .field-box {
|
|
|
+ height: 40px;
|
|
|
+ border: 1.5px solid var(--border);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 0 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: var(--text);
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+ .field-box.ph { color: var(--text-muted); }
|
|
|
+
|
|
|
+ .tip {
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--text-sec);
|
|
|
+ margin-bottom: 10px;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+ .tip.warn { color: #b45309; }
|
|
|
+ .form-tip { margin-top: 8px; font-size: 12px; color: var(--text-muted); }
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 36px;
|
|
|
+ padding: 0 14px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-family: inherit;
|
|
|
+ }
|
|
|
+ .btn-primary {
|
|
|
+ background: var(--primary);
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ .btn-block { width: 100%; height: 44px; }
|
|
|
+ .btn:disabled { opacity: 0.55; cursor: default; }
|
|
|
+
|
|
|
+ /* —— 已选:缩略图网格(替代文件名列表) —— */
|
|
|
+ .picked-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 12px;
|
|
|
+ }
|
|
|
+ .picked-thumb {
|
|
|
+ position: relative;
|
|
|
+ width: 72px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ .picked-thumb__img {
|
|
|
+ width: 72px;
|
|
|
+ height: 72px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ background: #f1f5f9;
|
|
|
+ }
|
|
|
+ .picked-thumb__img img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+ .picked-thumb__doc {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 2px;
|
|
|
+ padding: 6px;
|
|
|
+ font-size: 10px;
|
|
|
+ color: var(--text-muted);
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .picked-thumb__amt {
|
|
|
+ display: block;
|
|
|
+ width: 72px;
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--primary-dark);
|
|
|
+ text-align: center;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+ .picked-thumb__del {
|
|
|
+ position: absolute;
|
|
|
+ top: 2px;
|
|
|
+ right: 2px;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: none;
|
|
|
+ background: rgba(15, 23, 42, 0.55);
|
|
|
+ color: #fff;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1;
|
|
|
+ cursor: pointer;
|
|
|
+ z-index: 2;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bottom-bar {
|
|
|
+ position: absolute;
|
|
|
+ left: 0; right: 0; bottom: 0;
|
|
|
+ padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
|
|
+ background: #fff;
|
|
|
+ border-top: 1px solid var(--border);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* —— Sheet:缩略图多选 + 滚动加载 —— */
|
|
|
+ .mask {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ background: rgba(15, 23, 42, 0.45);
|
|
|
+ opacity: 0;
|
|
|
+ pointer-events: none;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+ z-index: 20;
|
|
|
+ }
|
|
|
+ .mask.show { opacity: 1; pointer-events: auto; }
|
|
|
+
|
|
|
+ .sheet {
|
|
|
+ position: absolute;
|
|
|
+ left: 0; right: 0; bottom: 0;
|
|
|
+ height: 78%;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 16px 16px 0 0;
|
|
|
+ z-index: 30;
|
|
|
+ transform: translateY(105%);
|
|
|
+ transition: transform 0.25s ease;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+ .sheet.show { transform: translateY(0); }
|
|
|
+
|
|
|
+ .sheet__title {
|
|
|
+ padding: 14px 16px;
|
|
|
+ text-align: center;
|
|
|
+ font-weight: 700;
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .sheet__close {
|
|
|
+ position: absolute;
|
|
|
+ right: 12px;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ color: var(--text-muted);
|
|
|
+ font-size: 22px;
|
|
|
+ cursor: pointer;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sheet__body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 12px;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumb-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumb-card {
|
|
|
+ position: relative;
|
|
|
+ border: 1.5px solid var(--border);
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fff;
|
|
|
+ overflow: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 0;
|
|
|
+ text-align: left;
|
|
|
+ font-family: inherit;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+ .thumb-card.is-selected {
|
|
|
+ border-color: var(--primary);
|
|
|
+ box-shadow: 0 0 0 1px var(--primary);
|
|
|
+ }
|
|
|
+ .thumb-card__check {
|
|
|
+ position: absolute;
|
|
|
+ top: 6px;
|
|
|
+ right: 6px;
|
|
|
+ z-index: 2;
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 1.5px solid #fff;
|
|
|
+ background: rgba(15, 23, 42, 0.25);
|
|
|
+ color: #fff;
|
|
|
+ font-size: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ .thumb-card.is-selected .thumb-card__check {
|
|
|
+ background: var(--primary);
|
|
|
+ border-color: #fff;
|
|
|
+ }
|
|
|
+ .thumb-card__preview {
|
|
|
+ aspect-ratio: 1 / 1;
|
|
|
+ background: #f1f5f9;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ .thumb-card__preview img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+ .thumb-card__doc {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 8px;
|
|
|
+ color: var(--text-muted);
|
|
|
+ font-size: 11px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .thumb-card__doc .ext {
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--text-sec);
|
|
|
+ }
|
|
|
+ .thumb-card__meta {
|
|
|
+ padding: 6px 8px 8px;
|
|
|
+ border-top: 1px solid var(--border);
|
|
|
+ }
|
|
|
+ .thumb-card__amt {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--primary-dark);
|
|
|
+ }
|
|
|
+ .thumb-card__sub {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 10px;
|
|
|
+ color: var(--text-muted);
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .load-more {
|
|
|
+ margin-top: 12px;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-muted);
|
|
|
+ padding: 8px;
|
|
|
+ }
|
|
|
+ .load-more.loading::after {
|
|
|
+ content: '加载中…';
|
|
|
+ }
|
|
|
+ .load-more.done::after {
|
|
|
+ content: '没有更多了';
|
|
|
+ }
|
|
|
+ .empty {
|
|
|
+ padding: 40px 16px;
|
|
|
+ text-align: center;
|
|
|
+ color: var(--text-muted);
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sheet__footer {
|
|
|
+ padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
|
|
+ border-top: 1px solid var(--border);
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 预览层 */
|
|
|
+ .preview-mask {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 50;
|
|
|
+ background: rgba(0, 0, 0, 0.88);
|
|
|
+ display: none;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ }
|
|
|
+ .preview-mask.show { display: flex; }
|
|
|
+ .preview-mask img {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 70%;
|
|
|
+ border-radius: 8px;
|
|
|
+ object-fit: contain;
|
|
|
+ }
|
|
|
+ .preview-mask .hint {
|
|
|
+ color: #cbd5e1;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 方案对比小标签 */
|
|
|
+ .variant-note {
|
|
|
+ margin-top: 16px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 10px;
|
|
|
+ border: 1px dashed #94a3b8;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #475569;
|
|
|
+ line-height: 1.6;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="page-intro">
|
|
|
+ <h1>报销申请 · 发票缩略图选择 Demo</h1>
|
|
|
+ <p>左侧:申请页已选发票改为缩略图(点击预览,× 删除)。右侧:从发票夹选择 = 3 列缩略图网格,滚动加载,默认每次 10 条。</p>
|
|
|
+ <p>可直接在手机框内操作;也可打开本地文件:<code>docs/superpowers/demos/2026-09-16-bx-reimburse-invoice-thumb-demo.html</code></p>
|
|
|
+ <div class="legend">
|
|
|
+ <span>缩略图 1:1 · aspectFill</span>
|
|
|
+ <span>pageSize = 10</span>
|
|
|
+ <span>多选 + 勾选角标</span>
|
|
|
+ <span>非图片显示扩展名占位</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stage">
|
|
|
+ <!-- ① 申请页:已选缩略图 -->
|
|
|
+ <div class="phone-wrap">
|
|
|
+ <div class="phone-label">
|
|
|
+ ① 报销申请 · 已选展示
|
|
|
+ <small>用缩略图替代发票名称</small>
|
|
|
+ </div>
|
|
|
+ <div class="phone" id="phone-apply">
|
|
|
+ <div class="header">
|
|
|
+ <div class="header__row">
|
|
|
+ <span class="header__back">←</span>
|
|
|
+ <span class="header__title">报销申请</span>
|
|
|
+ <span style="width:24px"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="body">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-h">
|
|
|
+ <h3>报销信息</h3>
|
|
|
+ <span class="badge">新建</span>
|
|
|
+ </div>
|
|
|
+ <div class="field">
|
|
|
+ <span class="field-label">申请人</span>
|
|
|
+ <div class="field-box">张三</div>
|
|
|
+ </div>
|
|
|
+ <div class="field">
|
|
|
+ <span class="field-label">报销日期 *</span>
|
|
|
+ <div class="field-box">2026-09-16</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-h">
|
|
|
+ <h3>关联发票(可选)</h3>
|
|
|
+ </div>
|
|
|
+ <div class="tip">发票余额 ¥1,280.00 · 本次合计 ¥356.50</div>
|
|
|
+ <button class="btn btn-primary" type="button" id="btn-open-picker">从发票夹选择</button>
|
|
|
+
|
|
|
+ <div class="picked-grid" id="picked-grid"></div>
|
|
|
+ <div class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。点击缩略图可预览。</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-h"><h3>报销说明</h3></div>
|
|
|
+ <div class="field-box ph">请输入报销说明</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bottom-bar">
|
|
|
+ <button class="btn btn-primary btn-block" type="button">保存</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="preview-mask" id="preview-mask">
|
|
|
+ <img id="preview-img" alt="预览" />
|
|
|
+ <div class="hint">点击空白处关闭</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="variant-note">
|
|
|
+ <b>已选区约定:</b>仅展示缩略图 + 金额角标文案;不再展示文件名。点击图片 → <code>uni.previewImage</code>;点 × 仅移除关联,不删发票夹原件。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ② 选择弹层:缩略图 + 滚动加载 -->
|
|
|
+ <div class="phone-wrap">
|
|
|
+ <div class="phone-label">
|
|
|
+ ② 从发票夹选择 · 缩略图列表
|
|
|
+ <small>滚动到底自动加载下一批(10 条)</small>
|
|
|
+ </div>
|
|
|
+ <div class="phone" id="phone-picker">
|
|
|
+ <div class="header">
|
|
|
+ <div class="header__row">
|
|
|
+ <span class="header__back">←</span>
|
|
|
+ <span class="header__title">报销申请</span>
|
|
|
+ <span style="width:24px"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="body" style="opacity:0.35; pointer-events:none;">
|
|
|
+ <div class="card"><div class="card-h"><h3>关联发票</h3></div><div class="tip">…</div></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="mask show"></div>
|
|
|
+ <div class="sheet show">
|
|
|
+ <div class="sheet__title">
|
|
|
+ 选择发票附件
|
|
|
+ <button class="sheet__close" type="button" title="关闭">×</button>
|
|
|
+ </div>
|
|
|
+ <div class="sheet__body" id="picker-scroll">
|
|
|
+ <div class="thumb-grid" id="thumb-grid"></div>
|
|
|
+ <div class="load-more" id="load-more"></div>
|
|
|
+ </div>
|
|
|
+ <div class="sheet__footer">
|
|
|
+ <button class="btn btn-primary btn-block" type="button" id="btn-confirm">
|
|
|
+ 确定(已选 <span id="sel-count">0</span>)
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="variant-note">
|
|
|
+ <b>选择态:</b>点整卡切换勾选(角标 ✓)。金额与日期在缩略图下方,便于快速辨认。PDF 等非图片用扩展名占位,选中逻辑相同。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ const PAGE_SIZE = 10
|
|
|
+ const PLACEHOLDERS = [
|
|
|
+ 'https://picsum.photos/seed/inv1/200/200',
|
|
|
+ 'https://picsum.photos/seed/inv2/200/200',
|
|
|
+ 'https://picsum.photos/seed/inv3/200/200',
|
|
|
+ 'https://picsum.photos/seed/inv4/200/200',
|
|
|
+ 'https://picsum.photos/seed/inv5/200/200',
|
|
|
+ 'https://picsum.photos/seed/inv6/200/200',
|
|
|
+ 'https://picsum.photos/seed/inv7/200/200',
|
|
|
+ 'https://picsum.photos/seed/inv8/200/200',
|
|
|
+ null, // PDF 占位
|
|
|
+ 'https://picsum.photos/seed/inv9/200/200'
|
|
|
+ ]
|
|
|
+
|
|
|
+ /** 模拟发票夹全量 28 条附件 */
|
|
|
+ const ALL = Array.from({ length: 28 }, (_, i) => {
|
|
|
+ const isPdf = i % 9 === 8
|
|
|
+ return {
|
|
|
+ key: `inv-${i + 1}:att-${i + 1}`,
|
|
|
+ amount: (80 + (i * 37) % 900) + (i % 3) * 0.5,
|
|
|
+ date: `2026-0${(i % 9) + 1}-${String((i % 27) + 1).padStart(2, '0')}`,
|
|
|
+ fileName: isPdf ? `电子发票_${i + 1}.pdf` : `发票照片_${i + 1}.jpg`,
|
|
|
+ fileType: isPdf ? 'application/pdf' : 'image/jpeg',
|
|
|
+ previewUrl: isPdf ? null : PLACEHOLDERS[i % PLACEHOLDERS.length] || PLACEHOLDERS[0]
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ let loaded = []
|
|
|
+ let page = 0
|
|
|
+ let loading = false
|
|
|
+ let finished = false
|
|
|
+ const draft = new Set(['inv-1:att-1', 'inv-3:att-3'])
|
|
|
+ let picked = [
|
|
|
+ { ...ALL[0] },
|
|
|
+ { ...ALL[2] }
|
|
|
+ ]
|
|
|
+
|
|
|
+ function money(v) {
|
|
|
+ return Number(v || 0).toLocaleString('zh-CN', {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderPicked() {
|
|
|
+ const el = document.getElementById('picked-grid')
|
|
|
+ if (!picked.length) {
|
|
|
+ el.innerHTML = '<div class="form-tip" style="margin:0">暂未选择发票</div>'
|
|
|
+ return
|
|
|
+ }
|
|
|
+ el.innerHTML = picked.map((p) => {
|
|
|
+ const inner = p.previewUrl
|
|
|
+ ? `<img src="${p.previewUrl}" alt="" />`
|
|
|
+ : `<div class="picked-thumb__doc"><span>PDF</span><span>${escapeHtml(p.fileName)}</span></div>`
|
|
|
+ return `
|
|
|
+ <div class="picked-thumb" data-key="${p.key}">
|
|
|
+ <button class="picked-thumb__del" type="button" data-del="${p.key}" aria-label="移除">×</button>
|
|
|
+ <div class="picked-thumb__img" data-preview="${p.key}">${inner}</div>
|
|
|
+ <span class="picked-thumb__amt">¥${money(p.amount)}</span>
|
|
|
+ </div>`
|
|
|
+ }).join('')
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderGrid() {
|
|
|
+ const grid = document.getElementById('thumb-grid')
|
|
|
+ grid.innerHTML = loaded.map((item) => {
|
|
|
+ const selected = draft.has(item.key)
|
|
|
+ const preview = item.previewUrl
|
|
|
+ ? `<img src="${item.previewUrl}" alt="" loading="lazy" />`
|
|
|
+ : `<div class="thumb-card__doc"><span class="ext">PDF</span><span>${escapeHtml(item.fileName)}</span></div>`
|
|
|
+ return `
|
|
|
+ <button type="button" class="thumb-card ${selected ? 'is-selected' : ''}" data-key="${item.key}">
|
|
|
+ <span class="thumb-card__check">${selected ? '✓' : ''}</span>
|
|
|
+ <div class="thumb-card__preview">${preview}</div>
|
|
|
+ <div class="thumb-card__meta">
|
|
|
+ <div class="thumb-card__amt">¥${money(item.amount)}</div>
|
|
|
+ <div class="thumb-card__sub">${item.date}</div>
|
|
|
+ </div>
|
|
|
+ </button>`
|
|
|
+ }).join('')
|
|
|
+ document.getElementById('sel-count').textContent = String(draft.size)
|
|
|
+ const tip = document.getElementById('load-more')
|
|
|
+ tip.className = 'load-more ' + (finished ? 'done' : loading ? 'loading' : '')
|
|
|
+ tip.textContent = finished
|
|
|
+ ? '没有更多了'
|
|
|
+ : loading
|
|
|
+ ? '加载中…'
|
|
|
+ : loaded.length
|
|
|
+ ? '上拉加载更多'
|
|
|
+ : ''
|
|
|
+ }
|
|
|
+
|
|
|
+ function escapeHtml(s) {
|
|
|
+ return String(s).replace(/[&<>"']/g, (c) => ({
|
|
|
+ '&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
|
+ })[c])
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadNext() {
|
|
|
+ if (loading || finished) return
|
|
|
+ loading = true
|
|
|
+ renderGrid()
|
|
|
+ setTimeout(() => {
|
|
|
+ const start = page * PAGE_SIZE
|
|
|
+ const chunk = ALL.slice(start, start + PAGE_SIZE)
|
|
|
+ loaded = loaded.concat(chunk)
|
|
|
+ page += 1
|
|
|
+ if (loaded.length >= ALL.length || chunk.length < PAGE_SIZE) finished = true
|
|
|
+ loading = false
|
|
|
+ renderGrid()
|
|
|
+ }, 420)
|
|
|
+ }
|
|
|
+
|
|
|
+ document.getElementById('picker-scroll').addEventListener('scroll', (e) => {
|
|
|
+ const el = e.currentTarget
|
|
|
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40) loadNext()
|
|
|
+ })
|
|
|
+
|
|
|
+ document.getElementById('thumb-grid').addEventListener('click', (e) => {
|
|
|
+ const card = e.target.closest('.thumb-card')
|
|
|
+ if (!card) return
|
|
|
+ const key = card.dataset.key
|
|
|
+ if (draft.has(key)) draft.delete(key)
|
|
|
+ else draft.add(key)
|
|
|
+ renderGrid()
|
|
|
+ })
|
|
|
+
|
|
|
+ document.getElementById('btn-confirm').addEventListener('click', () => {
|
|
|
+ picked = ALL.filter((i) => draft.has(i.key))
|
|
|
+ renderPicked()
|
|
|
+ alert('已同步到左侧「已选」区域(Demo 示意)')
|
|
|
+ })
|
|
|
+
|
|
|
+ document.getElementById('picked-grid').addEventListener('click', (e) => {
|
|
|
+ const del = e.target.closest('[data-del]')
|
|
|
+ if (del) {
|
|
|
+ const key = del.getAttribute('data-del')
|
|
|
+ picked = picked.filter((p) => p.key !== key)
|
|
|
+ draft.delete(key)
|
|
|
+ renderPicked()
|
|
|
+ renderGrid()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const preview = e.target.closest('[data-preview]')
|
|
|
+ if (!preview) return
|
|
|
+ const key = preview.getAttribute('data-preview')
|
|
|
+ const item = picked.find((p) => p.key === key)
|
|
|
+ if (!item) return
|
|
|
+ if (!item.previewUrl) {
|
|
|
+ alert('非图片附件:正式环境可跳转下载/打开;Demo 仅提示')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const mask = document.getElementById('preview-mask')
|
|
|
+ document.getElementById('preview-img').src = item.previewUrl
|
|
|
+ mask.classList.add('show')
|
|
|
+ })
|
|
|
+
|
|
|
+ document.getElementById('preview-mask').addEventListener('click', () => {
|
|
|
+ document.getElementById('preview-mask').classList.remove('show')
|
|
|
+ })
|
|
|
+
|
|
|
+ document.getElementById('btn-open-picker').addEventListener('click', () => {
|
|
|
+ alert('正式实现会打开右侧样式的底部弹层;本 Demo 请直接在右侧手机框操作选择。')
|
|
|
+ })
|
|
|
+
|
|
|
+ renderPicked()
|
|
|
+ loadNext()
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|