1976 lines
75 KiB
HTML
1976 lines
75 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>市场现场端剩余模块移动复刻 Demo</title>
|
||
<style>
|
||
:root {
|
||
--brand: #0a9f6d;
|
||
--brand-dark: #087f58;
|
||
--brand-soft: #e7f7f1;
|
||
--canvas: #f4f7f5;
|
||
--surface: #fff;
|
||
--text: #1f2d27;
|
||
--muted: #73817a;
|
||
--line: #e6ece9;
|
||
--danger: #e5484d;
|
||
--danger-soft: #fff0f0;
|
||
--warning: #d98516;
|
||
--warning-soft: #fff6e8;
|
||
--info: #4d6a5d;
|
||
--shadow: 0 10px 30px rgba(25, 65, 49, 0.12);
|
||
--radius: 16px;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html,
|
||
body {
|
||
min-height: 100%;
|
||
margin: 0;
|
||
color: var(--text);
|
||
font-family:
|
||
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB",
|
||
"Microsoft YaHei", sans-serif;
|
||
background: #eaf1ed;
|
||
}
|
||
|
||
button,
|
||
input,
|
||
textarea {
|
||
font: inherit;
|
||
}
|
||
|
||
button {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.demo-stage {
|
||
min-height: 100vh;
|
||
display: grid;
|
||
grid-template-columns: minmax(270px, 330px) 390px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 36px;
|
||
padding: 30px;
|
||
}
|
||
|
||
.demo-controls {
|
||
align-self: center;
|
||
padding: 26px;
|
||
border: 1px solid rgba(10, 159, 109, 0.12);
|
||
border-radius: 24px;
|
||
background: rgba(255, 255, 255, 0.94);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.eyebrow {
|
||
margin: 0 0 8px;
|
||
color: var(--brand);
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.demo-controls h2 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.demo-controls .intro {
|
||
margin: 10px 0 22px;
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
.control-label {
|
||
display: block;
|
||
margin: 18px 0 9px;
|
||
color: #506259;
|
||
font-size: 12px;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.segmented {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 4px;
|
||
padding: 4px;
|
||
border-radius: 14px;
|
||
background: #edf3f0;
|
||
}
|
||
|
||
.segmented button,
|
||
.module-grid button {
|
||
min-height: 44px;
|
||
border: 0;
|
||
border-radius: 11px;
|
||
color: #52635b;
|
||
background: transparent;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.segmented button.is-active,
|
||
.module-grid button.is-active {
|
||
color: #fff;
|
||
background: var(--brand);
|
||
box-shadow: 0 5px 14px rgba(10, 159, 109, 0.2);
|
||
}
|
||
|
||
.module-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.module-grid button {
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
}
|
||
|
||
.module-grid button:last-child {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.scope-hint {
|
||
margin: 18px 0 0;
|
||
padding: 13px 14px;
|
||
border-left: 3px solid var(--brand);
|
||
border-radius: 10px;
|
||
color: #4c6258;
|
||
background: var(--brand-soft);
|
||
font-size: 13px;
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.demo-note {
|
||
margin: 13px 0 0;
|
||
color: #8a9891;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.phone {
|
||
position: relative;
|
||
width: 390px;
|
||
height: min(844px, calc(100vh - 36px));
|
||
min-height: 650px;
|
||
overflow: hidden;
|
||
border: 8px solid #203129;
|
||
border-radius: 34px;
|
||
background: var(--canvas);
|
||
box-shadow: 0 28px 70px rgba(19, 47, 36, 0.3);
|
||
}
|
||
|
||
.phone::before {
|
||
content: "";
|
||
position: absolute;
|
||
z-index: 30;
|
||
top: 8px;
|
||
left: 50%;
|
||
width: 104px;
|
||
height: 23px;
|
||
border-radius: 0 0 14px 14px;
|
||
background: #203129;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
.mobile-nav {
|
||
position: absolute;
|
||
z-index: 10;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 66px;
|
||
display: grid;
|
||
grid-template-columns: 52px 1fr 52px;
|
||
align-items: end;
|
||
padding: 0 8px 10px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: #fff;
|
||
}
|
||
|
||
.mobile-nav h1 {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
font-size: 17px;
|
||
line-height: 34px;
|
||
text-align: center;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.nav-back,
|
||
.nav-more {
|
||
min-width: 44px;
|
||
min-height: 44px;
|
||
border: 0;
|
||
color: #263b31;
|
||
background: transparent;
|
||
font-size: 30px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.nav-more {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.mobile-body {
|
||
position: absolute;
|
||
top: 66px;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
scrollbar-width: none;
|
||
}
|
||
|
||
.mobile-body::-webkit-scrollbar,
|
||
.sheet-body::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.search-wrap {
|
||
position: sticky;
|
||
z-index: 5;
|
||
top: 0;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid rgba(230, 236, 233, 0.8);
|
||
background: #fff;
|
||
}
|
||
|
||
.search-box {
|
||
position: relative;
|
||
}
|
||
|
||
.search-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 14px;
|
||
color: #829189;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.search-box input {
|
||
width: 100%;
|
||
height: 42px;
|
||
padding: 0 38px;
|
||
border: 1px solid transparent;
|
||
border-radius: 22px;
|
||
outline: 0;
|
||
color: var(--text);
|
||
background: #f1f5f3;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.search-box input:focus {
|
||
border-color: rgba(10, 159, 109, 0.36);
|
||
background: #fff;
|
||
}
|
||
|
||
.search-clear {
|
||
position: absolute;
|
||
top: 50%;
|
||
right: 5px;
|
||
width: 34px;
|
||
min-height: 34px;
|
||
border: 0;
|
||
border-radius: 50%;
|
||
color: #829189;
|
||
background: transparent;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.list-scope {
|
||
display: flex;
|
||
gap: 7px;
|
||
align-items: center;
|
||
padding: 9px 16px 0;
|
||
color: #6d7e75;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.role-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
flex: 0 0 auto;
|
||
border-radius: 50%;
|
||
background: var(--brand);
|
||
}
|
||
|
||
.card-list {
|
||
display: grid;
|
||
gap: 12px;
|
||
padding: 12px 12px 96px;
|
||
}
|
||
|
||
.biz-card {
|
||
overflow: hidden;
|
||
border: 1px solid #e3ebe7;
|
||
border-radius: var(--radius);
|
||
background: var(--surface);
|
||
box-shadow: 0 5px 16px rgba(30, 65, 51, 0.07);
|
||
}
|
||
|
||
.card-head {
|
||
display: grid;
|
||
grid-template-columns: 34px minmax(0, 1fr) auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
padding: 14px 14px 11px;
|
||
border-bottom: 1px solid #edf1ef;
|
||
}
|
||
|
||
.index-dot {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
background: var(--brand);
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.card-title {
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-title strong {
|
||
display: block;
|
||
overflow: hidden;
|
||
font-size: 15px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-title small {
|
||
display: block;
|
||
margin-top: 3px;
|
||
overflow: hidden;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tag {
|
||
display: inline-flex;
|
||
min-height: 24px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 2px 8px;
|
||
border: 1px solid transparent;
|
||
border-radius: 999px;
|
||
color: var(--brand-dark);
|
||
background: var(--brand-soft);
|
||
font-size: 11px;
|
||
font-weight: 750;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tag.warning {
|
||
color: #a75b00;
|
||
background: var(--warning-soft);
|
||
}
|
||
|
||
.tag.danger {
|
||
color: #bf3439;
|
||
background: var(--danger-soft);
|
||
}
|
||
|
||
.tag.info {
|
||
color: #5b6f65;
|
||
background: #eef2f0;
|
||
}
|
||
|
||
.tag.primary {
|
||
color: #1f66ae;
|
||
background: #edf5ff;
|
||
}
|
||
|
||
.card-body {
|
||
padding: 12px 14px;
|
||
}
|
||
|
||
.field-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 10px 15px;
|
||
}
|
||
|
||
.field-item {
|
||
min-width: 0;
|
||
}
|
||
|
||
.field-item.span-2 {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.field-item label {
|
||
display: block;
|
||
margin-bottom: 3px;
|
||
color: #89968f;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.field-item div {
|
||
overflow: hidden;
|
||
color: #33483e;
|
||
font-size: 13px;
|
||
line-height: 1.45;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.field-item .money {
|
||
color: #a95e09;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.status-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-top: 12px;
|
||
padding-top: 11px;
|
||
border-top: 1px dashed #dfe7e3;
|
||
}
|
||
|
||
.privacy-note {
|
||
margin-top: 12px;
|
||
padding: 9px 10px;
|
||
border-radius: 9px;
|
||
color: #61756a;
|
||
background: #f0f4f2;
|
||
font-size: 11px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 10px 14px 13px;
|
||
border-top: 1px solid #edf1ef;
|
||
}
|
||
|
||
.action-btn {
|
||
min-height: 38px;
|
||
display: inline-flex;
|
||
flex: 1 1 76px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
padding: 0 10px;
|
||
border: 1px solid #dce6e1;
|
||
border-radius: 10px;
|
||
color: #40564b;
|
||
background: #fff;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.action-btn.primary {
|
||
border-color: var(--brand);
|
||
color: #fff;
|
||
background: var(--brand);
|
||
}
|
||
|
||
.action-btn.danger {
|
||
border-color: #f2c8ca;
|
||
color: var(--danger);
|
||
background: #fff;
|
||
}
|
||
|
||
.fab {
|
||
position: absolute;
|
||
z-index: 15;
|
||
right: 18px;
|
||
bottom: 22px;
|
||
width: 56px;
|
||
height: 56px;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 0 0 3px;
|
||
border: 0;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
background: var(--brand);
|
||
box-shadow: 0 9px 24px rgba(10, 159, 109, 0.35);
|
||
font-size: 34px;
|
||
font-weight: 300;
|
||
line-height: 1;
|
||
}
|
||
|
||
.fab[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
.list-footer {
|
||
padding: 4px 0 18px;
|
||
color: #929e98;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-state {
|
||
display: grid;
|
||
min-height: 420px;
|
||
place-items: center;
|
||
padding: 30px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-state .empty-icon {
|
||
display: block;
|
||
margin-bottom: 10px;
|
||
font-size: 40px;
|
||
}
|
||
|
||
.screen-overlay {
|
||
position: fixed;
|
||
z-index: 100;
|
||
inset: 0;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 18px;
|
||
background: rgba(18, 34, 27, 0.52);
|
||
}
|
||
|
||
.sheet {
|
||
position: relative;
|
||
width: min(390px, calc(100vw - 24px));
|
||
height: min(844px, calc(100vh - 30px));
|
||
overflow: hidden;
|
||
border: 7px solid #203129;
|
||
border-radius: 30px;
|
||
background: var(--canvas);
|
||
box-shadow: 0 28px 70px rgba(14, 35, 27, 0.4);
|
||
}
|
||
|
||
.sheet-nav {
|
||
height: 62px;
|
||
display: grid;
|
||
grid-template-columns: 52px 1fr 52px;
|
||
align-items: end;
|
||
padding: 0 8px 8px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: #fff;
|
||
}
|
||
|
||
.sheet-nav h2 {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
font-size: 16px;
|
||
line-height: 42px;
|
||
text-align: center;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.sheet-body {
|
||
position: absolute;
|
||
top: 62px;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 68px;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
}
|
||
|
||
.sheet.no-footer .sheet-body {
|
||
bottom: 0;
|
||
}
|
||
|
||
.sheet-footer {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
min-height: 68px;
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
padding: 10px 14px;
|
||
border-top: 1px solid var(--line);
|
||
background: #fff;
|
||
}
|
||
|
||
.bottom-btn {
|
||
min-height: 46px;
|
||
flex: 1;
|
||
border: 1px solid #dce6e1;
|
||
border-radius: 12px;
|
||
color: #465b51;
|
||
background: #fff;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.bottom-btn.primary {
|
||
border-color: var(--brand);
|
||
color: #fff;
|
||
background: var(--brand);
|
||
}
|
||
|
||
.form-section,
|
||
.detail-section {
|
||
margin-bottom: 12px;
|
||
overflow: hidden;
|
||
border: 1px solid #e4ebe7;
|
||
border-radius: 15px;
|
||
background: #fff;
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin: 0;
|
||
padding: 13px 14px 10px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.section-title::before {
|
||
content: "";
|
||
width: 3px;
|
||
height: 16px;
|
||
border-radius: 2px;
|
||
background: var(--brand);
|
||
}
|
||
|
||
.van-cell {
|
||
position: relative;
|
||
min-height: 56px;
|
||
display: grid;
|
||
grid-template-columns: 116px minmax(0, 1fr) 18px;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin: 0 14px;
|
||
border-top: 1px solid #edf1ef;
|
||
}
|
||
|
||
.van-cell:first-of-type {
|
||
border-top: 0;
|
||
}
|
||
|
||
.van-cell label {
|
||
color: #4a5c53;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.van-cell .required::before {
|
||
content: "*";
|
||
margin-right: 3px;
|
||
color: var(--danger);
|
||
}
|
||
|
||
.van-cell input,
|
||
.van-cell textarea,
|
||
.cell-value {
|
||
min-width: 0;
|
||
width: 100%;
|
||
padding: 8px 0;
|
||
border: 0;
|
||
outline: 0;
|
||
color: #253a30;
|
||
background: transparent;
|
||
font-size: 13px;
|
||
text-align: right;
|
||
}
|
||
|
||
.van-cell textarea {
|
||
min-height: 72px;
|
||
resize: none;
|
||
text-align: left;
|
||
}
|
||
|
||
.van-cell.textarea-cell {
|
||
grid-template-columns: 1fr;
|
||
gap: 0;
|
||
align-items: start;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.cell-arrow {
|
||
color: #a0aaa5;
|
||
font-size: 18px;
|
||
text-align: right;
|
||
}
|
||
|
||
.cell-value.placeholder {
|
||
color: #a6b0ab;
|
||
}
|
||
|
||
.cell-value.disabled {
|
||
color: #adb7b2;
|
||
}
|
||
|
||
.choice-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 4px 14px 14px;
|
||
}
|
||
|
||
.choice-chip {
|
||
min-height: 38px;
|
||
padding: 0 13px;
|
||
border: 1px solid #dce6e1;
|
||
border-radius: 10px;
|
||
color: #52655b;
|
||
background: #fff;
|
||
}
|
||
|
||
.choice-chip.active {
|
||
border-color: var(--brand);
|
||
color: var(--brand-dark);
|
||
background: var(--brand-soft);
|
||
}
|
||
|
||
.attachment-row {
|
||
display: flex;
|
||
gap: 10px;
|
||
overflow-x: auto;
|
||
padding: 0 14px 14px;
|
||
}
|
||
|
||
.upload-card,
|
||
.file-card {
|
||
width: 118px;
|
||
height: 86px;
|
||
flex: 0 0 auto;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.upload-card {
|
||
display: grid;
|
||
place-items: center;
|
||
border: 1.5px dashed #72c5a6;
|
||
color: var(--brand);
|
||
background: #f5fcf9;
|
||
font-size: 12px;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.upload-card span {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
font-size: 25px;
|
||
line-height: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.file-card {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
overflow: hidden;
|
||
border: 1px solid #dfe9e4;
|
||
color: #3f554a;
|
||
background: #eef5f2;
|
||
}
|
||
|
||
.file-icon {
|
||
display: grid;
|
||
flex: 1;
|
||
place-items: center;
|
||
color: var(--brand);
|
||
font-size: 25px;
|
||
}
|
||
|
||
.file-name {
|
||
overflow: hidden;
|
||
padding: 6px 8px;
|
||
background: #fff;
|
||
font-size: 10px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-delete {
|
||
position: absolute;
|
||
top: 4px;
|
||
right: 4px;
|
||
width: 24px;
|
||
min-height: 24px;
|
||
border: 0;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
background: rgba(31, 45, 39, 0.72);
|
||
}
|
||
|
||
.attachment-hint {
|
||
margin: -6px 14px 14px;
|
||
color: #8d9993;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.summary-item {
|
||
padding: 12px 8px;
|
||
border: 1px solid #e1e9e5;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
text-align: center;
|
||
}
|
||
|
||
.summary-item label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
}
|
||
|
||
.summary-item strong {
|
||
color: #31473c;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-item strong.highlight {
|
||
color: var(--brand-dark);
|
||
}
|
||
|
||
.timeline {
|
||
display: grid;
|
||
gap: 10px;
|
||
padding: 0 14px 14px;
|
||
}
|
||
|
||
.timeline-row {
|
||
position: relative;
|
||
padding: 10px 11px 10px 31px;
|
||
border-radius: 10px;
|
||
background: #f6f8f7;
|
||
}
|
||
|
||
.timeline-row::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 15px;
|
||
left: 13px;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--brand);
|
||
}
|
||
|
||
.timeline-row strong {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.timeline-row p {
|
||
margin: 5px 0 0;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.transfer-card {
|
||
margin: 0 14px 12px;
|
||
padding: 12px;
|
||
border: 1px solid #e0e9e4;
|
||
border-radius: 12px;
|
||
background: #f9fbfa;
|
||
}
|
||
|
||
.transfer-card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.transfer-card p {
|
||
margin: 4px 0;
|
||
color: #5c6f65;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.mini-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 7px;
|
||
margin-top: 9px;
|
||
}
|
||
|
||
.mini-actions button {
|
||
min-height: 34px;
|
||
padding: 0 10px;
|
||
border: 1px solid #d8e3de;
|
||
border-radius: 9px;
|
||
color: #41574c;
|
||
background: #fff;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.picker-layer,
|
||
.confirm-layer {
|
||
position: fixed;
|
||
z-index: 130;
|
||
inset: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
background: rgba(17, 33, 26, 0.48);
|
||
}
|
||
|
||
.picker-layer {
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.picker-panel {
|
||
width: min(390px, 100vw);
|
||
max-height: 68vh;
|
||
overflow: hidden;
|
||
border-radius: 22px 22px 0 0;
|
||
background: #fff;
|
||
box-shadow: 0 -12px 36px rgba(19, 44, 34, 0.2);
|
||
}
|
||
|
||
.picker-head {
|
||
min-height: 54px;
|
||
display: grid;
|
||
grid-template-columns: 70px 1fr 70px;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.picker-head h3 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
text-align: center;
|
||
}
|
||
|
||
.picker-head button {
|
||
min-height: 44px;
|
||
border: 0;
|
||
color: var(--brand);
|
||
background: transparent;
|
||
}
|
||
|
||
.picker-list {
|
||
max-height: 50vh;
|
||
overflow-y: auto;
|
||
padding: 6px 16px 20px;
|
||
}
|
||
|
||
.picker-option {
|
||
width: 100%;
|
||
min-height: 50px;
|
||
border: 0;
|
||
border-bottom: 1px solid #edf1ef;
|
||
color: #31473c;
|
||
background: #fff;
|
||
text-align: center;
|
||
}
|
||
|
||
.confirm-layer {
|
||
align-items: center;
|
||
padding: 24px;
|
||
}
|
||
|
||
.confirm-box {
|
||
width: min(320px, 100%);
|
||
overflow: hidden;
|
||
border-radius: 18px;
|
||
background: #fff;
|
||
box-shadow: 0 24px 58px rgba(17, 37, 29, 0.32);
|
||
}
|
||
|
||
.confirm-content {
|
||
padding: 24px 22px 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.confirm-content h3 {
|
||
margin: 0 0 9px;
|
||
font-size: 17px;
|
||
}
|
||
|
||
.confirm-content p {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.confirm-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
border-top: 1px solid var(--line);
|
||
}
|
||
|
||
.confirm-actions button {
|
||
min-height: 50px;
|
||
border: 0;
|
||
background: #fff;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.confirm-actions button + button {
|
||
border-left: 1px solid var(--line);
|
||
color: var(--brand);
|
||
}
|
||
|
||
.toast {
|
||
position: fixed;
|
||
z-index: 200;
|
||
top: 50%;
|
||
left: 50%;
|
||
max-width: 280px;
|
||
padding: 12px 18px;
|
||
border-radius: 12px;
|
||
color: #fff;
|
||
background: rgba(28, 43, 36, 0.9);
|
||
font-size: 13px;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transform: translate(-50%, -42%);
|
||
transition: 0.2s ease;
|
||
}
|
||
|
||
.toast.show {
|
||
opacity: 1;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
@media (max-width: 820px) {
|
||
.demo-stage {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
gap: 20px;
|
||
padding: 16px 10px 30px;
|
||
}
|
||
|
||
.demo-controls {
|
||
width: min(390px, 100%);
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.phone {
|
||
width: min(390px, calc(100vw - 20px));
|
||
height: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="demo-stage">
|
||
<aside class="demo-controls" aria-label="原型控制台">
|
||
<p class="eyebrow">Mobile parity demo</p>
|
||
<h2>市场现场端<br />剩余模块移动复刻</h2>
|
||
<p class="intro">
|
||
一个 HTML 查看 2 种角色 × 5 个模块。手机画布采用 OA 工作流与 Vant 4 的移动组件语义。
|
||
</p>
|
||
<span class="control-label">使用角色</span>
|
||
<div id="roleSwitch" class="segmented"></div>
|
||
<span class="control-label">业务模块</span>
|
||
<div id="moduleSwitch" class="module-grid"></div>
|
||
<p id="scopeHint" class="scope-hint"></p>
|
||
<p class="demo-note">提示:可点击搜索、卡片按钮、右下角新增按钮,并滚动列表查看动态分页效果。</p>
|
||
</aside>
|
||
|
||
<section class="phone" aria-label="移动端页面预览">
|
||
<header class="mobile-nav">
|
||
<button class="nav-back" type="button" aria-label="返回">‹</button>
|
||
<h1 id="pageTitle"></h1>
|
||
<button class="nav-more" type="button" aria-label="更多">•••</button>
|
||
</header>
|
||
<div id="mobileBody" class="mobile-body"></div>
|
||
<button id="fab" class="fab" type="button" aria-label="新增">+</button>
|
||
</section>
|
||
</main>
|
||
|
||
<div id="overlayRoot"></div>
|
||
<div id="floatLayer"></div>
|
||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||
|
||
<script>
|
||
const roles = {
|
||
specialist: {
|
||
label: '市场专员',
|
||
scope: '仅展示当前市场专员本人负责的现场单据;运费批次中的其他专员明细会脱敏。'
|
||
},
|
||
manager: {
|
||
label: '地区经理',
|
||
scope: '展示负责采集地区及指定为当前经理的单据;运费批次可查看分管范围内完整明细。'
|
||
}
|
||
}
|
||
|
||
const modules = {
|
||
protocol: {
|
||
title: '协议单',
|
||
create: true,
|
||
search: '搜索母亲姓名 / 协议编号 / 样本编码'
|
||
},
|
||
chase: {
|
||
title: '催缴信息',
|
||
create: false,
|
||
search: '搜索协议号 / 客户 / 样本编码'
|
||
},
|
||
payment: {
|
||
title: '缴费信息',
|
||
create: false,
|
||
search: '搜索协议号 / 客户 / 样本编码'
|
||
},
|
||
sample: {
|
||
title: '样本信息',
|
||
create: false,
|
||
search: '搜索母亲姓名 / 样本编码'
|
||
},
|
||
freight: {
|
||
title: '运费信息',
|
||
create: true,
|
||
search: '搜索批次号 / 母亲姓名 / 样本编码'
|
||
}
|
||
}
|
||
|
||
const names = ['李安然', '周雨桐', '陈嘉禾', '孙语柔', '吴思宁', '方知夏']
|
||
const hospitals = ['安徽省妇幼保健院', '合肥市第一人民医院', '六安市人民医院', '芜湖市第二人民医院', '蚌埠医学院一附院', '安庆市立医院']
|
||
|
||
const demoData = {
|
||
protocol: names.map((name, index) => ({
|
||
id: `p-${index + 1}`,
|
||
source: index === 2 || index === 5 ? 'research_donation' : 'cell_storage',
|
||
collectDate: `2026-10-${String(11 - index).padStart(2, '0')}`,
|
||
labStorageCode: `LAB-2610${String(11 - index).padStart(2, '0')}-${String(18 + index).padStart(3, '0')}`,
|
||
motherName: name,
|
||
protocolNo: index === 2 || index === 5 ? `KYJZ2026100${index + 1}` : `XBC2026101100${index + 1}`,
|
||
sampleTypeName: index % 2 ? '脐带间充质干细胞' : '脐带血造血干细胞',
|
||
coStored: index === 1 || index === 4,
|
||
regionName: index < 3 ? '合肥区域' : '皖南区域',
|
||
hospitalName: hospitals[index],
|
||
sampleCollectTime: `2026-10-${String(11 - index).padStart(2, '0')} ${index % 2 ? '14:20' : '09:20'}`,
|
||
payMode: index === 2 || index === 5 ? '—' : index % 2 ? '分期' : '趸交',
|
||
firstPayTime: index === 2 || index === 5 ? '—' : `2026-10-${String(11 - index).padStart(2, '0')} 10:05`,
|
||
salesUserName: index % 2 ? '张琳' : '王敏',
|
||
managerName: index < 3 ? '赵经理' : '钱经理',
|
||
transportMode: index % 2 ? '专员送达' : '冷链配送',
|
||
status: index === 4 ? '待生效' : index === 5 ? '已过期' : '生效中'
|
||
})),
|
||
chase: names.map((name, index) => ({
|
||
id: `c-${index + 1}`,
|
||
protocolNo: `XBC2026090${index + 1}`,
|
||
motherName: name,
|
||
motherPhone: `138****${String(3011 + index).slice(-4)}`,
|
||
sampleCode: `SMP-2026-${String(112 + index).padStart(4, '0')}`,
|
||
salesUserName: index % 2 ? '张琳' : '王敏',
|
||
payMode: index % 2 ? '分期' : '趸交',
|
||
scheduleNo: index + 1,
|
||
dueDate: `2026-${index < 3 ? '10' : '11'}-${String(8 + index).padStart(2, '0')}`,
|
||
dueAmount: 3200 + index * 500,
|
||
paidAmount: index % 3 === 0 ? 1200 : index % 3 === 1 ? 0 : 2000,
|
||
arrearsAmount: index % 3 === 0 ? 2000 + index * 500 : index % 3 === 1 ? 3700 + index * 500 : 2200 + index * 500,
|
||
chaseStatus: index < 3 ? '已逾期' : '30 天内到期',
|
||
days: index < 3 ? `${4 + index} 天` : `${12 + index} 天`,
|
||
protocolStatus: index === 5 ? '待生效' : '生效中'
|
||
})),
|
||
payment: names.map((name, index) => ({
|
||
id: `pay-${index + 1}`,
|
||
protocolNo: `XBC2026080${index + 1}`,
|
||
motherName: name,
|
||
motherPhone: `139****${String(6081 + index).slice(-4)}`,
|
||
sampleCode: `SMP-2026-${String(180 + index).padStart(4, '0')}`,
|
||
payMode: index % 2 ? '分期' : '趸交',
|
||
contractAmount: 28000 + index * 1200,
|
||
paidAmount: index % 3 === 0 ? 28000 + index * 1200 : 8000 + index * 1100,
|
||
remainAmount: index % 3 === 0 ? 0 : 20000 + index * 100,
|
||
cleared: index % 3 === 0 ? 1 : index % 3 === 1 ? 2 : 3,
|
||
active: index % 2 ? 6 : 4,
|
||
progress: index % 3 === 0 ? '已缴清' : index % 3 === 1 ? '部分缴费' : '待缴费',
|
||
canConfirm: index === 1 || index === 4
|
||
})),
|
||
sample: names.map((name, index) => ({
|
||
id: `s-${index + 1}`,
|
||
source: index === 5 ? 'research_donation' : 'cell_storage',
|
||
protocolStatus: index === 4 ? '待生效' : '生效中',
|
||
expectedDeliveryDate: `2026-1${index % 3}-${String(12 + index).padStart(2, '0')}`,
|
||
motherName: name,
|
||
newbornName: index < 2 ? '—' : ['安安', '乐乐', '晨晨', '一一'][index - 2],
|
||
sampleCode: `SMP-2026-${String(301 + index).padStart(4, '0')}`,
|
||
labStorageCode: index < 2 ? '—' : `LAB-RK-${String(71 + index).padStart(4, '0')}`,
|
||
sampleTypeName: index % 2 ? '脐带间充质干细胞' : '脐带血造血干细胞',
|
||
collection: index === 4 ? '未采集' : '已采集',
|
||
reception: index < 1 ? '未接收' : '已接收',
|
||
qc: index < 2 ? '未完成' : '已完成',
|
||
storage: index < 3 ? '未入库' : '已入库',
|
||
storageCert: index === 3 ? '已办理' : '未办理',
|
||
insurance: index === 3 ? '已办理' : '未办理'
|
||
})),
|
||
freight: names.map((name, index) => ({
|
||
id: `f-${index + 1}`,
|
||
batchNo: `YF20261011${String(index + 1).padStart(3, '0')}`,
|
||
transportMode: index % 2 ? '专员送达' : '冷链配送',
|
||
freightAmount: 120 + index * 25,
|
||
linkedSampleCount: 2 + (index % 3),
|
||
source: index === 4 ? '科研捐赠' : '细胞存储',
|
||
motherName: name,
|
||
newbornName: index < 2 ? '—' : ['安安', '乐乐', '晨晨', '一一'][index - 2],
|
||
sampleCode: `SMP-2026-${String(421 + index).padStart(4, '0')}`,
|
||
sampleTypeName: index % 2 ? '脐带间充质干细胞' : '脐带血造血干细胞',
|
||
creator: index % 2 ? '张琳' : '王敏',
|
||
createTime: `2026-10-${String(11 - index).padStart(2, '0')} ${index % 2 ? '15:10' : '09:30'}`,
|
||
managerDetails: [`${name} · SMP-2026-${String(421 + index).padStart(4, '0')}`, `关联样本 · SMP-2026-${String(521 + index).padStart(4, '0')}`]
|
||
}))
|
||
}
|
||
|
||
const state = {
|
||
role: 'specialist',
|
||
module: 'protocol',
|
||
keyword: '',
|
||
visibleCount: 3,
|
||
pageSize: 3,
|
||
loadingMore: false
|
||
}
|
||
|
||
const regions = {
|
||
合肥区域: ['安徽省妇幼保健院', '合肥市第一人民医院', '安徽医科大学第一附属医院'],
|
||
皖南区域: ['芜湖市第二人民医院', '宣城市人民医院', '马鞍山市妇幼保健院'],
|
||
皖西区域: ['六安市人民医院', '阜阳市人民医院']
|
||
}
|
||
|
||
let formDraft = null
|
||
let toastTimer = null
|
||
let pickerCallback = null
|
||
let confirmCallback = null
|
||
|
||
const el = (id) => document.getElementById(id)
|
||
const money = (value) => `¥${Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`
|
||
const esc = (value) =>
|
||
String(value ?? '—')
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''')
|
||
|
||
function tag(text, tone = '') {
|
||
return `<span class="tag ${tone}">${esc(text)}</span>`
|
||
}
|
||
|
||
function field(label, value, className = '') {
|
||
return `<div class="field-item ${className}"><label>${esc(label)}</label><div>${value}</div></div>`
|
||
}
|
||
|
||
function icon(name) {
|
||
const icons = { view: '⌕', edit: '✎', audit: '✓', delete: '⌫', doc: '▤', pay: '¥', link: '⌁', add: '+', cert: '▧', insurance: '◇' }
|
||
return `<span aria-hidden="true">${icons[name] || '•'}</span>`
|
||
}
|
||
|
||
function action(label, actionName, id, options = {}) {
|
||
return `<button class="action-btn ${options.tone || ''}" type="button" data-action="${actionName}" data-id="${id}">${icon(options.icon)}${esc(label)}</button>`
|
||
}
|
||
|
||
function toneFor(text) {
|
||
if (/逾期|作废|拒绝|欠缴/.test(text)) return 'danger'
|
||
if (/待|未|部分|到期/.test(text)) return 'warning'
|
||
if (/来源|科研|冷链|分期/.test(text)) return 'primary'
|
||
if (/已|生效|正常|趸交/.test(text)) return ''
|
||
return 'info'
|
||
}
|
||
|
||
function cardShell(row, index, title, subTitle, status, body, actions) {
|
||
return `
|
||
<article class="biz-card" data-row-id="${row.id}">
|
||
<div class="card-head">
|
||
<span class="index-dot">${index + 1}</span>
|
||
<div class="card-title"><strong>${esc(title)}</strong><small>${esc(subTitle)}</small></div>
|
||
${tag(status, toneFor(status))}
|
||
</div>
|
||
<div class="card-body">${body}</div>
|
||
<div class="card-actions">${actions}</div>
|
||
</article>`
|
||
}
|
||
|
||
function renderProtocolCard(row, index) {
|
||
const sourceLabel = row.source === 'cell_storage' ? '细胞存储' : '科研捐赠'
|
||
const body = `
|
||
<div class="field-grid">
|
||
${field('业务来源', tag(sourceLabel, row.source === 'cell_storage' ? '' : 'warning'))}
|
||
${field('采集日期', esc(row.collectDate))}
|
||
${field('实验室流转编号', esc(row.labStorageCode), 'span-2')}
|
||
${field(row.source === 'cell_storage' ? '协议编号' : '捐赠编号', esc(row.protocolNo), 'span-2')}
|
||
${field('样本类型', esc(row.sampleTypeName), 'span-2')}
|
||
${field('是否同存', tag(row.coStored ? '是' : '否', row.coStored ? 'warning' : 'info'))}
|
||
${field('采集地区', esc(row.regionName))}
|
||
${field('采集医院', esc(row.hospitalName), 'span-2')}
|
||
${field('采集时间', esc(row.sampleCollectTime), 'span-2')}
|
||
${field('缴费方式', tag(row.payMode, toneFor(row.payMode)))}
|
||
${field('首期缴费时间', esc(row.firstPayTime))}
|
||
${field('市场专员', esc(row.salesUserName))}
|
||
${field('地区经理', esc(row.managerName))}
|
||
${field('运输方式', esc(row.transportMode), 'span-2')}
|
||
</div>`
|
||
const actions =
|
||
action('审核', 'audit-protocol', row.id, { icon: 'audit', tone: 'primary' }) +
|
||
action('编辑', 'edit-protocol', row.id, { icon: 'edit' }) +
|
||
action('查看', 'view-protocol', row.id, { icon: 'view' }) +
|
||
action('删除', 'delete-protocol', row.id, { icon: 'delete', tone: 'danger' })
|
||
return cardShell(row, index, row.motherName, row.protocolNo, row.status, body, actions)
|
||
}
|
||
|
||
function renderChaseCard(row, index) {
|
||
const body = `
|
||
<div class="field-grid">
|
||
${field('协议号', esc(row.protocolNo), 'span-2')}
|
||
${field('客户', esc(row.motherName))}
|
||
${field('联系电话', esc(row.motherPhone))}
|
||
${field('样本编码', esc(row.sampleCode), 'span-2')}
|
||
${field('市场专员', esc(row.salesUserName))}
|
||
${field('缴费方式', tag(row.payMode, toneFor(row.payMode)))}
|
||
${field('期次', `第 ${row.scheduleNo} 期`)}
|
||
${field('应缴日', esc(row.dueDate))}
|
||
${field('应缴金额', `<span class="money">${money(row.dueAmount)}</span>`)}
|
||
${field('已缴金额', `<span class="money">${money(row.paidAmount)}</span>`)}
|
||
${field('欠缴金额', `<span class="money">${money(row.arrearsAmount)}</span>`)}
|
||
${field('协议状态', tag(row.protocolStatus, toneFor(row.protocolStatus)))}
|
||
</div>
|
||
<div class="status-row">${tag(`${row.chaseStatus} ${row.days}`, toneFor(row.chaseStatus))}</div>`
|
||
const actions =
|
||
(row.paidAmount ? action('转账明细', 'view-chase-transfer', row.id, { icon: 'pay' }) : '') +
|
||
action('协议单', 'view-related-protocol', row.id, { icon: 'doc', tone: 'primary' })
|
||
return cardShell(row, index, `${row.motherName} · 第 ${row.scheduleNo} 期`, row.sampleCode, row.chaseStatus, body, actions)
|
||
}
|
||
|
||
function renderPaymentCard(row, index) {
|
||
const body = `
|
||
<div class="field-grid">
|
||
${field('协议号', esc(row.protocolNo), 'span-2')}
|
||
${field('客户', esc(row.motherName))}
|
||
${field('联系电话', esc(row.motherPhone))}
|
||
${field('样本编码', esc(row.sampleCode), 'span-2')}
|
||
${field('缴费方式', tag(row.payMode, toneFor(row.payMode)))}
|
||
${field('合同额', `<span class="money">${money(row.contractAmount)}</span>`)}
|
||
${field('已缴', `<span class="money">${money(row.paidAmount)}</span>`)}
|
||
${field('待缴', `<span class="money">${money(row.remainAmount)}</span>`)}
|
||
${field('整体进度', `已缴清 ${row.cleared}/${row.active} 期`)}
|
||
${field('缴费进度', tag(row.progress, toneFor(row.progress)))}
|
||
</div>`
|
||
const actions =
|
||
action('查看', 'view-payment', row.id, { icon: 'view', tone: 'primary' }) +
|
||
(row.canConfirm ? action('缴费确认', 'confirm-payment', row.id, { icon: 'audit' }) : '') +
|
||
action('协议单', 'view-related-protocol', row.id, { icon: 'doc' })
|
||
return cardShell(row, index, row.motherName, row.protocolNo, row.progress, body, actions)
|
||
}
|
||
|
||
function renderSampleCard(row, index) {
|
||
const body = `
|
||
<div class="field-grid">
|
||
${field('协议状态', tag(row.protocolStatus, toneFor(row.protocolStatus)))}
|
||
${field('预产期', esc(row.expectedDeliveryDate))}
|
||
${field('母亲姓名', esc(row.motherName))}
|
||
${field('新生儿姓名', esc(row.newbornName))}
|
||
${field('样本编码', esc(row.sampleCode), 'span-2')}
|
||
${field('实验室入库编码', esc(row.labStorageCode), 'span-2')}
|
||
${field('样本类型', esc(row.sampleTypeName), 'span-2')}
|
||
</div>
|
||
<div class="status-row">
|
||
${tag(`采集 · ${row.collection}`, toneFor(row.collection))}
|
||
${tag(`接收 · ${row.reception}`, toneFor(row.reception))}
|
||
${tag(`质检 · ${row.qc}`, toneFor(row.qc))}
|
||
${tag(`入库 · ${row.storage}`, toneFor(row.storage))}
|
||
${row.source === 'cell_storage' ? tag(`储存证 · ${row.storageCert}`, toneFor(row.storageCert)) : ''}
|
||
${row.source === 'cell_storage' ? tag(`保险 · ${row.insurance}`, toneFor(row.insurance)) : ''}
|
||
</div>`
|
||
let actions = action('查看', 'view-sample', row.id, { icon: 'view', tone: 'primary' })
|
||
if (row.source === 'cell_storage') {
|
||
actions += action('储存证质检信息', 'view-storage-qc', row.id, { icon: 'doc' })
|
||
if (row.storage === '已入库' && row.storageCert === '未办理') actions += action('登记储存证', 'register-cert', row.id, { icon: 'cert' })
|
||
if (row.storage === '已入库' && row.insurance === '未办理') actions += action('登记保险', 'register-insurance', row.id, { icon: 'insurance' })
|
||
}
|
||
return cardShell(row, index, row.motherName, row.sampleCode, row.protocolStatus, body, actions)
|
||
}
|
||
|
||
function renderFreightCard(row, index) {
|
||
const detailBlock = state.role === 'manager'
|
||
? `<div class="status-row">${row.managerDetails.map((item) => tag(item, 'info')).join('')}</div>`
|
||
: '<div class="privacy-note">🔒 同批次其他专员明细已脱敏</div>'
|
||
const body = `
|
||
<div class="field-grid">
|
||
${field('批次号', esc(row.batchNo), 'span-2')}
|
||
${field('运输方式', tag(row.transportMode, 'primary'))}
|
||
${field('运费金额', `<span class="money">${money(row.freightAmount)}</span>`)}
|
||
${field('关联数', state.role === 'manager' ? String(row.linkedSampleCount) : '仅本人相关')}
|
||
${field('来源', tag(row.source, row.source === '细胞存储' ? '' : 'warning'))}
|
||
${field('母亲姓名', esc(row.motherName))}
|
||
${field('子女姓名', esc(row.newbornName))}
|
||
${field('样本编码', esc(row.sampleCode), 'span-2')}
|
||
${field('样本类型', esc(row.sampleTypeName), 'span-2')}
|
||
${field('创建人', esc(row.creator))}
|
||
${field('创建时间', esc(row.createTime))}
|
||
</div>${detailBlock}`
|
||
const actions =
|
||
action('编辑', 'edit-freight', row.id, { icon: 'edit', tone: 'primary' }) +
|
||
action('关联样本', 'link-freight', row.id, { icon: 'link' }) +
|
||
action('取消关联', 'unlink-freight', row.id, { icon: 'delete', tone: 'danger' })
|
||
return cardShell(row, index, row.batchNo, `${row.transportMode} · ${money(row.freightAmount)}`, row.source, body, actions)
|
||
}
|
||
|
||
function renderCard(moduleKey, row, index) {
|
||
const renderers = {
|
||
protocol: renderProtocolCard,
|
||
chase: renderChaseCard,
|
||
payment: renderPaymentCard,
|
||
sample: renderSampleCard,
|
||
freight: renderFreightCard
|
||
}
|
||
return renderers[moduleKey](row, index)
|
||
}
|
||
|
||
function filteredRows() {
|
||
const keyword = state.keyword.trim().toLowerCase()
|
||
if (!keyword) return demoData[state.module]
|
||
return demoData[state.module].filter((row) => JSON.stringify(row).toLowerCase().includes(keyword))
|
||
}
|
||
|
||
function renderControls() {
|
||
el('roleSwitch').innerHTML = Object.entries(roles)
|
||
.map(([key, item]) => `<button type="button" data-role="${key}" class="${state.role === key ? 'is-active' : ''}">${item.label}</button>`)
|
||
.join('')
|
||
el('moduleSwitch').innerHTML = Object.entries(modules)
|
||
.map(([key, item]) => `<button type="button" data-module="${key}" class="${state.module === key ? 'is-active' : ''}">${item.title}</button>`)
|
||
.join('')
|
||
el('scopeHint').textContent = roles[state.role].scope
|
||
}
|
||
|
||
function renderList() {
|
||
const rows = filteredRows()
|
||
const visible = rows.slice(0, state.visibleCount)
|
||
el('pageTitle').textContent = `${roles[state.role].label} · ${modules[state.module].title}`
|
||
el('fab').hidden = !modules[state.module].create
|
||
el('mobileBody').innerHTML = `
|
||
<div class="search-wrap">
|
||
<div class="search-box">
|
||
<span class="search-icon">⌕</span>
|
||
<input id="keywordInput" type="search" value="${esc(state.keyword)}" placeholder="${esc(modules[state.module].search)}" aria-label="关键词搜索" />
|
||
${state.keyword ? '<button class="search-clear" type="button" data-action="clear-search" aria-label="清空">×</button>' : ''}
|
||
</div>
|
||
</div>
|
||
<div class="list-scope"><span class="role-dot"></span>${roles[state.role].label}数据范围 · 共 ${rows.length} 条示例数据</div>
|
||
${visible.length ? `<div class="card-list">${visible.map((row, index) => renderCard(state.module, row, index)).join('')}<div class="list-footer">${state.loadingMore ? '正在加载…' : visible.length < rows.length ? '继续向下滚动加载' : '没有更多了'}</div></div>` : '<div class="empty-state"><div><span class="empty-icon">⌕</span>没有找到匹配的数据</div></div>'}
|
||
`
|
||
const input = el('keywordInput')
|
||
if (input) {
|
||
input.addEventListener('input', (event) => {
|
||
state.keyword = event.target.value
|
||
state.visibleCount = state.pageSize
|
||
renderList()
|
||
requestAnimationFrame(() => {
|
||
const next = el('keywordInput')
|
||
if (next) {
|
||
next.focus()
|
||
next.setSelectionRange(next.value.length, next.value.length)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
function renderApp() {
|
||
renderControls()
|
||
renderList()
|
||
}
|
||
|
||
function loadMore() {
|
||
if (state.loadingMore) return
|
||
const total = filteredRows().length
|
||
if (state.visibleCount >= total) return
|
||
state.loadingMore = true
|
||
renderList()
|
||
window.setTimeout(() => {
|
||
state.visibleCount = Math.min(state.visibleCount + state.pageSize, total)
|
||
state.loadingMore = false
|
||
renderList()
|
||
showToast(`已加载 ${Math.min(state.visibleCount, total)} / ${total} 条`)
|
||
}, 520)
|
||
}
|
||
|
||
function showToast(message) {
|
||
const node = el('toast')
|
||
node.textContent = message
|
||
node.classList.add('show')
|
||
window.clearTimeout(toastTimer)
|
||
toastTimer = window.setTimeout(() => node.classList.remove('show'), 1800)
|
||
}
|
||
|
||
function openSheet({ title, body, footer = '' }) {
|
||
el('overlayRoot').innerHTML = `
|
||
<div class="screen-overlay">
|
||
<section class="sheet ${footer ? '' : 'no-footer'}" role="dialog" aria-modal="true" aria-label="${esc(title)}">
|
||
<header class="sheet-nav">
|
||
<button class="nav-back" type="button" data-action="close-sheet" aria-label="返回">‹</button>
|
||
<h2>${esc(title)}</h2>
|
||
<span></span>
|
||
</header>
|
||
<div class="sheet-body">${body}</div>
|
||
${footer ? `<footer class="sheet-footer">${footer}</footer>` : ''}
|
||
</section>
|
||
</div>`
|
||
}
|
||
|
||
function closeSheet() {
|
||
el('overlayRoot').innerHTML = ''
|
||
el('floatLayer').innerHTML = ''
|
||
}
|
||
|
||
function openPicker(title, options, callback) {
|
||
pickerCallback = callback
|
||
el('floatLayer').innerHTML = `
|
||
<div class="picker-layer">
|
||
<section class="picker-panel" role="dialog" aria-modal="true" aria-label="${esc(title)}">
|
||
<header class="picker-head"><button type="button" data-action="close-float">取消</button><h3>${esc(title)}</h3><span></span></header>
|
||
<div class="picker-list">${options.map((item, index) => `<button class="picker-option" type="button" data-picker-index="${index}">${esc(item)}</button>`).join('')}</div>
|
||
</section>
|
||
</div>`
|
||
}
|
||
|
||
function confirmAction(message, callback) {
|
||
confirmCallback = callback
|
||
el('floatLayer').innerHTML = `
|
||
<div class="confirm-layer">
|
||
<section class="confirm-box" role="alertdialog" aria-modal="true">
|
||
<div class="confirm-content"><h3>请确认</h3><p>${esc(message)}</p></div>
|
||
<div class="confirm-actions"><button type="button" data-action="close-float">取消</button><button type="button" data-action="confirm-ok">确认</button></div>
|
||
</section>
|
||
</div>`
|
||
}
|
||
|
||
function formCell(label, value, options = {}) {
|
||
const valueClass = value ? '' : 'placeholder'
|
||
return `<div class="van-cell" ${options.action ? `data-action="${options.action}"` : ''}>
|
||
<label class="${options.required ? 'required' : ''}">${esc(label)}</label>
|
||
${options.input ? `<input data-draft-key="${options.key}" value="${esc(value || '')}" placeholder="${esc(options.placeholder || '请输入')}" />` : `<div class="cell-value ${valueClass} ${options.disabled ? 'disabled' : ''}">${esc(value || options.placeholder || '请选择')}</div>`}
|
||
<span class="cell-arrow">${options.action && !options.disabled ? '›' : ''}</span>
|
||
</div>`
|
||
}
|
||
|
||
function textareaCell(label, value, key) {
|
||
return `<div class="van-cell textarea-cell"><label>${esc(label)}</label><textarea data-draft-key="${key}" placeholder="请输入${esc(label)}">${esc(value || '')}</textarea></div>`
|
||
}
|
||
|
||
function attachmentArea(files, prefix = 'protocol') {
|
||
return `<div class="attachment-row">
|
||
<button class="upload-card" type="button" data-action="add-attachment" data-prefix="${prefix}"><div><span>+</span>上传附件</div></button>
|
||
${files.map((file, index) => `<div class="file-card"><div class="file-icon">▤</div><div class="file-name">${esc(file)}</div><button class="file-delete" type="button" data-action="remove-attachment" data-index="${index}" aria-label="删除附件">×</button></div>`).join('')}
|
||
</div><p class="attachment-hint">选择文件后立即上传;单个文件不超过 20MB</p>`
|
||
}
|
||
|
||
function syncDraftInputs() {
|
||
document.querySelectorAll('[data-draft-key]').forEach((node) => {
|
||
formDraft[node.dataset.draftKey] = node.value
|
||
})
|
||
}
|
||
|
||
function openProtocolForm(row = null) {
|
||
if (!formDraft || formDraft.kind !== 'protocol' || formDraft.id !== (row?.id || 'new')) {
|
||
formDraft = {
|
||
kind: 'protocol',
|
||
id: row?.id || 'new',
|
||
source: row?.source || 'cell_storage',
|
||
protocolNo: row?.protocolNo || '',
|
||
registerDate: row?.collectDate || '2026-10-11',
|
||
motherName: row?.motherName || '',
|
||
motherPhone: row ? '138****3011' : '',
|
||
idCard: '',
|
||
expectedDeliveryDate: '',
|
||
region: row?.regionName || '',
|
||
hospital: row?.hospitalName || '',
|
||
collectTime: row?.sampleCollectTime || '',
|
||
sampleType: row?.sampleTypeName || '',
|
||
sampleCode: row ? `SMP-${row.protocolNo.slice(-6)}` : '',
|
||
storageYears: row?.source === 'research_donation' ? '' : '20',
|
||
amount: row?.source === 'research_donation' ? '' : '28800',
|
||
remark: '',
|
||
attachments: row ? ['协议扫描件.pdf', '知情同意书.jpg'] : []
|
||
}
|
||
}
|
||
const title = row ? '编辑协议单' : '新增协议单'
|
||
const hospitalDisabled = !formDraft.region
|
||
const body = `
|
||
<section class="form-section">
|
||
<h3 class="section-title">协议信息</h3>
|
||
<div class="choice-row"><button type="button" class="choice-chip ${formDraft.source === 'cell_storage' ? 'active' : ''}" data-action="source-cell">细胞存储</button><button type="button" class="choice-chip ${formDraft.source === 'research_donation' ? 'active' : ''}" data-action="source-donation">科研捐赠</button></div>
|
||
${formCell(formDraft.source === 'cell_storage' ? '协议编号' : '捐赠编号', formDraft.protocolNo, { input: true, key: 'protocolNo', placeholder: '请输入编号' })}
|
||
${formCell(formDraft.source === 'cell_storage' ? '登记日期' : '捐赠日期', formDraft.registerDate, { action: 'pick-date', required: true })}
|
||
${formDraft.source === 'cell_storage' ? formCell('协议总金额', formDraft.amount, { input: true, key: 'amount', placeholder: '请输入金额' }) : ''}
|
||
${textareaCell('备注', formDraft.remark, 'remark')}
|
||
</section>
|
||
<section class="form-section">
|
||
<h3 class="section-title">采集信息</h3>
|
||
${formCell('采集地区', formDraft.region, { action: 'pick-region', required: true })}
|
||
${formCell('采集医院', formDraft.hospital, { action: hospitalDisabled ? '' : 'pick-hospital', required: true, disabled: hospitalDisabled, placeholder: hospitalDisabled ? '请先选择采集地区' : '请选择采集医院' })}
|
||
${formCell('采集时间', formDraft.collectTime, { action: 'pick-datetime' })}
|
||
</section>
|
||
<section class="form-section">
|
||
<h3 class="section-title">客户信息</h3>
|
||
${formCell('母亲姓名', formDraft.motherName, { input: true, key: 'motherName', required: true })}
|
||
${formCell('联系电话', formDraft.motherPhone, { input: true, key: 'motherPhone' })}
|
||
${formCell('证件号码', formDraft.idCard, { input: true, key: 'idCard' })}
|
||
${formCell('预产期', formDraft.expectedDeliveryDate, { action: 'pick-delivery-date' })}
|
||
</section>
|
||
<section class="form-section">
|
||
<h3 class="section-title">样本信息</h3>
|
||
${formCell('样本类型', formDraft.sampleType, { action: 'pick-sample-type', required: true })}
|
||
${formCell('样本编码', formDraft.sampleCode, { input: true, key: 'sampleCode' })}
|
||
${formDraft.source === 'cell_storage' ? formCell('存储年限', formDraft.storageYears, { input: true, key: 'storageYears' }) : ''}
|
||
<div class="choice-row"><button type="button" class="choice-chip" data-action="add-sample">+ 新增一笔样本</button></div>
|
||
</section>
|
||
<section class="form-section">
|
||
<h3 class="section-title">附件与确认</h3>
|
||
${attachmentArea(formDraft.attachments)}
|
||
<div class="choice-row"><button type="button" class="choice-chip active" data-action="toggle-confirm">✓ 我已核对以上信息</button></div>
|
||
</section>`
|
||
const footer = '<button class="bottom-btn" type="button" data-action="close-sheet">取消</button><button class="bottom-btn primary" type="button" data-action="save-protocol">确定</button>'
|
||
openSheet({ title, body, footer })
|
||
}
|
||
|
||
function protocolDetail(row, title = '协议详情') {
|
||
const body = `
|
||
<section class="detail-section"><h3 class="section-title">协议信息</h3><div class="card-body"><div class="field-grid">
|
||
${field('业务来源', tag(row.source === 'cell_storage' ? '细胞存储' : '科研捐赠'))}
|
||
${field('协议状态', tag(row.status, toneFor(row.status)))}
|
||
${field('协议编号', esc(row.protocolNo), 'span-2')}
|
||
${field('母亲姓名', esc(row.motherName))}
|
||
${field('样本类型', esc(row.sampleTypeName))}
|
||
</div></div></section>
|
||
<section class="detail-section"><h3 class="section-title">采集信息</h3><div class="card-body"><div class="field-grid">
|
||
${field('采集地区', esc(row.regionName))}${field('采集医院', esc(row.hospitalName))}${field('采集时间', esc(row.sampleCollectTime), 'span-2')}${field('市场专员', esc(row.salesUserName))}${field('地区经理', esc(row.managerName))}
|
||
</div></div></section>
|
||
<section class="detail-section"><h3 class="section-title">附件信息</h3>${attachmentArea(['协议扫描件.pdf', '知情同意书.jpg'], 'read-only')}</section>`
|
||
openSheet({ title, body, footer: '<button class="bottom-btn primary" type="button" data-action="close-sheet">返回</button>' })
|
||
}
|
||
|
||
function openPaymentDetail(row) {
|
||
const body = `
|
||
<div class="summary-grid">
|
||
<div class="summary-item"><label>合同额</label><strong>${money(row.contractAmount)}</strong></div>
|
||
<div class="summary-item"><label>已缴</label><strong class="highlight">${money(row.paidAmount)}</strong></div>
|
||
<div class="summary-item"><label>待缴</label><strong>${money(row.remainAmount)}</strong></div>
|
||
</div>
|
||
<section class="detail-section"><h3 class="section-title">客户与协议</h3><div class="card-body"><div class="field-grid">${field('协议号', esc(row.protocolNo), 'span-2')}${field('客户', esc(row.motherName))}${field('样本编码', esc(row.sampleCode))}${field('缴费方式', tag(row.payMode, toneFor(row.payMode)))}${field('缴费进度', tag(row.progress, toneFor(row.progress)))}</div></div></section>
|
||
<section class="detail-section"><h3 class="section-title">缴费计划</h3><div class="timeline">
|
||
<div class="timeline-row"><strong>第 1 期 · 已缴清</strong><p>应缴 2026-09-10 · ${money(Math.min(row.paidAmount, 8000))}</p></div>
|
||
<div class="timeline-row"><strong>第 2 期 · 待确认</strong><p>应缴 2026-10-10 · ${money(5200)}</p></div>
|
||
<div class="timeline-row"><strong>第 3 期 · 待缴费</strong><p>应缴 2026-11-10 · ${money(5200)}</p></div>
|
||
</div></section>
|
||
<section class="detail-section"><h3 class="section-title">转账明细</h3>
|
||
<div class="transfer-card"><div class="transfer-card-head"><strong>转账笔 1</strong>${tag('待财务确认', 'warning')}</div><p>转账方式:微信转账</p><p>转账金额:${money(5200)}</p><p>转账时间:2026-10-11 10:20</p><div class="mini-actions"><button type="button" data-action="edit-transfer">编辑</button><button type="button" data-action="submit-transfer">提交财务确认</button><button type="button" data-action="withdraw-transfer">撤回</button></div></div>
|
||
</section>`
|
||
openSheet({ title: '样本缴费详情', body, footer: '<button class="bottom-btn" type="button" data-action="close-sheet">返回</button><button class="bottom-btn primary" type="button" data-action="register-transfer">登记转账</button>' })
|
||
}
|
||
|
||
function openChaseTransfer(row) {
|
||
const body = `
|
||
<div class="summary-grid"><div class="summary-item"><label>应缴</label><strong>${money(row.dueAmount)}</strong></div><div class="summary-item"><label>实缴合计</label><strong class="highlight">${money(row.paidAmount)}</strong></div><div class="summary-item"><label>欠缴</label><strong>${money(row.arrearsAmount)}</strong></div></div>
|
||
<section class="detail-section"><h3 class="section-title">转账明细</h3><div class="transfer-card"><div class="transfer-card-head"><strong>微信转账</strong>${tag('已确认')}</div><p>转账金额:${money(row.paidAmount)}</p><p>转账时间:2026-10-06 15:35</p><p>财务备注:凭证清晰,到账无误</p></div></section>`
|
||
openSheet({ title: `${row.motherName} · 第 ${row.scheduleNo} 期转账记录`, body, footer: '<button class="bottom-btn primary" type="button" data-action="close-sheet">返回</button>' })
|
||
}
|
||
|
||
function openTransferForm() {
|
||
formDraft = { kind: 'transfer', amount: '5200', time: '2026-10-11 10:20', type: '微信转账', remark: '', attachments: ['转账凭证.jpg'] }
|
||
renderTransferForm()
|
||
}
|
||
|
||
function renderTransferForm() {
|
||
const body = `<section class="form-section"><h3 class="section-title">转账信息</h3>${formCell('转账金额', formDraft.amount, { input: true, key: 'amount', required: true })}${formCell('转账时间', formDraft.time, { action: 'pick-transfer-time' })}${formCell('转账方式', formDraft.type, { action: 'pick-transfer-type', required: true })}${textareaCell('备注', formDraft.remark, 'remark')}</section><section class="form-section"><h3 class="section-title">转账凭证</h3>${attachmentArea(formDraft.attachments, 'transfer')}</section>`
|
||
openSheet({ title: '登记转账', body, footer: '<button class="bottom-btn" type="button" data-action="close-sheet">取消</button><button class="bottom-btn primary" type="button" data-action="save-transfer">保存</button>' })
|
||
}
|
||
|
||
function openSampleDetail(row, qcOnly = false) {
|
||
const body = qcOnly
|
||
? `<section class="detail-section"><h3 class="section-title">储存证质检信息</h3><div class="timeline"><div class="timeline-row"><strong>细胞活率检测 · 通过</strong><p>检测值 96.8% · 2026-10-10 14:30</p></div><div class="timeline-row"><strong>微生物检测 · 通过</strong><p>无菌、支原体阴性 · 2026-10-10 16:20</p></div><div class="timeline-row"><strong>质检结论 · 合格</strong><p>可办理储存证</p></div></div></section>`
|
||
: `<section class="detail-section"><h3 class="section-title">样本信息</h3><div class="card-body"><div class="field-grid">${field('母亲姓名', esc(row.motherName))}${field('新生儿姓名', esc(row.newbornName))}${field('样本编码', esc(row.sampleCode), 'span-2')}${field('实验室入库编码', esc(row.labStorageCode), 'span-2')}${field('样本类型', esc(row.sampleTypeName), 'span-2')}</div><div class="status-row">${tag(`采集 · ${row.collection}`, toneFor(row.collection))}${tag(`接收 · ${row.reception}`, toneFor(row.reception))}${tag(`质检 · ${row.qc}`, toneFor(row.qc))}${tag(`入库 · ${row.storage}`, toneFor(row.storage))}</div></div></section>`
|
||
openSheet({ title: qcOnly ? '储存证质检信息' : '样本详情', body, footer: '<button class="bottom-btn primary" type="button" data-action="close-sheet">返回</button>' })
|
||
}
|
||
|
||
function openHandleForm(row, kind) {
|
||
formDraft = {
|
||
kind: 'handle',
|
||
rowId: row.id,
|
||
handleKind: kind,
|
||
time: '2026-10-11 14:30',
|
||
user: state.role === 'specialist' ? '王敏' : '赵经理',
|
||
remark: '',
|
||
attachments: []
|
||
}
|
||
renderHandleForm()
|
||
}
|
||
|
||
function renderHandleForm() {
|
||
const label = formDraft.handleKind === 'cert' ? '储存证' : '保险'
|
||
const body = `<section class="form-section"><h3 class="section-title">${label}办理信息</h3>${formCell('办理时间', formDraft.time, { action: 'pick-handle-time', required: true })}${formCell('办理人员', formDraft.user, { action: 'pick-handler', required: true })}${textareaCell('备注', formDraft.remark, 'remark')}</section><section class="form-section"><h3 class="section-title">${label}附件</h3>${attachmentArea(formDraft.attachments, 'handle')}</section>`
|
||
openSheet({ title: `登记${label}`, body, footer: `<button class="bottom-btn" type="button" data-action="close-sheet">取消</button><button class="bottom-btn primary" type="button" data-action="save-handle" data-row-id="${formDraft.rowId}">保存</button>` })
|
||
}
|
||
|
||
function openFreightForm(row = null) {
|
||
formDraft = { kind: 'freight', id: row?.id || 'new', transportMode: row?.transportMode || '', amount: row ? String(row.freightAmount) : '', selected: row ? [row.sampleCode] : [] }
|
||
renderFreightForm()
|
||
}
|
||
|
||
function renderFreightForm() {
|
||
const available = state.role === 'specialist' ? demoData.sample.slice(0, 3) : demoData.sample.slice(0, 5)
|
||
const body = `<section class="form-section"><h3 class="section-title">运费批次</h3>${formCell('运输方式', formDraft.transportMode, { action: 'pick-transport', required: true })}${formCell('运费金额', formDraft.amount, { input: true, key: 'amount', required: true })}</section><section class="form-section"><h3 class="section-title">关联样本</h3><div class="timeline">${available.map((item) => `<button type="button" class="timeline-row" data-action="toggle-freight-sample" data-sample="${item.sampleCode}" style="border:1px solid ${formDraft.selected.includes(item.sampleCode) ? 'var(--brand)' : 'transparent'};text-align:left"><strong>${formDraft.selected.includes(item.sampleCode) ? '✓ ' : ''}${item.motherName} · ${item.sampleCode}</strong><p>${item.sampleTypeName}</p></button>`).join('')}</div></section>`
|
||
openSheet({ title: formDraft.id === 'new' ? '新增运费' : '编辑运费', body, footer: '<button class="bottom-btn" type="button" data-action="close-sheet">取消</button><button class="bottom-btn primary" type="button" data-action="save-freight">保存</button>' })
|
||
}
|
||
|
||
function openFreightRelation(row, unlink = false) {
|
||
const items = state.role === 'specialist' ? demoData.sample.slice(0, 2) : demoData.sample.slice(0, 5)
|
||
const body = `<section class="detail-section"><h3 class="section-title">${unlink ? '选择要取消关联的样本' : '选择要关联的样本'}</h3><div class="timeline">${items.map((item, index) => `<button type="button" class="timeline-row" data-action="toggle-relation" style="border:1px solid ${index === 0 ? 'var(--brand)' : 'transparent'};text-align:left"><strong>${index === 0 ? '✓ ' : ''}${item.motherName} · ${item.sampleCode}</strong><p>${item.sampleTypeName}</p></button>`).join('')}</div>${state.role === 'specialist' ? '<div class="privacy-note" style="margin:0 14px 14px">仅提供当前市场专员自己的样本;其他专员明细不可选择。</div>' : ''}</section>`
|
||
openSheet({ title: unlink ? '取消关联' : '关联样本', body, footer: `<button class="bottom-btn" type="button" data-action="close-sheet">取消</button><button class="bottom-btn primary" type="button" data-action="save-relation">${unlink ? '确认取消' : '确认关联'}</button>` })
|
||
}
|
||
|
||
function openGenericProtocolFromRelated(row) {
|
||
const protocol = demoData.protocol.find((item) => item.motherName === row.motherName) || demoData.protocol[0]
|
||
protocolDetail(protocol, '关联协议单')
|
||
}
|
||
|
||
function findRow(moduleKey, id) {
|
||
return demoData[moduleKey].find((item) => item.id === id)
|
||
}
|
||
|
||
function handleAction(actionName, source) {
|
||
const id = source.dataset.id
|
||
if (actionName === 'clear-search') {
|
||
state.keyword = ''
|
||
state.visibleCount = state.pageSize
|
||
renderList()
|
||
return
|
||
}
|
||
if (actionName === 'close-sheet') return closeSheet()
|
||
if (actionName === 'close-float') {
|
||
el('floatLayer').innerHTML = ''
|
||
return
|
||
}
|
||
if (actionName === 'confirm-ok') {
|
||
const callback = confirmCallback
|
||
el('floatLayer').innerHTML = ''
|
||
if (callback) callback()
|
||
return
|
||
}
|
||
|
||
if (actionName === 'view-protocol') return protocolDetail(findRow('protocol', id))
|
||
if (actionName === 'edit-protocol') {
|
||
formDraft = null
|
||
return openProtocolForm(findRow('protocol', id))
|
||
}
|
||
if (actionName === 'audit-protocol') {
|
||
const row = findRow('protocol', id)
|
||
return confirmAction(`确认审核通过 ${row.motherName} 的协议单 ${row.protocolNo} 吗?`, () => showToast('审核已通过'))
|
||
}
|
||
if (actionName === 'delete-protocol') {
|
||
const row = findRow('protocol', id)
|
||
return confirmAction(`删除后该协议单将进入回收状态,确认删除 ${row.protocolNo} 吗?`, () => showToast('协议单已删除(Demo)'))
|
||
}
|
||
if (actionName === 'view-chase-transfer') return openChaseTransfer(findRow('chase', id))
|
||
if (actionName === 'view-related-protocol') return openGenericProtocolFromRelated(findRow(state.module, id))
|
||
if (actionName === 'view-payment') return openPaymentDetail(findRow('payment', id))
|
||
if (actionName === 'confirm-payment') return confirmAction('确认当前转账凭证与到账金额无误吗?', () => showToast('缴费确认成功'))
|
||
if (actionName === 'view-sample') return openSampleDetail(findRow('sample', id))
|
||
if (actionName === 'view-storage-qc') return openSampleDetail(findRow('sample', id), true)
|
||
if (actionName === 'register-cert') return openHandleForm(findRow('sample', id), 'cert')
|
||
if (actionName === 'register-insurance') return openHandleForm(findRow('sample', id), 'insurance')
|
||
if (actionName === 'edit-freight') return openFreightForm(findRow('freight', id))
|
||
if (actionName === 'link-freight') return openFreightRelation(findRow('freight', id), false)
|
||
if (actionName === 'unlink-freight') return openFreightRelation(findRow('freight', id), true)
|
||
if (actionName === 'source-cell' || actionName === 'source-donation') {
|
||
syncDraftInputs()
|
||
formDraft.source = actionName === 'source-cell' ? 'cell_storage' : 'research_donation'
|
||
return openProtocolForm(formDraft.id === 'new' ? null : findRow('protocol', formDraft.id))
|
||
}
|
||
if (actionName === 'pick-region') {
|
||
syncDraftInputs()
|
||
return openPicker('选择采集地区', Object.keys(regions), (value) => {
|
||
formDraft.region = value
|
||
formDraft.hospital = ''
|
||
openProtocolForm(formDraft.id === 'new' ? null : findRow('protocol', formDraft.id))
|
||
})
|
||
}
|
||
if (actionName === 'pick-hospital') {
|
||
syncDraftInputs()
|
||
return openPicker('选择采集医院', regions[formDraft.region] || [], (value) => {
|
||
formDraft.hospital = value
|
||
openProtocolForm(formDraft.id === 'new' ? null : findRow('protocol', formDraft.id))
|
||
})
|
||
}
|
||
if (actionName === 'pick-sample-type') {
|
||
syncDraftInputs()
|
||
return openPicker('选择样本类型', ['脐带血造血干细胞', '脐带间充质干细胞', '胎盘间充质干细胞'], (value) => {
|
||
formDraft.sampleType = value
|
||
openProtocolForm(formDraft.id === 'new' ? null : findRow('protocol', formDraft.id))
|
||
})
|
||
}
|
||
if (['pick-date', 'pick-delivery-date', 'pick-datetime'].includes(actionName)) {
|
||
syncDraftInputs()
|
||
const values = actionName === 'pick-datetime' ? ['2026-10-11 09:20', '2026-10-11 14:30', '2026-10-12 08:00'] : ['2026-10-11', '2026-10-12', '2026-11-01']
|
||
return openPicker('选择日期时间', values, (value) => {
|
||
if (actionName === 'pick-date') formDraft.registerDate = value
|
||
if (actionName === 'pick-delivery-date') formDraft.expectedDeliveryDate = value
|
||
if (actionName === 'pick-datetime') formDraft.collectTime = value
|
||
openProtocolForm(formDraft.id === 'new' ? null : findRow('protocol', formDraft.id))
|
||
})
|
||
}
|
||
if (actionName === 'add-attachment') {
|
||
syncDraftInputs()
|
||
formDraft.attachments.push(`新上传附件-${formDraft.attachments.length + 1}.jpg`)
|
||
if (formDraft.kind === 'protocol') openProtocolForm(formDraft.id === 'new' ? null : findRow('protocol', formDraft.id))
|
||
else if (formDraft.kind === 'transfer') renderTransferForm()
|
||
else renderHandleForm()
|
||
return showToast('附件上传成功')
|
||
}
|
||
if (actionName === 'remove-attachment') {
|
||
syncDraftInputs()
|
||
formDraft.attachments.splice(Number(source.dataset.index), 1)
|
||
if (formDraft.kind === 'protocol') openProtocolForm(formDraft.id === 'new' ? null : findRow('protocol', formDraft.id))
|
||
else if (formDraft.kind === 'transfer') renderTransferForm()
|
||
else renderHandleForm()
|
||
return showToast('附件已移除')
|
||
}
|
||
if (actionName === 'add-sample') return showToast('已新增一笔样本卡片(Demo)')
|
||
if (actionName === 'toggle-confirm') return showToast('已确认核对信息')
|
||
if (actionName === 'save-protocol') {
|
||
syncDraftInputs()
|
||
if (!formDraft.motherName || !formDraft.region || !formDraft.hospital || !formDraft.sampleType) return showToast('请完善必填信息')
|
||
closeSheet()
|
||
return showToast('协议单保存成功')
|
||
}
|
||
if (actionName === 'register-transfer' || actionName === 'edit-transfer') return openTransferForm()
|
||
if (actionName === 'pick-transfer-type') {
|
||
syncDraftInputs()
|
||
return openPicker('选择转账方式', ['微信转账', '支付宝转账', '银行转账', '现金'], (value) => {
|
||
formDraft.type = value
|
||
renderTransferForm()
|
||
})
|
||
}
|
||
if (actionName === 'pick-transfer-time') {
|
||
syncDraftInputs()
|
||
return openPicker('选择转账时间', ['2026-10-11 10:20', '2026-10-11 14:30'], (value) => {
|
||
formDraft.time = value
|
||
renderTransferForm()
|
||
})
|
||
}
|
||
if (actionName === 'save-transfer') {
|
||
closeSheet()
|
||
return showToast('转账笔保存成功')
|
||
}
|
||
if (actionName === 'submit-transfer') return confirmAction('提交后将由财务人员确认到账,是否继续?', () => showToast('已提交财务确认'))
|
||
if (actionName === 'withdraw-transfer') return confirmAction('确认撤回该转账笔吗?', () => showToast('转账笔已撤回'))
|
||
if (actionName === 'pick-handle-time') return openPicker('选择办理时间', ['2026-10-11 14:30', '2026-10-12 09:00'], (value) => {
|
||
formDraft.time = value
|
||
renderHandleForm()
|
||
})
|
||
if (actionName === 'pick-handler') return openPicker('选择办理人员', ['王敏', '张琳', '赵经理'], (value) => {
|
||
formDraft.user = value
|
||
renderHandleForm()
|
||
})
|
||
if (actionName === 'save-handle') {
|
||
closeSheet()
|
||
return showToast(`登记${formDraft.handleKind === 'cert' ? '储存证' : '保险'}成功`)
|
||
}
|
||
if (actionName === 'pick-transport') {
|
||
syncDraftInputs()
|
||
return openPicker('选择运输方式', ['冷链配送', '专员送达', '客户自送'], (value) => {
|
||
formDraft.transportMode = value
|
||
renderFreightForm()
|
||
})
|
||
}
|
||
if (actionName === 'toggle-freight-sample') {
|
||
syncDraftInputs()
|
||
const sample = source.dataset.sample
|
||
formDraft.selected = formDraft.selected.includes(sample) ? formDraft.selected.filter((item) => item !== sample) : [...formDraft.selected, sample]
|
||
return renderFreightForm()
|
||
}
|
||
if (actionName === 'save-freight') {
|
||
syncDraftInputs()
|
||
if (!formDraft.transportMode || !formDraft.amount) return showToast('请选择运输方式并填写运费金额')
|
||
closeSheet()
|
||
return showToast('运费信息保存成功')
|
||
}
|
||
if (actionName === 'toggle-relation') {
|
||
source.style.borderColor = source.style.borderColor === 'var(--brand)' ? 'transparent' : 'var(--brand)'
|
||
const strong = source.querySelector('strong')
|
||
strong.textContent = strong.textContent.startsWith('✓ ') ? strong.textContent.slice(2) : `✓ ${strong.textContent}`
|
||
}
|
||
if (actionName === 'save-relation') {
|
||
closeSheet()
|
||
return showToast('关联关系已更新')
|
||
}
|
||
}
|
||
|
||
el('roleSwitch').addEventListener('click', (event) => {
|
||
const button = event.target.closest('[data-role]')
|
||
if (!button) return
|
||
state.role = button.dataset.role
|
||
state.visibleCount = state.pageSize
|
||
state.keyword = ''
|
||
renderApp()
|
||
})
|
||
|
||
el('moduleSwitch').addEventListener('click', (event) => {
|
||
const button = event.target.closest('[data-module]')
|
||
if (!button) return
|
||
state.module = button.dataset.module
|
||
state.visibleCount = state.pageSize
|
||
state.keyword = ''
|
||
el('mobileBody').scrollTop = 0
|
||
renderApp()
|
||
})
|
||
|
||
el('mobileBody').addEventListener('click', (event) => {
|
||
const target = event.target.closest('[data-action]')
|
||
if (target) handleAction(target.dataset.action, target)
|
||
})
|
||
|
||
el('mobileBody').addEventListener('scroll', () => {
|
||
const body = el('mobileBody')
|
||
if (body.scrollTop + body.clientHeight >= body.scrollHeight - 80) loadMore()
|
||
})
|
||
|
||
el('overlayRoot').addEventListener('click', (event) => {
|
||
const target = event.target.closest('[data-action]')
|
||
if (target) handleAction(target.dataset.action, target)
|
||
})
|
||
|
||
el('floatLayer').addEventListener('click', (event) => {
|
||
const option = event.target.closest('[data-picker-index]')
|
||
if (option) {
|
||
const values = [...event.currentTarget.querySelectorAll('[data-picker-index]')].map((node) => node.textContent)
|
||
const value = values[Number(option.dataset.pickerIndex)]
|
||
const callback = pickerCallback
|
||
el('floatLayer').innerHTML = ''
|
||
if (callback) callback(value)
|
||
return
|
||
}
|
||
const target = event.target.closest('[data-action]')
|
||
if (target) handleAction(target.dataset.action, target)
|
||
})
|
||
|
||
el('fab').addEventListener('click', () => {
|
||
if (state.module === 'protocol') {
|
||
formDraft = null
|
||
openProtocolForm()
|
||
}
|
||
if (state.module === 'freight') openFreightForm()
|
||
})
|
||
|
||
renderApp()
|
||
</script>
|
||
</body>
|
||
</html>
|