diff --git a/ONE-OS小程序/小羚羚.jsx b/ONE-OS小程序/小羚羚.jsx index 4dc2e23..8c97556 100644 --- a/ONE-OS小程序/小羚羚.jsx +++ b/ONE-OS小程序/小羚羚.jsx @@ -225,12 +225,66 @@ const PAGE_STYLE = ` .xll-mod-form-value { color:${COLOR_TEXT}; text-align:right; flex:1; word-break:break-all; } .xll-mod-form-input { flex:1; min-height:40px; border:1px solid ${COLOR_LINE}; border-radius:8px; padding:0 10px; font-size:14px; text-align:right; outline:none; } .xll-mod-form-input:focus { border-color:${XLL_GREEN}; box-shadow:0 0 0 2px ${XLL_GREEN_SOFT}; } +.xll-mod-form-page select.xll-mod-form-input, +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input, +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input[type="date"], +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input[type="datetime-local"], +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input.xll-mod-form-picker, +.xll-mod-form-page .tc-section-form select.xll-mod-form-input, +.xll-mod-form-page select.xll-dv-metric-input, +.xll-mod-form-page input.xll-dv-metric-input[type="datetime-local"] { border:none; background:transparent; border-radius:0; box-shadow:none; padding-right:0; } +.xll-mod-form-page select.xll-mod-form-input:focus, +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input:focus, +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input[type="date"]:focus, +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input[type="datetime-local"]:focus, +.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input.xll-mod-form-picker:focus, +.xll-mod-form-page .tc-section-form select.xll-mod-form-input:focus, +.xll-mod-form-page select.xll-dv-metric-input:focus, +.xll-mod-form-page input.xll-dv-metric-input[type="datetime-local"]:focus { border:none; box-shadow:none; } +.xll-vr-module.xll-mod-form-page select.xll-mod-form-input:focus, +.xll-vr-module.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input[type="date"]:focus, +.xll-vr-module.xll-mod-form-page .xll-mod-form-row input.xll-mod-form-input[type="datetime-local"]:focus, +.xll-vr-module.xll-mod-form-page .tc-section-form select.xll-mod-form-input:focus { border:none; box-shadow:none; } .xll-mod-foot-btns { display:flex; gap:10px; padding:14px; flex-shrink:0; background:${COLOR_BG}; border-top:1px solid ${COLOR_LINE}; } .xll-mod-foot-btns button { flex:1; min-height:48px; border-radius:12px; font-size:15px; font-weight:600; cursor:pointer; border:none; touch-action:manipulation; } .xll-mod-detail-wrap { flex:1; min-height:0; display:flex; flex-direction:column; position:relative; overflow:hidden; } .xll-mod-drawer-types { display:flex; flex-wrap:wrap; gap:8px; } -.xll-mod-drawer-type-btn { min-height:40px; padding:0 14px; border:1px solid ${COLOR_LINE}; border-radius:10px; background:${COLOR_BG}; font-size:13px; color:${COLOR_TEXT_SEC}; cursor:pointer; } +.xll-mod-drawer-type-btn { min-height:40px; padding:0 14px; border:1px solid ${COLOR_LINE}; border-radius:10px; background:${COLOR_BG}; font-size:13px; color:${COLOR_TEXT_SEC}; cursor:pointer; touch-action:manipulation; } .xll-mod-drawer-type-btn.active { border-color:${XLL_GREEN}; color:${XLL_GREEN_DEEP}; background:${XLL_GREEN_SOFT}; font-weight:600; } +.xll-mod-drawer-section { margin-bottom:18px; } +.xll-mod-drawer-section-title { font-size:13px; font-weight:600; color:${COLOR_TEXT}; margin-bottom:10px; } +.xll-mod-drawer-form-card { background:${COLOR_PAGE}; border-radius:12px; padding:0 14px; border:1px solid ${COLOR_LINE}; } +.xll-mod-drawer-form-card .xll-mod-form-row { padding:12px 0; margin:0; } +.xll-mod-drawer-form-card .xll-mod-form-row input.xll-mod-form-input { min-height:36px; border:none; background:transparent; border-radius:0; box-shadow:none; text-align:right; padding:0; } +.xll-mod-drawer-form-card .xll-mod-form-row input.xll-mod-form-input:focus { box-shadow:none; } +.xll-mod-drawer-form-card .xll-mod-form-row input.xll-mod-form-input::placeholder { color:${COLOR_MUTED}; } +.xll-mod-drawer-date-row { display:flex; align-items:center; gap:8px; padding:12px 0; } +.xll-mod-drawer-date-row input { flex:1; min-width:0; min-height:36px; border:none; background:transparent; font-size:14px; text-align:center; outline:none; color:${COLOR_TEXT}; font-family:inherit; } +.xll-mod-drawer-date-sep { font-size:13px; color:${COLOR_MUTED}; flex-shrink:0; } +.xll-mod-drawer-hint { font-size:12px; color:${COLOR_MUTED}; line-height:1.55; margin-top:8px; } +.xll-mod-drawer-actions { display:flex; gap:10px; margin-top:20px; } +.xll-mod-sheet-overlay { position:fixed; inset:0; z-index:200; display:flex; flex-direction:column; justify-content:flex-end; } +.xll-mod-sheet-mask { position:absolute; inset:0; border:none; padding:0; background:rgba(0,0,0,.45); cursor:pointer; } +.xll-mod-sheet-panel { position:relative; z-index:1; width:100%; max-height:min(72vh, 460px); background:${COLOR_BG}; border-radius:16px 16px 0 0; display:flex; flex-direction:column; box-shadow:0 -8px 32px rgba(15,23,42,.12); animation:xll-mod-sheet-up .24s ease; padding-bottom:env(safe-area-inset-bottom,0px); } +@keyframes xll-mod-sheet-up { from { transform:translateY(100%); } to { transform:translateY(0); } } +.xll-mod-sheet-handle { width:36px; height:4px; border-radius:999px; background:${COLOR_LINE}; margin:8px auto 0; flex-shrink:0; } +.xll-mod-sheet-header { position:relative; display:flex; align-items:center; justify-content:center; min-height:48px; padding:8px 48px 10px; border-bottom:1px solid ${COLOR_LINE}; flex-shrink:0; } +.xll-mod-sheet-title { font-size:16px; font-weight:700; color:${COLOR_TEXT}; text-align:center; } +.xll-mod-sheet-close { position:absolute; right:8px; top:50%; transform:translateY(-50%); width:36px; height:36px; border:none; border-radius:999px; background:transparent; color:${COLOR_MUTED}; font-size:22px; line-height:1; cursor:pointer; touch-action:manipulation; } +.xll-mod-sheet-close:active { background:${COLOR_PAGE}; } +.xll-mod-sheet-body { flex:1; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:4px 0 8px; } +.xll-mod-sheet-option { width:100%; display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:52px; padding:12px 16px; border:none; border-bottom:1px solid ${COLOR_LINE}; background:transparent; text-align:left; cursor:pointer; touch-action:manipulation; box-sizing:border-box; } +.xll-mod-sheet-option:last-child { border-bottom:none; } +.xll-mod-sheet-option:active { background:${COLOR_PAGE}; } +.xll-mod-sheet-option.active { background:${XLL_GREEN_SOFT}; } +.xll-mod-sheet-option-text { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; } +.xll-mod-sheet-option-main { font-size:15px; font-weight:600; color:${COLOR_TEXT}; line-height:1.35; } +.xll-mod-sheet-option-sub { font-size:12px; color:${COLOR_MUTED}; line-height:1.4; } +.xll-mod-sheet-option-check { flex-shrink:0; width:22px; height:22px; border-radius:999px; background:${XLL_GREEN}; color:#fff; font-size:13px; font-weight:700; display:inline-flex; align-items:center; justify-content:center; } +.xll-mod-sheet-panel--filter { max-height:min(88vh, 580px); padding-bottom:0; } +.xll-mod-sheet-scroll { flex:1; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:12px 16px 8px; } +.xll-mod-sheet-footer { flex-shrink:0; display:flex; gap:10px; padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px)); border-top:1px solid ${COLOR_LINE}; background:${COLOR_BG}; } +.xll-mod-sheet-footer button { flex:1; min-height:44px; border-radius:12px; font-size:15px; font-weight:600; touch-action:manipulation; } .xll-mod-timeline { padding-left:4px; } .xll-mod-step { display:flex; gap:12px; margin-bottom:14px; } .xll-mod-step-dot { width:22px; height:22px; border-radius:50%; background:${COLOR_LINE}; color:#fff; font-size:12px; display:flex; align-items:center; justify-content:center; flex-shrink:0; } @@ -245,6 +299,123 @@ const PAGE_STYLE = ` .xll-dv-step { flex-shrink:0; font-size:11px; padding:5px 10px; border-radius:999px; background:${COLOR_PAGE}; color:${COLOR_MUTED}; border:1px solid transparent; } .xll-dv-step.active { background:${XLL_GREEN_SOFT}; color:${XLL_GREEN_DEEP}; border-color:rgba(122,185,41,.35); font-weight:600; } .xll-dv-step.done { color:${COLOR_SUCCESS}; background:rgba(0,180,42,.08); } +.xll-dv-step-nav { display:flex; align-items:center; gap:0; } +.xll-dv-step-nav-item { flex:1; min-width:0; display:flex; flex-direction:column; align-items:center; gap:4px; padding:4px 2px; border:none; background:transparent; cursor:pointer; touch-action:manipulation; } +.xll-dv-step-nav-item:active { opacity:.78; } +.xll-dv-step-nav-index { width:22px; height:22px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:${COLOR_MUTED}; background:${COLOR_PAGE}; border:1px solid ${COLOR_LINE}; } +.xll-dv-step-nav-item.active .xll-dv-step-nav-index { color:#fff; background:${XLL_GREEN}; border-color:${XLL_GREEN}; } +.xll-dv-step-nav-item.done .xll-dv-step-nav-index { color:${COLOR_SUCCESS}; background:rgba(0,180,42,.1); border-color:rgba(0,180,42,.35); } +.xll-dv-step-nav-label { font-size:11px; color:${COLOR_MUTED}; line-height:1.3; text-align:center; white-space:nowrap; } +.xll-dv-step-nav-item.active .xll-dv-step-nav-label { color:${XLL_GREEN_DEEP}; font-weight:600; } +.xll-dv-step-nav-connector { flex:0 0 24px; height:2px; background:${COLOR_LINE}; margin-top:-14px; } +.xll-dv-step-nav-item.done + .xll-dv-step-nav-connector, .xll-dv-step-nav-connector.done { background:rgba(0,180,42,.35); } +.xll-dv-metric-unit-wrap { display:flex; align-items:stretch; gap:0; } +.xll-dv-metric-unit-wrap .xll-dv-metric-input { flex:1; border-top-right-radius:0; border-bottom-right-radius:0; } +.xll-dv-metric-unit-suffix { flex-shrink:0; min-width:44px; display:flex; align-items:center; justify-content:center; padding:0 10px; border:1px solid ${COLOR_LINE}; border-left:none; border-radius:0 10px 10px 0; background:${COLOR_PAGE}; font-size:13px; font-weight:600; color:${COLOR_TEXT_SEC}; } +.xll-dv-metric-input[type="number"] { -moz-appearance:textfield; appearance:textfield; } +.xll-dv-metric-input[type="number"]::-webkit-outer-spin-button, .xll-dv-metric-input[type="number"]::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; } +.xll-dv-inspection-block { margin:0 14px 12px; border:1px solid ${COLOR_LINE}; border-radius:12px; overflow:hidden; background:${COLOR_BG}; } +.xll-dv-inspection-cat { padding:8px 12px; font-size:12px; font-weight:700; color:${COLOR_TEXT}; background:${COLOR_PAGE}; border-bottom:1px solid ${COLOR_LINE}; } +.xll-dv-inspection-row { display:flex; align-items:center; gap:10px; min-height:44px; padding:8px 12px; border-bottom:1px solid ${COLOR_LINE}; font-size:13px; } +.xll-dv-inspection-row:last-child { border-bottom:none; } +.xll-dv-inspection-item { flex:1; min-width:0; color:${COLOR_TEXT}; line-height:1.35; } +.xll-dv-inspection-status { flex-shrink:0; font-size:12px; font-weight:600; color:${COLOR_SUCCESS}; } +.xll-dv-inspection-status.off { color:${COLOR_DANGER}; } +.xll-dv-inspection-tread { width:64px; min-height:32px; border:1px solid ${COLOR_LINE}; border-radius:8px; padding:0 8px; font-size:13px; text-align:right; outline:none; background:#fff; } +.xll-dv-inspection-switch { width:44px; height:26px; border-radius:999px; border:none; background:${COLOR_LINE}; position:relative; cursor:pointer; flex-shrink:0; touch-action:manipulation; } +.xll-dv-inspection-switch.on { background:${XLL_GREEN}; } +.xll-dv-inspection-switch::after { content:''; position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.15); transition:transform .15s ease; } +.xll-dv-inspection-switch.on::after { transform:translateX(18px); } +.xll-dv-inspection-controls { display:flex; align-items:center; gap:8px; flex-shrink:0; } +.xll-dv-inspection-remark { width:76px; min-height:32px; border:1px solid ${COLOR_LINE}; border-radius:8px; padding:0 8px; font-size:12px; text-align:right; outline:none; background:#fff; color:${COLOR_TEXT}; } +.xll-dv-inspection-remark::placeholder { color:${COLOR_MUTED}; } +.xll-dv-inspection-remark:focus { border-color:${XLL_GREEN}; box-shadow:0 0 0 2px ${XLL_GREEN_SOFT}; } +.xll-dv-inspection-remark-read { flex-shrink:0; max-width:88px; font-size:12px; color:${COLOR_TEXT_SEC}; text-align:right; word-break:break-all; } +.xll-dv-photo-capture-scroll { background:linear-gradient(180deg, ${XLL_GREEN_SOFT} 0%, ${COLOR_PAGE} 42%, ${COLOR_BG} 100%); } +.xll-dv-photo-capture-page { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:28px 20px 32px; min-height:min(58vh,420px); text-align:center; } +.xll-dv-photo-capture-card { width:100%; max-width:340px; padding:28px 22px 24px; border-radius:20px; background:${COLOR_BG}; border:1px solid rgba(122,185,41,.18); box-shadow:0 12px 40px rgba(29,33,41,.08), 0 2px 8px rgba(122,185,41,.08); } +.xll-dv-photo-capture-icon { width:72px; height:72px; margin:0 auto 18px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:linear-gradient(145deg, ${XLL_GREEN_SOFT}, rgba(122,185,41,.22)); color:${XLL_GREEN_DEEP}; box-shadow:inset 0 0 0 1px rgba(122,185,41,.2); } +.xll-dv-photo-capture-icon svg { width:34px; height:34px; display:block; } +.xll-dv-photo-capture-title { margin:0 0 8px; font-size:20px; font-weight:700; color:${COLOR_TEXT}; line-height:1.4; letter-spacing:.02em; } +.xll-dv-photo-capture-sub { margin:0 0 20px; font-size:13px; color:${COLOR_TEXT_SEC}; line-height:1.6; } +.xll-dv-photo-capture-resume { margin-bottom:18px; padding:12px 14px; border-radius:12px; background:${COLOR_PAGE}; border:1px solid ${COLOR_LINE}; text-align:left; } +.xll-dv-photo-capture-progress-bar { height:6px; border-radius:999px; background:${COLOR_LINE}; overflow:hidden; margin-bottom:8px; } +.xll-dv-photo-capture-progress-bar > span { display:block; height:100%; border-radius:999px; background:linear-gradient(90deg, ${XLL_GREEN}, ${XLL_GREEN_DEEP}); transition:width .35s ease; } +.xll-dv-photo-capture-resume-text { font-size:12px; color:${COLOR_TEXT_SEC}; line-height:1.5; } +.xll-dv-photo-capture-resume-text strong { color:${XLL_GREEN_DEEP}; font-weight:700; } +.xll-dv-photo-capture-tips { margin:0; padding:0; list-style:none; text-align:left; } +.xll-dv-photo-capture-tips li { position:relative; padding-left:18px; font-size:12px; color:${COLOR_MUTED}; line-height:1.65; } +.xll-dv-photo-capture-tips li + li { margin-top:6px; } +.xll-dv-photo-capture-tips li::before { content:''; position:absolute; left:0; top:8px; width:6px; height:6px; border-radius:50%; background:${XLL_GREEN}; opacity:.75; } +.xll-dv-photo-capture-badge { display:inline-flex; align-items:center; min-height:24px; padding:0 10px; margin-bottom:14px; border-radius:999px; font-size:11px; font-weight:700; letter-spacing:.04em; } +.xll-dv-photo-capture-badge--body { color:${XLL_GREEN_DEEP}; background:${XLL_GREEN_SOFT}; border:1px solid rgba(122,185,41,.28); } +.xll-dv-photo-capture-badge--chassis { color:#0E7490; background:rgba(14,116,144,.1); border:1px solid rgba(14,116,144,.22); } +.xll-dv-photo-capture-badge--tire { color:#C2410C; background:rgba(194,65,12,.1); border:1px solid rgba(194,65,12,.2); } +.xll-dv-photo-capture-soon { margin:0 0 16px; font-size:14px; font-weight:600; color:${COLOR_TEXT_SEC}; letter-spacing:.12em; } +.xll-dv-photo-capture-countdown-ring { position:relative; width:108px; height:108px; margin:0 auto 18px; display:flex; align-items:center; justify-content:center; } +.xll-dv-photo-capture-countdown-ring::before { content:''; position:absolute; inset:0; border-radius:50%; background:conic-gradient(${XLL_GREEN} 0deg, ${XLL_GREEN_SOFT} 280deg, ${COLOR_LINE} 280deg); animation:xllPhotoRingSpin 3s linear infinite; } +.xll-dv-photo-capture-countdown-ring::after { content:''; position:absolute; inset:6px; border-radius:50%; background:${COLOR_BG}; box-shadow:inset 0 2px 8px rgba(29,33,41,.06); } +.xll-dv-photo-capture-countdown-num { position:relative; z-index:1; font-size:44px; font-weight:800; color:${XLL_GREEN_DEEP}; line-height:1; font-variant-numeric:tabular-nums; animation:xllPhotoCountPop .55s ease; } +.xll-dv-photo-capture-target { margin:0 0 10px; font-size:22px; font-weight:700; color:${COLOR_TEXT}; line-height:1.35; } +.xll-dv-photo-capture-countdown-tip { margin:0 0 16px; font-size:12px; color:${COLOR_MUTED}; line-height:1.5; } +.xll-dv-photo-capture-step-pill { display:inline-flex; align-items:center; min-height:30px; padding:0 14px; border-radius:999px; background:${COLOR_PAGE}; border:1px solid ${COLOR_LINE}; font-size:12px; font-weight:600; color:${COLOR_TEXT_SEC}; } +.xll-dv-photo-capture-step-pill em { font-style:normal; color:${XLL_GREEN_DEEP}; font-weight:700; } +@keyframes xllPhotoCountPop { 0% { transform:scale(.72); opacity:.35; } 55% { transform:scale(1.08); } 100% { transform:scale(1); opacity:1; } } +@keyframes xllPhotoRingSpin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } } +.xll-dv-photo-done-list { margin:0 14px 12px; border:1px solid ${COLOR_LINE}; border-radius:12px; overflow:hidden; background:${COLOR_BG}; } +.xll-dv-photo-done-row { display:flex; align-items:center; justify-content:space-between; gap:10px; min-height:44px; padding:10px 12px; border-bottom:1px solid ${COLOR_LINE}; font-size:13px; } +.xll-dv-photo-done-row:last-child { border-bottom:none; } +.xll-dv-photo-done-ok { font-size:12px; font-weight:600; color:${COLOR_SUCCESS}; flex-shrink:0; } +.xll-mod-action-bar.xll-dv-photo-action-bar { padding-left:20px; padding-right:20px; } +.xll-mod-action-bar.xll-dv-photo-action-bar .xll-dv-photo-action-btn { flex:1; min-height:48px; border-radius:24px; font-size:16px; font-weight:600; } +.xll-dv-photo-reshoot-bar .tc-section-head { display:none; } +.xll-dv-photo-thumb { position:relative; aspect-ratio:1; border-radius:10px; overflow:hidden; border:1px solid ${COLOR_LINE}; background:${COLOR_PAGE}; cursor:pointer; touch-action:manipulation; } +.xll-dv-photo-thumb--empty { cursor:default; } +.xll-dv-photo-thumb:active:not(.xll-dv-photo-thumb--empty) { opacity:.92; } +.xll-dv-photo-thumb img { width:100%; height:100%; object-fit:cover; display:block; } +.xll-dv-photo-thumb-placeholder { width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:600; color:${XLL_GREEN_DEEP}; background:${XLL_GREEN_SOFT}; } +.xll-dv-photo-thumb-placeholder--reshoot { color:${COLOR_WARN}; background:rgba(255,125,0,.08); box-shadow:inset 0 0 0 1px rgba(255,125,0,.35); } +.xll-dv-photo-thumb-del { position:absolute; top:6px; right:6px; width:22px; height:22px; border-radius:50%; border:1.5px solid rgba(255,255,255,.9); background:rgba(245,63,63,.94); color:#fff; font-size:13px; font-weight:700; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center; box-shadow:0 2px 8px rgba(15,23,42,.22); z-index:2; touch-action:manipulation; padding:0; } +.xll-dv-photo-thumb-del:active { transform:scale(.94); background:rgba(220,38,38,.98); } +.xll-dv-photo-thumb-label { margin-top:8px; font-size:11px; color:${COLOR_TEXT_SEC}; line-height:1.35; text-align:center; } +.xll-dv-photo-thumb-tread { margin-top:3px; font-size:10px; color:${XLL_GREEN_DEEP}; text-align:center; font-weight:600; } +.xll-dv-photo-viewer { position:absolute; inset:0; z-index:80; display:flex; flex-direction:column; background:#0f1419; color:#fff; } +.xll-dv-photo-viewer-top { flex-shrink:0; display:flex; align-items:flex-start; justify-content:space-between; gap:10px; padding:12px 14px calc(10px + env(safe-area-inset-top,0px)); background:linear-gradient(180deg,rgba(0,0,0,.72),rgba(0,0,0,.2)); } +.xll-dv-photo-viewer-close { flex-shrink:0; min-height:32px; padding:0 12px; border-radius:999px; border:1px solid rgba(255,255,255,.22); background:rgba(255,255,255,.1); color:#fff; font-size:13px; font-weight:600; cursor:pointer; } +.xll-dv-photo-viewer-meta { flex:1; min-width:0; text-align:center; } +.xll-dv-photo-viewer-cat { font-size:11px; color:rgba(255,255,255,.65); margin-bottom:2px; } +.xll-dv-photo-viewer-title { font-size:15px; font-weight:700; line-height:1.35; } +.xll-dv-photo-viewer-counter { flex-shrink:0; min-height:28px; padding:0 10px; border-radius:999px; background:rgba(255,255,255,.12); font-size:12px; font-weight:700; display:flex; align-items:center; } +.xll-dv-photo-viewer-stage { flex:1; min-height:0; display:flex; align-items:center; justify-content:center; gap:8px; padding:0 8px; touch-action:pan-y; } +.xll-dv-photo-viewer-img-wrap { flex:1; min-width:0; height:100%; display:flex; align-items:center; justify-content:center; } +.xll-dv-photo-viewer-img-wrap img { max-width:100%; max-height:100%; object-fit:contain; display:block; border-radius:8px; } +.xll-dv-photo-viewer-nav { flex-shrink:0; width:40px; height:40px; border-radius:50%; border:1px solid rgba(255,255,255,.22); background:rgba(255,255,255,.1); color:#fff; font-size:24px; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center; touch-action:manipulation; } +.xll-dv-photo-viewer-nav:disabled { opacity:.28; cursor:not-allowed; } +.xll-dv-photo-viewer-nav:not(:disabled):active { background:rgba(255,255,255,.2); } +.xll-dv-photo-viewer-foot { flex-shrink:0; padding:12px 16px calc(14px + env(safe-area-inset-bottom,0px)); text-align:center; background:rgba(0,0,0,.45); font-size:13px; color:rgba(255,255,255,.85); } +.xll-dv-photo-viewer-tread { font-weight:700; color:#86efac; } +.xll-dv-photo-viewer-hint { margin-top:4px; font-size:11px; color:rgba(255,255,255,.45); } +.xll-dv-photo-camera-overlay { position:absolute; inset:0; z-index:70; display:flex; flex-direction:column; background:#1a1f2e; color:#fff; } +.xll-dv-photo-camera-top { flex-shrink:0; padding:12px 16px; text-align:center; font-size:14px; font-weight:600; background:rgba(0,0,0,.35); } +.xll-dv-photo-camera-view { flex:1; min-height:0; position:relative; overflow:hidden; background:#2d3748; } +.xll-dv-photo-camera-view img { width:100%; height:100%; object-fit:cover; display:block; } +.xll-dv-photo-camera-placeholder { font-size:16px; color:rgba(255,255,255,.55); } +.xll-dv-camera-viewfinder { position:relative; width:100%; height:100%; overflow:hidden; background:#2d3748; touch-action:none; cursor:crosshair; } +.xll-dv-camera-preview { width:100%; height:100%; display:flex; align-items:center; justify-content:center; transition:transform .22s ease; will-change:transform; } +.xll-dv-camera-preview img { width:100%; height:100%; object-fit:cover; display:block; pointer-events:none; user-select:none; } +.xll-dv-camera-focus-ring { position:absolute; z-index:2; width:56px; height:56px; margin:-28px 0 0 -28px; border:2px solid #fff; border-radius:4px; box-shadow:0 0 0 1px rgba(0,0,0,.35); pointer-events:none; animation:xll-dv-focus-pulse .35s ease; } +@keyframes xll-dv-focus-pulse { from { transform:scale(1.12); opacity:.55; } to { transform:scale(1); opacity:1; } } +.xll-dv-camera-zoom { position:absolute; z-index:3; right:12px; top:50%; transform:translateY(-50%); display:flex; flex-direction:column; align-items:center; gap:6px; padding:8px 6px; border-radius:12px; background:rgba(0,0,0,.45); } +.xll-dv-camera-zoom-btn { width:32px; height:32px; border:none; border-radius:8px; background:rgba(255,255,255,.16); color:#fff; font-size:18px; font-weight:700; line-height:1; cursor:pointer; touch-action:manipulation; } +.xll-dv-camera-zoom-btn:active { background:rgba(255,255,255,.28); } +.xll-dv-camera-zoom-val { font-size:11px; font-weight:700; color:#fff; min-width:32px; text-align:center; font-variant-numeric:tabular-nums; } +.xll-dv-camera-focus-tip { position:absolute; z-index:3; left:50%; bottom:14px; transform:translateX(-50%); padding:4px 10px; border-radius:999px; background:rgba(0,0,0,.45); color:rgba(255,255,255,.88); font-size:11px; pointer-events:none; white-space:nowrap; } +.xll-dv-photo-camera-tread { display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:52px; padding:0 16px; background:${COLOR_BG}; color:${COLOR_TEXT}; border-top:1px solid ${COLOR_LINE}; } +.xll-dv-photo-camera-tread-label { font-size:15px; color:${COLOR_MUTED}; flex-shrink:0; } +.xll-dv-photo-camera-actions { display:flex; gap:10px; padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px)); background:${COLOR_BG}; border-top:1px solid ${COLOR_LINE}; } +.xll-dv-photo-album-btn { flex:0 0 auto; min-width:88px; min-height:44px; padding:0 12px; border:1px solid ${COLOR_LINE}; border-radius:10px; background:#fff; color:${COLOR_TEXT_SEC}; font-size:14px; font-weight:600; cursor:pointer; touch-action:manipulation; } +.xll-dv-photo-album-btn:active { opacity:.88; } +.xll-dv-photo-camera-skip { flex:0 0 auto; min-width:88px; min-height:44px; padding:0 12px; border:1px solid ${COLOR_LINE}; border-radius:10px; background:#fff; color:${COLOR_TEXT_SEC}; font-size:14px; font-weight:600; cursor:pointer; } .xll-dv-status { display:inline-flex; font-size:11px; font-weight:600; padding:2px 8px; border-radius:999px; margin-left:8px; vertical-align:middle; } .xll-dv-status.neutral { color:${COLOR_TEXT_SEC}; background:${COLOR_PAGE}; } .xll-dv-status.warn { color:${COLOR_WARN}; background:rgba(255,125,0,.12); } @@ -257,21 +428,232 @@ const PAGE_STYLE = ` .xll-dv-photo-block { margin-bottom:14px; } .xll-dv-photo-title { font-size:13px; font-weight:600; color:${COLOR_TEXT}; margin-bottom:8px; } .xll-dv-photo-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; } -.xll-dv-photo-slot { aspect-ratio:1; border-radius:8px; border:1px dashed ${COLOR_LINE}; background:${COLOR_PAGE}; display:flex; align-items:center; justify-content:center; font-size:11px; color:${COLOR_MUTED}; text-align:center; padding:4px; cursor:pointer; touch-action:manipulation; } +.xll-dv-photo-slot { aspect-ratio:1; border-radius:10px; border:1px dashed ${COLOR_LINE}; background:${COLOR_PAGE}; display:flex; align-items:center; justify-content:center; font-size:11px; color:${COLOR_MUTED}; text-align:center; padding:4px; cursor:pointer; touch-action:manipulation; } .xll-dv-photo-slot:active { background:${XLL_GREEN_SOFT}; border-color:${XLL_GREEN}; } .xll-dv-view-val { color:#000 !important; } .xll-dv-module .tc-section-form { padding:0 14px 14px; } .xll-dv-module .tc-section-form .xll-mod-form-row { padding:10px 0; } .xll-dv-module .tc-section-form .xll-mod-form-row:last-child { border-bottom:none; } .xll-dv-module .tc-section-hint { padding:0 14px 12px; font-size:12px; color:${COLOR_MUTED}; line-height:1.55; } -.xll-dv-module .xll-dv-photo-block { margin-bottom:0; padding:0 14px 14px; } -.xll-dv-filter-field { margin-bottom:14px; } -.xll-dv-filter-label { display:block; font-size:13px; font-weight:600; color:${COLOR_TEXT}; margin-bottom:8px; } -.xll-dv-filter-input { width:100%; min-height:44px; border:1px solid ${COLOR_LINE}; border-radius:10px; padding:0 12px; font-size:14px; box-sizing:border-box; outline:none; background:${COLOR_BG}; color:${COLOR_TEXT}; } -.xll-dv-filter-input:focus { border-color:${XLL_GREEN}; box-shadow:0 0 0 2px ${XLL_GREEN_SOFT}; } -.xll-dv-filter-date-row { display:flex; align-items:center; gap:8px; } -.xll-dv-filter-date-row .xll-dv-filter-input { flex:1; min-width:0; } -.xll-dv-filter-hint { font-size:12px; color:${COLOR_MUTED}; line-height:1.55; margin-top:8px; } +.xll-dv-module .xll-dv-photo-block { margin-bottom:0; padding:12px 14px 14px; } +.xll-dv-step-label { font-size:13px; font-weight:700; color:${COLOR_TEXT}; margin-bottom:10px; } +.xll-dv-vehicle-picker { width:100%; min-height:44px; display:flex; align-items:center; justify-content:space-between; gap:10px; padding:0; border:none; border-radius:0; background:transparent; font-size:15px; color:${COLOR_TEXT}; cursor:pointer; touch-action:manipulation; text-align:left; } +.xll-dv-vehicle-picker:active { opacity:.72; } +.xll-dv-vehicle-picker.placeholder { color:${COLOR_MUTED}; } +.xll-dv-vehicle-picker-chevron { flex-shrink:0; color:${COLOR_MUTED}; font-size:18px; line-height:1; } +.xll-dv-pick-toolbar { flex-shrink:0; padding:10px 14px 0; background:${COLOR_BG}; } +.xll-dv-pick-parking { display:flex; align-items:center; gap:8px; margin-top:10px; width:100%; padding:10px 12px; border-radius:10px; background:${COLOR_PAGE}; border:1px solid ${COLOR_LINE}; font-size:13px; color:${COLOR_TEXT_SEC}; cursor:pointer; touch-action:manipulation; box-sizing:border-box; } +.xll-dv-pick-parking:active { background:${XLL_GREEN_SOFT}; border-color:${XLL_GREEN}; } +.xll-dv-pick-parking-label { flex-shrink:0; font-size:12px; color:${COLOR_MUTED}; } +.xll-dv-pick-parking strong { flex:1; min-width:0; color:${COLOR_TEXT}; font-weight:600; font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.xll-dv-pick-parking-arrow { flex-shrink:0; color:${COLOR_MUTED}; font-size:12px; } +.xll-dv-pick-list { flex:1; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:12px 14px calc(16px + env(safe-area-inset-bottom,0px)); } +.xll-dv-pick-card { margin-bottom:12px; opacity:1; } +.xll-dv-pick-card.blocked { opacity:.72; } +.xll-dv-pick-card.blocked .xll-mod-card { border-color:#FECACA; background:linear-gradient(180deg,#fff 0%,#FFF5F5 100%); } +.xll-dv-readiness-bar { display:flex; align-items:center; min-height:32px; padding:6px 12px; margin:8px 0 0; border-radius:8px; font-size:12px; font-weight:600; line-height:1.4; } +.xll-dv-readiness-bar.ready { color:#047857; background:rgba(0,180,42,.1); } +.xll-dv-readiness-bar.blocked { color:#DC2626; background:rgba(220,38,38,.08); } +.xll-dv-section-badge { display:inline-flex; align-items:center; justify-content:center; width:22px; height:22px; border-radius:8px; background:${XLL_GREEN_SOFT}; color:${XLL_GREEN_DEEP}; font-size:12px; font-weight:800; flex-shrink:0; margin-right:8px; } +.xll-dv-required-tag { display:inline-flex; align-items:center; margin-left:6px; padding:1px 6px; border-radius:4px; font-size:10px; font-weight:600; color:#E11D48; background:rgba(244,63,94,.1); vertical-align:middle; line-height:1.4; } +.xll-dv-section-head-title { display:flex; align-items:center; font-size:15px; font-weight:700; color:${COLOR_TEXT}; } +.xll-dv-section-actions { display:flex; align-items:center; gap:6px; flex-shrink:0; } +.xll-dv-section-action-btn { min-height:28px; padding:0 10px; border-radius:999px; font-size:12px; font-weight:600; cursor:pointer; touch-action:manipulation; white-space:nowrap; } +.xll-dv-section-action-btn--primary { border:none; background:${XLL_GREEN_SOFT}; color:${XLL_GREEN_DEEP}; } +.xll-dv-section-action-btn--primary:active { opacity:.82; } +.xll-dv-section-action-btn--ghost { border:1px solid ${COLOR_LINE}; background:${COLOR_PAGE}; color:${COLOR_TEXT_SEC}; } +.xll-dv-section-action-btn--ghost:active { opacity:.82; } +.xll-dv-validate-overlay { position:absolute; inset:0; z-index:50; display:flex; align-items:center; justify-content:center; padding:24px; box-sizing:border-box; } +.xll-dv-validate-mask { position:absolute; inset:0; background:rgba(0,0,0,.45); border:none; padding:0; cursor:pointer; } +.xll-dv-validate-card { position:relative; z-index:1; width:100%; max-width:320px; background:${COLOR_BG}; border-radius:14px; padding:18px 16px 14px; box-shadow:0 12px 40px rgba(15,23,42,.2); animation:xll-dv-validate-in .22s ease; } +@keyframes xll-dv-validate-in { from { opacity:0; transform:scale(.96); } to { opacity:1; transform:scale(1); } } +.xll-dv-validate-title { font-size:16px; font-weight:700; color:${COLOR_TEXT}; margin-bottom:6px; } +.xll-dv-validate-plate { font-size:13px; color:${COLOR_MUTED}; margin-bottom:12px; } +.xll-dv-validate-list { margin:0; padding:0 0 0 18px; font-size:14px; color:${COLOR_TEXT_SEC}; line-height:1.65; } +.xll-dv-validate-list li { margin-bottom:8px; } +.xll-dv-validate-list li:last-child { margin-bottom:0; } +.xll-dv-validate-ok { width:100%; min-height:44px; margin-top:16px; border:none; border-radius:12px; background:${XLL_GREEN}; color:#fff; font-size:15px; font-weight:600; cursor:pointer; touch-action:manipulation; } +.xll-dv-validate-ok:active { opacity:.88; } +.xll-dv-confirm-actions { display:flex; gap:10px; margin-top:16px; } +.xll-dv-confirm-cancel, .xll-dv-confirm-ok { flex:1; min-height:44px; border-radius:12px; font-size:15px; font-weight:600; cursor:pointer; touch-action:manipulation; } +.xll-dv-confirm-cancel { border:1px solid ${COLOR_LINE}; background:${COLOR_PAGE}; color:${COLOR_TEXT_SEC}; } +.xll-dv-confirm-cancel:active { opacity:.82; } +.xll-dv-confirm-ok { border:none; background:${XLL_GREEN}; color:#fff; } +.xll-dv-confirm-ok:active { opacity:.88; } +.xll-dv-equip-block { padding:0 14px 12px; } +.xll-dv-equip-row { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:10px 0; border-bottom:1px solid ${COLOR_LINE}; font-size:14px; } +.xll-dv-equip-row:last-child { border-bottom:none; } +.xll-dv-equip-label { color:${COLOR_MUTED}; flex-shrink:0; min-width:108px; line-height:1.45; } +.xll-dv-equip-val { flex:1; text-align:right; color:${COLOR_TEXT}; line-height:1.45; } +.xll-dv-equip-val strong { font-weight:700; } +.xll-dv-equip-switch-wrap { display:flex; align-items:center; justify-content:flex-end; gap:8px; flex:1; min-width:0; } +.xll-dv-equip-switch-label { font-size:13px; color:${COLOR_TEXT_SEC}; font-weight:600; min-width:16px; text-align:right; } +.xll-dv-equip-switch { position:relative; width:44px; height:26px; border-radius:999px; border:none; background:${COLOR_LINE}; cursor:pointer; padding:0; flex-shrink:0; transition:background .2s; touch-action:manipulation; } +.xll-dv-equip-switch.on { background:${XLL_GREEN}; } +.xll-dv-equip-switch:disabled { opacity:.55; cursor:default; } +.xll-dv-equip-switch::after { content:''; position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:#fff; transition:transform .2s; box-shadow:0 1px 3px rgba(0,0,0,.2); } +.xll-dv-equip-switch.on::after { transform:translateX(18px); } +.xll-dv-equip-photo-status { display:block; font-size:12px; color:${COLOR_MUTED}; margin-top:4px; } +.xll-dv-equip-photo-status.done { color:${COLOR_SUCCESS}; } +.xll-dv-equip-photo-status.pending { color:${COLOR_WARN}; } +.xll-dv-equip-photo-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin-top:10px; } +.xll-dv-equip-photo-item { min-width:0; } +.xll-dv-equip-photo-item-label { font-size:11px; color:${COLOR_MUTED}; margin-bottom:6px; } +.xll-dv-equip-photo-slot { aspect-ratio:4/3; border-radius:8px; border:1px dashed ${COLOR_LINE}; background:${COLOR_PAGE}; display:flex; align-items:center; justify-content:center; font-size:11px; color:${COLOR_MUTED}; text-align:center; padding:4px; cursor:pointer; touch-action:manipulation; } +.xll-dv-equip-photo-slot.done { border-style:solid; border-color:rgba(122,185,41,.35); color:${XLL_GREEN_DEEP}; background:${XLL_GREEN_SOFT}; } +.xll-dv-equip-photo-slot:active { opacity:.82; } +.xll-dv-spare-tire-block { margin-top:10px; } +.xll-dv-spare-tire-hint { font-size:11px; color:${COLOR_MUTED}; line-height:1.55; margin-bottom:8px; } +.xll-dv-spare-tread-field { margin-top:10px; } +.xll-dv-spare-tread-label { font-size:11px; color:${COLOR_MUTED}; margin-bottom:6px; display:flex; align-items:center; justify-content:space-between; gap:8px; } +.xll-dv-spare-tread-ocr-tag { font-size:10px; color:${XLL_GREEN_DEEP}; background:${XLL_GREEN_SOFT}; padding:2px 6px; border-radius:4px; font-weight:600; white-space:nowrap; } +.xll-dv-spare-tread-input { width:100%; min-height:40px; border:1px solid ${COLOR_LINE}; border-radius:8px; padding:0 12px; font-size:15px; color:${COLOR_TEXT}; background:#fff; outline:none; box-sizing:border-box; } +.xll-dv-spare-tread-input:focus { border-color:${XLL_GREEN}; box-shadow:0 0 0 2px rgba(122,185,41,.15); } +.xll-dv-spare-tread-input:disabled { background:${COLOR_PAGE}; color:${COLOR_TEXT_SEC}; } +.xll-dv-spare-photo-result { margin-top:10px; border-radius:10px; overflow:hidden; border:1px solid ${COLOR_LINE}; cursor:pointer; touch-action:manipulation; } +.xll-dv-spare-photo-preview { aspect-ratio:4/3; background:#2d3748; display:flex; align-items:center; justify-content:center; overflow:hidden; } +.xll-dv-spare-photo-preview img { width:100%; height:100%; object-fit:cover; display:block; } +.xll-dv-spare-photo-preview-placeholder { font-size:15px; color:rgba(255,255,255,.72); font-weight:500; } +.xll-dv-spare-tread-row { display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:44px; padding:0 14px; background:${COLOR_BG}; border-top:1px solid ${COLOR_LINE}; } +.xll-dv-spare-tread-row-label { font-size:14px; color:${COLOR_MUTED}; flex-shrink:0; } +.xll-dv-spare-tread-row-val { font-size:15px; color:${COLOR_TEXT}; font-weight:600; font-variant-numeric:tabular-nums; } +.xll-dv-spare-capture { position:absolute; inset:0; z-index:60; display:flex; flex-direction:column; background:#1a1f2e; color:#fff; } +.xll-dv-spare-capture-photo { flex:1; min-height:0; display:flex; align-items:center; justify-content:center; background:#2d3748; margin:0; overflow:hidden; } +.xll-dv-spare-capture-photo img { width:100%; height:100%; object-fit:cover; display:block; } +.xll-dv-spare-capture-photo-placeholder { font-size:18px; color:rgba(255,255,255,.55); font-weight:500; } +.xll-dv-spare-capture-tread { display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:52px; padding:0 16px; background:${COLOR_BG}; border-top:1px solid ${COLOR_LINE}; } +.xll-dv-spare-capture-tread-label { font-size:15px; color:${COLOR_MUTED}; flex-shrink:0; } +.xll-dv-spare-capture-tread-input-wrap { display:flex; align-items:center; gap:6px; flex:1; justify-content:flex-end; min-width:0; } +.xll-dv-spare-capture-tread-input { width:72px; min-height:36px; border:none; background:transparent; font-size:18px; font-weight:700; color:${COLOR_TEXT}; text-align:right; outline:none; font-variant-numeric:tabular-nums; } +.xll-dv-spare-capture-tread-unit { font-size:15px; color:${COLOR_MUTED}; flex-shrink:0; } +.xll-dv-spare-capture-actions { display:flex; gap:10px; padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px)); background:${COLOR_BG}; border-top:1px solid ${COLOR_LINE}; } +.xll-dv-spare-capture-retake { flex:0 0 auto; min-width:108px; min-height:44px; padding:0 14px; border:1px solid ${XLL_GREEN}; border-radius:10px; background:#fff; color:${XLL_GREEN_DEEP}; font-size:15px; font-weight:600; cursor:pointer; touch-action:manipulation; } +.xll-dv-spare-capture-done { flex:1; min-height:44px; border:none; border-radius:10px; background:${XLL_GREEN}; color:#fff; font-size:15px; font-weight:600; cursor:pointer; touch-action:manipulation; } +.xll-dv-spare-capture-done:active, .xll-dv-spare-capture-retake:active { opacity:.88; } +.xll-dv-training-done-tag { display:inline-flex; align-items:center; min-height:28px; padding:0 10px; border-radius:999px; font-size:12px; font-weight:600; color:${COLOR_SUCCESS}; background:rgba(0,180,42,.1); white-space:nowrap; } +.xll-dv-training-pending-tag { display:inline-flex; align-items:center; min-height:28px; padding:0 10px; border-radius:999px; font-size:12px; font-weight:600; color:${COLOR_WARN}; background:rgba(255,125,0,.12); white-space:nowrap; } +.xll-dv-training-pending-panel { margin:0 14px 14px; padding:14px; border-radius:12px; background:rgba(255,125,0,.06); border:1px solid rgba(255,125,0,.2); } +.xll-dv-training-pending-hint { font-size:13px; color:${COLOR_TEXT_SEC}; line-height:1.55; margin-bottom:12px; } +.xll-dv-training-bound-kv { margin:0 14px 12px; padding:12px; border-radius:10px; background:${COLOR_PAGE}; border:1px solid ${COLOR_LINE}; } +.xll-dv-training-bound-row { display:flex; justify-content:space-between; gap:12px; font-size:13px; line-height:1.5; padding:4px 0; } +.xll-dv-training-bound-label { color:${COLOR_MUTED}; flex-shrink:0; } +.xll-dv-training-bound-val { color:${COLOR_TEXT}; font-weight:600; text-align:right; word-break:break-all; } +.xll-dv-training-cells { margin:0 14px 14px; border-radius:12px; overflow:hidden; background:${COLOR_BG}; border:1px solid ${COLOR_LINE}; } +.xll-dv-training-cell { width:100%; display:flex; align-items:center; gap:12px; min-height:56px; padding:12px 14px; border:none; border-bottom:1px solid ${COLOR_LINE}; background:${COLOR_BG}; cursor:pointer; touch-action:manipulation; text-align:left; box-sizing:border-box; } +.xll-dv-training-cell:last-child { border-bottom:none; } +.xll-dv-training-cell:active { background:${COLOR_PAGE}; } +.xll-dv-training-cell-icon { width:36px; height:36px; border-radius:10px; display:flex; align-items:center; justify-content:center; font-size:18px; flex-shrink:0; } +.xll-dv-training-cell-icon--scan { background:${XLL_GREEN_SOFT}; color:${XLL_GREEN_DEEP}; } +.xll-dv-training-cell-icon--edit { background:rgba(22,93,255,.1); color:#165DFF; } +.xll-dv-training-cell-main { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; } +.xll-dv-training-cell-title { font-size:15px; font-weight:600; color:${COLOR_TEXT}; line-height:1.35; } +.xll-dv-training-cell-desc { font-size:12px; color:${COLOR_MUTED}; line-height:1.4; } +.xll-dv-training-cell-arrow { flex-shrink:0; color:${COLOR_MUTED}; font-size:20px; line-height:1; font-weight:300; } +.xll-dv-training-qr-wrap { padding:0 14px 14px; } +.xll-dv-training-qr-card { padding:20px 16px 18px; border-radius:12px; background:${COLOR_BG}; border:1px solid ${COLOR_LINE}; text-align:center; } +.xll-dv-training-qr-img { width:200px; height:200px; margin:0 auto 14px; display:block; border-radius:8px; border:1px solid ${COLOR_LINE}; background:#fff; object-fit:contain; } +.xll-dv-training-qr-title { font-size:15px; font-weight:600; color:${COLOR_TEXT}; margin-bottom:6px; } +.xll-dv-training-qr-hint { font-size:13px; color:${COLOR_TEXT_SEC}; line-height:1.55; } +.xll-dv-training-qr-wechat { display:inline-flex; align-items:center; gap:4px; margin-top:8px; font-size:12px; color:${COLOR_MUTED}; } +.xll-dv-driver-manual-page .tc-scroll { padding-bottom:calc(88px + env(safe-area-inset-bottom,0px)); } +.xll-dv-driver-manual-photos { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; padding:0 14px 14px; } +.xll-dv-driver-panel { margin:0 14px 14px; padding:12px 14px; border-radius:12px; background:${COLOR_PAGE}; border:1px solid ${COLOR_LINE}; } +.xll-dv-driver-kv { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px 14px; margin-bottom:12px; } +.xll-dv-driver-kv-item.full { grid-column:1 / -1; } +.xll-dv-driver-licenses { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; } +.xll-dv-driver-licenses.cols-3 { grid-template-columns:repeat(3,minmax(0,1fr)); } +.xll-dv-driver-license-item { min-width:0; } +.xll-dv-driver-license-label { font-size:11px; color:${COLOR_MUTED}; margin-bottom:6px; } +.xll-dv-driver-license-thumb { aspect-ratio:4/3; border-radius:8px; border:1px solid ${COLOR_LINE}; background:linear-gradient(135deg,#f8fafc 0%,#eef2f7 100%); display:flex; align-items:center; justify-content:center; font-size:11px; color:${COLOR_TEXT_SEC}; text-align:center; padding:4px; overflow:hidden; } +.xll-dv-driver-license-thumb img { width:100%; height:100%; object-fit:cover; display:block; } +.xll-dv-driver-manual-photo-item { min-width:0; } +.xll-dv-driver-manual-photo-label { font-size:12px; color:${COLOR_MUTED}; margin-bottom:6px; display:flex; align-items:center; gap:6px; } +.xll-dv-driver-manual-photo-slot { aspect-ratio:4/3; border-radius:10px; border:1px dashed ${COLOR_LINE}; background:${COLOR_PAGE}; display:flex; align-items:center; justify-content:center; font-size:12px; color:${COLOR_TEXT_SEC}; cursor:pointer; touch-action:manipulation; overflow:hidden; } +.xll-dv-driver-manual-photo-slot.done { border-style:solid; border-color:${XLL_GREEN}; color:${XLL_GREEN_DEEP}; background:#fff; } +.xll-dv-driver-manual-photo-slot img { width:100%; height:100%; object-fit:cover; display:block; } +.xll-dv-kv-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px 14px; padding:12px 14px 14px; } +.xll-dv-kv-item { min-width:0; } +.xll-dv-kv-label { font-size:11px; color:${COLOR_MUTED}; margin-bottom:3px; } +.xll-dv-kv-val { font-size:13px; color:${COLOR_TEXT}; font-weight:500; line-height:1.4; word-break:break-all; } +.xll-dv-kv-item.full { grid-column:1 / -1; } +.xll-dv-selected-vehicle { margin:0 14px 12px; padding:12px 14px; border-radius:12px; border:1px solid rgba(122,185,41,.35); background:linear-gradient(135deg,#f0fdf4 0%,#fff 100%); } +.xll-dv-vehicle-pick-section .tc-section-head { border-bottom:none; padding:12px 14px 0; } +.xll-dv-vehicle-pick-section:not(.has-vehicle) .tc-section-head { padding-bottom:12px; } +.xll-dv-vehicle-pick-section .xll-dv-selected-vehicle { margin:12px 14px 12px; } +.xll-dv-selected-vehicle-plate { font-size:18px; font-weight:800; color:${COLOR_TEXT}; margin-bottom:4px; } +.xll-dv-selected-vehicle-sub { font-size:12px; color:${COLOR_TEXT_SEC}; line-height:1.45; } +.xll-dv-selected-vehicle-actions { display:flex; gap:8px; margin-top:10px; } +.xll-dv-selected-vehicle-actions button { flex:1; min-height:36px; border-radius:10px; font-size:13px; font-weight:600; cursor:pointer; touch-action:manipulation; } +.xll-dv-chip-group { display:flex; flex-wrap:wrap; gap:8px; padding:0 14px 14px; } +.xll-dv-chip-opt { min-height:36px; padding:0 14px; border:1px solid ${COLOR_LINE}; border-radius:999px; background:${COLOR_BG}; font-size:13px; color:${COLOR_TEXT_SEC}; cursor:pointer; touch-action:manipulation; } +.xll-dv-chip-opt.active { border-color:${XLL_GREEN}; color:${XLL_GREEN_DEEP}; background:${XLL_GREEN_SOFT}; font-weight:600; } +.xll-dv-metrics-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; padding:12px 14px 14px; } +.xll-dv-metric-field { display:flex; flex-direction:column; gap:6px; } +.xll-dv-metric-field.full { grid-column:1 / -1; } +.xll-dv-metric-label { font-size:12px; font-weight:600; color:${COLOR_MUTED}; } +.xll-dv-metric-input { min-height:44px; border:1px solid ${COLOR_LINE}; border-radius:10px; padding:0 12px; font-size:15px; font-weight:600; color:${COLOR_TEXT}; outline:none; background:${COLOR_BG}; width:100%; box-sizing:border-box; } +.xll-dv-metric-input:focus { border-color:${XLL_GREEN}; box-shadow:0 0 0 2px ${XLL_GREEN_SOFT}; } +.xll-dv-metric-remark { width:100%; min-height:72px; border:1px solid ${COLOR_LINE}; border-radius:10px; padding:10px 12px; font-size:14px; color:${COLOR_TEXT}; resize:vertical; box-sizing:border-box; font-family:inherit; outline:none; background:${COLOR_BG}; } +.xll-dv-metric-remark:focus { border-color:${XLL_GREEN}; box-shadow:0 0 0 2px ${XLL_GREEN_SOFT}; } +.xll-dv-delivery-location { padding:0 14px 14px; } +.xll-dv-delivery-location-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px; } +.xll-dv-delivery-location-label { font-size:12px; font-weight:600; color:${COLOR_MUTED}; } +.xll-dv-delivery-location-plate { font-size:12px; font-weight:600; color:${XLL_GREEN_DEEP}; } +.xll-dv-delivery-map { position:relative; height:220px; border-radius:12px; overflow:hidden; border:1px solid ${COLOR_LINE}; background:#e8f0e4; box-shadow:inset 0 1px 4px rgba(0,0,0,.04); } +.xll-dv-delivery-map-canvas { position:absolute; inset:0; background:linear-gradient(160deg,#dce8d4 0%,#e8f2e0 28%,#d0e0c8 52%,#c5d8bc 78%,#dbe8d2 100%); } +.xll-dv-delivery-map-water { position:absolute; top:8%; right:-6%; width:46%; height:34%; border-radius:42% 58% 60% 40%; background:linear-gradient(135deg,rgba(147,197,253,.55) 0%,rgba(96,165,250,.42) 100%); transform:rotate(-8deg); } +.xll-dv-delivery-map-road { position:absolute; background:rgba(255,255,255,.82); box-shadow:0 0 0 1px rgba(148,163,184,.25); } +.xll-dv-delivery-map-road--h { height:5px; left:-4%; right:-4%; top:46%; transform:rotate(-4deg); } +.xll-dv-delivery-map-road--v { width:5px; top:-6%; bottom:-6%; left:54%; transform:rotate(6deg); } +.xll-dv-delivery-map-road--d { height:4px; width:68%; left:12%; top:28%; transform:rotate(18deg); } +.xll-dv-delivery-map-marker { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:3; display:flex; align-items:center; justify-content:center; } +.xll-dv-delivery-map-marker-pin { width:38px; height:38px; border-radius:50%; background:#2563EB; border:3px solid #fff; box-shadow:0 4px 14px rgba(37,99,235,.38); display:flex; align-items:center; justify-content:center; } +.xll-dv-delivery-map-marker-pin::after { content:''; width:10px; height:10px; border-radius:50%; background:#fff; } +.xll-dv-delivery-map-foot { position:absolute; left:0; right:0; bottom:0; z-index:2; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 10px; background:linear-gradient(180deg,rgba(255,255,255,0) 0%,rgba(255,255,255,.94) 35%,rgba(255,255,255,.98) 100%); font-size:12px; color:${COLOR_TEXT_SEC}; } +.xll-dv-delivery-map-foot strong { color:${COLOR_TEXT}; font-weight:600; } +.xll-dv-delivery-map .xll-map-brand { z-index:2; } +.xll-dv-delivery-map--pending .xll-dv-delivery-map-canvas { opacity:.72; } +.xll-dv-section-action-btn:disabled { opacity:.55; cursor:not-allowed; } +.xll-dv-summary-card { margin:14px 14px 14px; padding:14px; border-radius:12px; background:${COLOR_PAGE}; border:1px dashed ${COLOR_LINE}; font-size:13px; color:${COLOR_TEXT_SEC}; line-height:1.6; } +.xll-dv-borderless-picker { flex:1; min-height:40px; border:none; background:transparent; padding:0; font-size:14px; color:${COLOR_TEXT}; text-align:right; cursor:pointer; touch-action:manipulation; } +.xll-dv-borderless-picker.placeholder { color:${COLOR_MUTED}; } +.xll-dv-borderless-picker:active { opacity:.72; } +.xll-dv-sign-pending-hint { margin:0; padding:10px 12px; border-radius:10px; background:rgba(37,99,235,.08); color:#2563EB; font-size:12px; line-height:1.55; } +.xll-dv-sign-pending-foot { padding:12px 0 0; } +.xll-dv-sign-success-page { display:flex; flex-direction:column; height:100%; min-height:0; background:${COLOR_BG}; } +.xll-dv-sign-success-body { flex:1; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:24px 20px; text-align:center; } +.xll-dv-sign-success-icon { width:72px; height:72px; border-radius:50%; background:${XLL_GREEN_SOFT}; color:${XLL_GREEN_DEEP}; display:flex; align-items:center; justify-content:center; font-size:36px; font-weight:700; margin-bottom:18px; } +.xll-dv-sign-success-title { font-size:20px; font-weight:700; color:${COLOR_TEXT}; line-height:1.4; margin-bottom:8px; } +.xll-dv-sign-success-desc { font-size:14px; color:${COLOR_TEXT_SEC}; line-height:1.6; max-width:280px; } +.xll-dv-sign-success-countdown { margin-top:14px; font-size:13px; color:${COLOR_MUTED}; } +.xll-dv-authorized-section .tc-section-head { border-bottom:none; padding:12px 14px 0; } +.xll-dv-authorized-panel { padding:12px 14px 14px; } +.xll-dv-authorized-section .xll-dv-summary-card { margin:0 0 12px; } +.xll-dv-authorized-hint { margin:0 0 10px; font-size:12px; color:${COLOR_TEXT_SEC}; line-height:1.55; } +.xll-dv-authorized-subtitle { margin:0 0 8px; font-size:12px; font-weight:600; color:${COLOR_MUTED}; } +.xll-dv-authorized-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; } +.xll-dv-authorized-card { position:relative; display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:96px; padding:14px 10px 12px; border-radius:12px; border:1.5px solid ${COLOR_LINE}; background:${COLOR_BG}; text-align:center; cursor:pointer; touch-action:manipulation; transition:border-color .15s ease, background .15s ease, box-shadow .15s ease; -webkit-appearance:none; appearance:none; font:inherit; color:inherit; } +.xll-dv-authorized-card:active:not(:disabled):not(.xll-dv-authorized-card--readonly) { opacity:.9; } +.xll-dv-authorized-card.active { border-color:${XLL_GREEN}; background:${XLL_GREEN_SOFT}; box-shadow:0 0 0 1px rgba(122,185,41,.25); } +.xll-dv-authorized-card:disabled { cursor:default; opacity:1; color:inherit; } +.xll-dv-authorized-card:not(.active):disabled { border-color:${COLOR_LINE}; background:${COLOR_PAGE}; } +.xll-dv-authorized-card--readonly { flex-direction:row; align-items:center; justify-content:flex-start; gap:12px; min-height:0; padding:12px 14px; text-align:left; cursor:default; grid-column:1 / -1; } +.xll-dv-authorized-avatar { flex-shrink:0; width:40px; height:40px; border-radius:50%; background:${COLOR_PAGE}; color:${COLOR_TEXT_SEC}; font-size:16px; font-weight:700; display:flex; align-items:center; justify-content:center; } +.xll-dv-authorized-card.active .xll-dv-authorized-avatar { background:${XLL_GREEN}; color:#fff; } +.xll-dv-authorized-card-body { min-width:0; flex:1; } +.xll-dv-authorized-card--readonly .xll-dv-authorized-avatar { width:44px; height:44px; } +.xll-dv-authorized-card-check { position:absolute; top:8px; right:8px; width:18px; height:18px; border-radius:50%; border:1.5px solid ${COLOR_LINE}; background:${COLOR_BG}; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:transparent; line-height:1; } +.xll-dv-authorized-card.active .xll-dv-authorized-card-check { border-color:${XLL_GREEN}; background:${XLL_GREEN}; color:#fff; } +.xll-dv-authorized-card-name { font-size:14px; font-weight:700; color:${COLOR_TEXT}; line-height:1.35; } +.xll-dv-authorized-card-phone { margin-top:4px; font-size:12px; color:${COLOR_TEXT_SEC}; line-height:1.4; } +.xll-dv-authorized-card.active .xll-dv-authorized-card-name { color:${XLL_GREEN_DEEP}; } +.xll-dv-authorized-card.active .xll-dv-authorized-card-phone { color:${XLL_GREEN_DEEP}; opacity:.88; } +.xll-dv-photo-readonly-empty { margin:0 14px 14px; padding:12px 14px; border-radius:10px; background:${COLOR_PAGE}; border:1px dashed ${COLOR_LINE}; font-size:12px; color:${COLOR_MUTED}; text-align:center; line-height:1.55; } +.xll-dv-photo-thumb-watermark { position:absolute; left:0; right:0; bottom:0; padding:4px 5px; background:linear-gradient(180deg, transparent, rgba(0,0,0,.72)); color:#fff; font-size:9px; line-height:1.35; text-align:left; pointer-events:none; } +.xll-dv-photo-thumb-watermark-loc { opacity:.92; margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.xll-dv-summary-card strong { color:${COLOR_TEXT}; } +.xll-dv-module .tc-section { margin-top:12px; } +.xll-dv-module .tc-scroll { padding-bottom:calc(88px + env(safe-area-inset-bottom,0px)); } .xll-vr-module .xll-mod-tab.active { color:#E11D48; } .xll-vr-module .xll-mod-chip.active { border-color:#F43F5E; color:#E11D48; background:rgba(244,63,94,.12); font-weight:600; } .xll-vr-module .xll-mod-card-btn { border-color:rgba(244,63,94,.35); background:rgba(244,63,94,.1); color:#E11D48; } @@ -379,13 +761,14 @@ const PAGE_STYLE = ` .xll-biz-section { margin:0 14px 14px; background:${COLOR_BG}; border-radius:14px; padding:14px 12px 6px; box-shadow:0 2px 8px rgba(15,23,42,.04); border:1px solid rgba(0,0,0,.03); } .xll-biz-section-title { font-size:13px; font-weight:600; color:${COLOR_MUTED}; margin-bottom:12px; padding-left:4px; letter-spacing:0.02em; } .xll-biz-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px 8px; } -.xll-biz-item { display:flex; flex-direction:column; align-items:center; gap:8px; padding:6px 2px 10px; border:none; background:transparent; cursor:pointer; touch-action:manipulation; position:relative; border-radius:12px; transition:background 0.15s ease; min-height:88px; } +.xll-biz-item { display:flex; flex-direction:column; align-items:center; gap:8px; padding:6px 2px 10px; border:none; background:transparent; cursor:pointer; touch-action:manipulation; position:relative; border-radius:12px; transition:background 0.15s ease; min-height:88px; -webkit-tap-highlight-color:transparent; } .xll-biz-item:active { background:${COLOR_PAGE}; } .xll-biz-item:focus-visible { outline:2px solid ${XLL_GREEN}; outline-offset:2px; } +.xll-biz-icon-wrap { position:relative; display:inline-flex; flex-shrink:0; overflow:visible; } .xll-biz-icon { width:48px; height:48px; border-radius:12px; background:${COLOR_PAGE}; border:1px solid ${COLOR_LINE}; display:flex; align-items:center; justify-content:center; color:${XLL_GREEN_DEEP}; transition:transform 0.15s ease, box-shadow 0.15s ease; } .xll-biz-item:active .xll-biz-icon { transform:scale(0.95); } .xll-biz-label { font-size:12px; color:${COLOR_TEXT}; text-align:center; line-height:1.35; font-weight:500; } -.xll-biz-badge { position:absolute; top:2px; right:calc(50% - 32px); min-width:18px; height:18px; padding:0 4px; border-radius:999px; background:${XLL_GREEN}; color:#fff; font-size:10px; font-weight:700; display:flex; align-items:center; justify-content:center; font-variant-numeric:tabular-nums; box-shadow:0 1px 4px rgba(122,185,41,.4); } +.xll-biz-badge { position:absolute; top:-4px; right:-4px; min-width:18px; height:18px; padding:0 4px; border-radius:999px; background:${XLL_GREEN}; color:#fff; font-size:10px; font-weight:700; display:flex; align-items:center; justify-content:center; font-variant-numeric:tabular-nums; box-shadow:0 1px 4px rgba(122,185,41,.4); z-index:2; pointer-events:none; } .xll-map-tabs { display:flex; background:${COLOR_BG}; border-bottom:1px solid ${COLOR_LINE}; } .xll-map-tab { flex:1; min-height:44px; border:none; background:transparent; font-size:15px; color:${COLOR_TEXT_SEC}; cursor:pointer; position:relative; font-weight:500; touch-action:manipulation; transition:color 0.2s ease; } .xll-map-tab.active { color:${XLL_GREEN}; font-weight:700; } @@ -829,60 +1212,637 @@ const arDaysTag = (task) => { /* ── 交车(参照 web端/交车管理.jsx + Axhub 交车原型) ── */ const DV_OPERATOR_REGIONS = ['浙江省-嘉兴市']; -const DV_RESERVE_PLATES = [ - { plateNo: '浙F80088', parkingLot: '嘉兴港区氢能停车场', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774701' }, - { plateNo: '浙F88601', parkingLot: '平湖指定停车场', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123401' }, - { plateNo: '浙F88602', parkingLot: '平湖指定停车场', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123402' }, + +/** 运维人员权限下可操作的停车场(备车库) */ +const DV_OPERATOR_PARKING_LOTS = [ + { key: 'all', label: '全部停车场' }, + { key: 'jiaxing', label: '嘉兴港区氢能停车场' }, + { key: 'pinghu', label: '平湖指定停车场' }, + { key: 'nanhu', label: '南湖科技大道停车场' }, ]; +/** + * 交车选车候选(已备车 · 权限停车场内) + * readiness: ready | ctp_expired | commercial_expired | license_expired + */ +const DV_DELIVERY_PICK_VEHICLES = [ + { plateNo: '浙F80088', parkingLot: '嘉兴港区氢能停车场', parkingKey: 'jiaxing', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774701', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '自有', insuranceStatus: '正常', licenseStatus: '正常', readiness: 'ready' }, + { plateNo: '浙F88601', parkingLot: '平湖指定停车场', parkingKey: 'pinghu', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123401', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '自有', insuranceStatus: '正常', licenseStatus: '正常', readiness: 'ready' }, + { plateNo: '浙F88602', parkingLot: '平湖指定停车场', parkingKey: 'pinghu', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123402', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '离线', vehicleSource: '自有', insuranceStatus: '正常', licenseStatus: '正常', readiness: 'ready' }, + { plateNo: '浙F88603', parkingLot: '平湖指定停车场', parkingKey: 'pinghu', brand: '飞驰', model: '49吨牵引车头', vin: 'LNBSCPKB8RR123403', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '自有', insuranceStatus: '异常', licenseStatus: '正常', readiness: 'ctp_expired' }, + { plateNo: '浙F88604', parkingLot: '嘉兴港区氢能停车场', parkingKey: 'jiaxing', brand: '宇通', model: '18吨双飞翼货车', vin: 'LKLG7C4E4NA774702', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '外租', insuranceStatus: '异常', licenseStatus: '正常', readiness: 'commercial_expired' }, + { plateNo: '浙F88605', parkingLot: '南湖科技大道停车场', parkingKey: 'nanhu', brand: '东风', model: 'DFH1180厢式货车', vin: 'LKLG7C4E4NA774759', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '离线', vehicleSource: '自有', insuranceStatus: '正常', licenseStatus: '异常', readiness: 'license_expired' }, + { plateNo: '浙F88606', parkingLot: '南湖科技大道停车场', parkingKey: 'nanhu', brand: '福田', model: '奥铃4.5吨冷藏车', vin: 'LKLG7C4E4NA774760', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '自有', insuranceStatus: '正常', licenseStatus: '正常', readiness: 'ready' }, +]; + +const DV_READINESS_META = { + ready: { label: '已就绪可交车', canPick: true, blocked: false }, + ctp_expired: { label: '交强险已到期无法交车', canPick: false, blocked: true }, + commercial_expired: { label: '商业险已到期无法交车', canPick: false, blocked: true }, + ctp_and_commercial_expired: { label: '交强险、商业险已到期无法交车', canPick: false, blocked: true }, + license_expired: { label: '行驶证已到期无法交车', canPick: false, blocked: true }, +}; + +const dvGetReadinessMeta = (v) => DV_READINESS_META[v.readiness] || DV_READINESS_META.ready; + +/** 交车位置坐标(原型 · 按备车库/交车地点) */ +const DV_DELIVERY_COORD_BY_PARKING = { + jiaxing: { lat: 30.7428, lng: 121.0562, label: '嘉兴港区氢能停车场' }, + pinghu: { lat: 30.6772, lng: 121.0153, label: '平湖指定停车场' }, + nanhu: { lat: 30.7465, lng: 120.7582, label: '南湖科技大道停车场' }, +}; + +const dvGetDeliveryLocationMeta = (plateNo, row) => { + const vehicle = DV_DELIVERY_PICK_VEHICLES.find((v) => v.plateNo === String(plateNo || '').trim()); + if (vehicle?.parkingKey && DV_DELIVERY_COORD_BY_PARKING[vehicle.parkingKey]) { + const coord = DV_DELIVERY_COORD_BY_PARKING[vehicle.parkingKey]; + return { + lat: coord.lat, + lng: coord.lng, + label: coord.label, + address: vehicle.parkingLot || coord.label, + plateNo: vehicle.plateNo, + }; + } + const addr = String(row?.deliveryAddress || '').trim(); + if (/港区|氢能/.test(addr)) { + const coord = DV_DELIVERY_COORD_BY_PARKING.jiaxing; + return { ...coord, address: addr || coord.label, plateNo: plateNo || '' }; + } + if (/平湖/.test(addr)) { + const coord = DV_DELIVERY_COORD_BY_PARKING.pinghu; + return { ...coord, address: addr || coord.label, plateNo: plateNo || '' }; + } + if (/南湖/.test(addr)) { + const coord = DV_DELIVERY_COORD_BY_PARKING.nanhu; + return { ...coord, address: addr || coord.label, plateNo: plateNo || '' }; + } + return { + lat: 30.7102, + lng: 121.0208, + label: row?.deliveryRegion || '嘉兴市', + address: addr || row?.deliveryRegion || '交车区域', + plateNo: plateNo || '', + }; +}; + +/** 车辆是否已接入 GPS(在线视为有 GPS 坐标) */ +const dvVehicleHasGpsDevice = (plateNo) => { + const vehicle = DV_DELIVERY_PICK_VEHICLES.find((v) => v.plateNo === String(plateNo || '').trim()); + if (!vehicle) return false; + return vehicle.onlineStatus === '在线'; +}; + +const dvResolveDeliveryLocation = (plateNo, row, formLocation) => { + if (dvVehicleHasGpsDevice(plateNo)) { + return { ...dvGetDeliveryLocationMeta(plateNo, row), source: 'vehicle' }; + } + if (formLocation && formLocation.lat != null && formLocation.lng != null) { + return { + lat: formLocation.lat, + lng: formLocation.lng, + address: formLocation.address || '当前定位', + label: formLocation.address || '当前定位', + plateNo: plateNo || '', + source: 'current', + }; + } + return { + ...dvGetDeliveryLocationMeta(plateNo, row), + address: '暂未定位,请点击获取当前定位', + source: 'pending', + }; +}; + +/** 识别车牌校验:以下运营状态视为系统不可交车匹配(待运营走 2/3/4 明细校验) */ +const DV_RECOGNIZE_OPERATE_NOT_FOUND = ['租赁', '自营', '退出运营']; + +/** 识别车牌扩展车辆池(含各类校验场景,选车列表仍仅用 DV_DELIVERY_PICK_VEHICLES) */ +const DV_RECOGNIZE_EXTRA_VEHICLES = [ + { plateNo: '浙F88701', parkingLot: '嘉兴港区氢能停车场', parkingKey: 'jiaxing', brand: '东风', model: 'DFH1180厢式货车', vin: 'LKLG7C4E4NA774801', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '未备车', onlineStatus: '离线', vehicleSource: '自有', insuranceStatus: '正常', licenseStatus: '正常', readiness: 'ready' }, + { plateNo: '浙F88702', parkingLot: '平湖指定停车场', parkingKey: 'pinghu', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123501', region: '浙江省 · 嘉兴市', operateStatus: '租赁', vehicleStatus: '已交车', onlineStatus: '在线', vehicleSource: '自有', insuranceStatus: '正常', licenseStatus: '正常', readiness: 'ready' }, + { plateNo: '浙F88704', parkingLot: '嘉兴港区氢能停车场', parkingKey: 'jiaxing', brand: '宇通', model: '18吨双飞翼货车', vin: 'LKLG7C4E4NA774804', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '外租', insuranceStatus: '异常', licenseStatus: '正常', readiness: 'ctp_and_commercial_expired', ctpExpired: true, commercialExpired: true }, + { plateNo: '浙F88706', parkingLot: '南湖科技大道停车场', parkingKey: 'nanhu', brand: '福田', model: '奥铃4.5吨冷藏车', vin: 'LKLG7C4E4NA774806', region: '浙江省 · 嘉兴市', operateStatus: '可运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '自有', insuranceStatus: '异常', licenseStatus: '异常', readiness: 'ctp_expired', ctpExpired: true }, + { plateNo: '浙F88707', parkingLot: '平湖指定停车场', parkingKey: 'pinghu', brand: '飞驰', model: '49吨牵引车头', vin: 'LNBSCPKB8RR123507', region: '浙江省 · 嘉兴市', operateStatus: '待运营', vehicleStatus: '已备车', onlineStatus: '在线', vehicleSource: '自有', insuranceStatus: '异常', licenseStatus: '正常', readiness: 'commercial_expired', commercialExpired: true }, +]; + +const DV_RECOGNIZE_VEHICLE_POOL = [...DV_DELIVERY_PICK_VEHICLES, ...DV_RECOGNIZE_EXTRA_VEHICLES]; + +const dvFindRecognizeVehicle = (plateNo) => { + const q = String(plateNo || '').trim().toUpperCase(); + if (!q) return null; + return DV_RECOGNIZE_VEHICLE_POOL.find((v) => v.plateNo.toUpperCase() === q) || null; +}; + +const dvBuildInsuranceExpireMsg = (vehicle) => { + if (!vehicle || vehicle.vehicleStatus !== '已备车') return null; + const ctp = vehicle.readiness === 'ctp_expired' || vehicle.readiness === 'ctp_and_commercial_expired' || vehicle.ctpExpired === true; + const commercial = vehicle.readiness === 'commercial_expired' || vehicle.readiness === 'ctp_and_commercial_expired' || vehicle.commercialExpired === true; + if (ctp && commercial) return '该车辆交强险、商业险已到期,如已购买请联系采购部上传'; + if (ctp) return '该车辆交强险已到期,如已购买请联系采购部上传'; + if (commercial) return '该车辆商业险已到期,如已购买请联系采购部上传'; + if (vehicle.insuranceStatus === '异常') return '该车辆交强险、商业险已到期,如已购买请联系采购部上传'; + return null; +}; + +const dvValidateRecognizedPlate = (vehicle) => { + if (!vehicle) { + return { ok: false, messages: ['系统未匹配到该车辆,请联系管理员确认'] }; + } + if (DV_RECOGNIZE_OPERATE_NOT_FOUND.includes(vehicle.operateStatus)) { + return { ok: false, messages: ['系统未匹配到该车辆,请联系管理员确认'] }; + } + const messages = []; + if (vehicle.vehicleStatus !== '已备车') { + messages.push('该车辆未备车,请先进行备车'); + } else { + const insMsg = dvBuildInsuranceExpireMsg(vehicle); + if (insMsg) messages.push(insMsg); + if (vehicle.licenseStatus === '异常' || vehicle.readiness === 'license_expired') { + messages.push('该车辆行驶证已到期,如已年审请联系运维部上传'); + } + } + return { ok: messages.length === 0, messages }; +}; + +/** 原型:识别演示车牌序列(循环展示各类校验结果) */ +const DV_OCR_DEMO_PLATES = ['浙F88601', '浙F99999', '浙F88702', '浙F88701', '浙F88603', '浙F88704', '浙F88605', '浙F88706', '浙F88707']; + +/** 原型:备胎胎纹检测仪 OCR 演示读数(mm) */ +const DV_SPARE_TREAD_OCR_DEMO = ['5.2', '4.8', '6.0', '3.6', '5.5', '']; +const DV_SPARE_TIRE_DEMO_PHOTO = 'https://picsum.photos/seed/spare-tire-tread/800/600'; + +/** 原型:驾驶培训证件演示图 */ +const DV_DRIVER_DOC_DEMO = { + idFront: 'https://picsum.photos/seed/driver-id-front/400/300', + idBack: 'https://picsum.photos/seed/driver-id-back/400/300', + licenseFront: 'https://picsum.photos/seed/driver-lic-front/400/300', + licenseBack: 'https://picsum.photos/seed/driver-lic-back/400/300', + qualification: 'https://picsum.photos/seed/driver-qual/400/300', + portrait: 'https://picsum.photos/seed/driver-portrait/400/300', +}; + +/** 原型:驾驶培训视频二维码(微信扫码) */ +const DV_DRIVER_TRAINING_QR = 'https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=https%3A%2F%2Fone-os.driver-training.demo%2Fwatch'; + +const dvBuildDriverTrainingCodeUrl = (draft) => { + const q = new URLSearchParams({ + mode: 'manual', + phone: String(draft?.driverPhone || '').trim(), + name: String(draft?.driverName || '').trim(), + idNo: String(draft?.driverIdNo || '').trim(), + }); + return `https://one-os.driver-training.demo/manual-sign?${q.toString()}`; +}; + +const dvDriverTrainingCodeQrUrl = (signUrl) => ( + `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(signUrl)}` +); + +const dvValidateDriverManualDraft = (draft, heavy) => { + if (!String(draft?.driverPhone || '').trim()) return { ok: false, message: '请输入手机号' }; + if (!String(draft?.driverName || '').trim()) return { ok: false, message: '请输入姓名' }; + if (!String(draft?.driverIdNo || '').trim()) return { ok: false, message: '请输入身份证号' }; + if (!draft?.driverIdFront || !draft?.driverIdBack) return { ok: false, message: '请上传身份证正反面' }; + if (!draft?.driverLicenseFront || !draft?.driverLicenseBack) return { ok: false, message: '请上传驾驶证正反面' }; + if (!draft?.driverFrontPhoto) return { ok: false, message: '请上传司机正面照片' }; + if (heavy && !draft?.driverQualification) return { ok: false, message: '请上传从业资格证' }; + return { ok: true, message: '' }; +}; + +const DV_DRIVER_MANUAL_EMPTY = { + driverPhone: '', + driverName: '', + driverIdNo: '', + driverIdFront: false, + driverIdBack: false, + driverLicenseFront: false, + driverLicenseBack: false, + driverQualification: false, + driverFrontPhoto: false, + driverIdFrontUrl: '', + driverIdBackUrl: '', + driverLicenseFrontUrl: '', + driverLicenseBackUrl: '', + driverQualificationUrl: '', + driverFrontPhotoUrl: '', +}; + +const DV_RESERVE_PLATES = DV_DELIVERY_PICK_VEHICLES.filter((v) => dvGetReadinessMeta(v).canPick); + +/** 后装设备记录(按车牌,选车后只读反写) */ +const DV_REAR_EQUIP_BY_PLATE = { + 浙F80088: { hasAd: false, hasBigWord: false, adPhotoDone: false, bigWordPhotoDone: false, hasTailgate: true, tailgatePhotoDone: true }, + 浙F88601: { hasAd: false, hasBigWord: false, adPhotoDone: false, bigWordPhotoDone: false, hasTailgate: true, tailgatePhotoDone: true }, + 浙F88602: { hasAd: false, hasBigWord: false, adPhotoDone: false, bigWordPhotoDone: false, hasTailgate: true, tailgatePhotoDone: false }, + 浙F88603: { hasAd: true, hasBigWord: true, adPhotoDone: true, bigWordPhotoDone: false, hasTailgate: false, tailgatePhotoDone: false }, + 浙F88604: { hasAd: false, hasBigWord: false, adPhotoDone: false, bigWordPhotoDone: false, hasTailgate: true, tailgatePhotoDone: true }, + 浙F88605: { hasAd: true, hasBigWord: false, adPhotoDone: false, bigWordPhotoDone: false, hasTailgate: true, tailgatePhotoDone: true }, + 浙F88606: { hasAd: true, hasBigWord: true, adPhotoDone: true, bigWordPhotoDone: true, hasTailgate: false, tailgatePhotoDone: false }, +}; + +const dvIsHeavyVehicle = (vehicleType, model) => { + const text = `${vehicleType || ''}${model || ''}`; + const tonMatch = text.match(/(\d+(?:\.\d+)?)\s*吨/); + if (tonMatch) return parseFloat(tonMatch[1]) >= 18; + return /18\s*吨|18T|49\s*吨/i.test(text); +}; + +const dvGetRearEquipRecord = (plateNo, row) => { + const key = String(plateNo || '').trim(); + if (key && DV_REAR_EQUIP_BY_PLATE[key]) return { ...DV_REAR_EQUIP_BY_PLATE[key] }; + const hasAd = row?.hasAd === '有'; + const hasTailgate = row?.hasTailgate === '有'; + return { + hasAd, + hasBigWord: hasAd, + adPhotoDone: hasAd, + bigWordPhotoDone: hasAd, + hasTailgate, + tailgatePhotoDone: hasTailgate, + }; +}; + +const dvMockDriverTrainingInfo = (heavy) => ({ + driverName: '王涛', + driverPhone: '13812345678', + driverIdNo: '330421199001011234', + driverIdFront: true, + driverIdBack: true, + driverLicenseFront: true, + driverLicenseBack: true, + driverQualification: !!heavy, + driverFrontPhoto: true, + driverIdFrontUrl: DV_DRIVER_DOC_DEMO.idFront, + driverIdBackUrl: DV_DRIVER_DOC_DEMO.idBack, + driverLicenseFrontUrl: DV_DRIVER_DOC_DEMO.licenseFront, + driverLicenseBackUrl: DV_DRIVER_DOC_DEMO.licenseBack, + driverQualificationUrl: heavy ? DV_DRIVER_DOC_DEMO.qualification : '', + driverFrontPhotoUrl: DV_DRIVER_DOC_DEMO.portrait, +}); + +/** 交车照片分类 */ +const DV_PHOTO_CATEGORIES = [ + { key: 'body', label: '车身情况' }, + { key: 'chassis', label: '底盘情况' }, + { key: 'tire', label: '轮胎情况' }, + { key: 'defect', label: '瑕疵情况' }, + { key: 'other', label: '其他情况' }, +]; + +/** 交车照片项(连拍顺序与分类) */ +const DV_PHOTO_ITEMS = [ + { key: 'dashboard', label: '仪表盘', category: 'body', required: true, tread: false }, + { key: 'front', label: '车辆正面', category: 'body', required: true, tread: false }, + { key: 'front_bottom', label: '正前方底部', category: 'chassis', required: true, tread: false }, + { key: 'left_front', label: '车辆左前方', category: 'body', required: true, tread: false }, + { key: 'left_front_bottom', label: '左侧前方底部', category: 'chassis', required: true, tread: false }, + { key: 'left_front_tire', label: '左前轮', category: 'tire', required: true, tread: true }, + { key: 'left_rear', label: '车辆左后方', category: 'body', required: true, tread: false }, + { key: 'left_rear_bottom', label: '左侧后方底部', category: 'chassis', required: true, tread: false }, + { key: 'left_rear_tire_inner', label: '左后轮(内)', category: 'tire', required: true, tread: true }, + { key: 'left_rear_tire_outer', label: '左后轮(外)', category: 'tire', required: true, tread: true }, + { key: 'right_rear', label: '车辆右后方', category: 'body', required: true, tread: false }, + { key: 'right_rear_bottom', label: '右侧后方底部', category: 'chassis', required: true, tread: false }, + { key: 'right_rear_tire_inner', label: '右后轮(内)', category: 'tire', required: true, tread: true }, + { key: 'right_rear_tire_outer', label: '右后轮(外)', category: 'tire', required: true, tread: true }, + { key: 'right_front', label: '车辆右前方', category: 'body', required: true, tread: false }, + { key: 'right_front_bottom', label: '右侧前方底部', category: 'chassis', required: true, tread: false }, + { key: 'right_front_tire', label: '右前轮', category: 'tire', required: true, tread: true }, + { key: 'spare', label: '备胎', category: 'tire', required: true, tread: true }, +]; + +const DV_PHOTO_CAPTURE_SEQUENCE = DV_PHOTO_ITEMS.map((item) => item.key); + +const dvPhotoItemByKey = (key) => DV_PHOTO_ITEMS.find((item) => item.key === key); + +const dvPhotoCategoryLabel = (categoryKey) => ( + DV_PHOTO_CATEGORIES.find((cat) => cat.key === categoryKey)?.label || '' +); + +const dvPhotoCaptured = (photos, key) => { + const val = photos?.[key]; + if (!val) return false; + if (val === true) return true; + return !!val.captured; +}; + +const dvGetPhotoRecord = (photos, key) => { + const val = photos?.[key]; + if (!val || val === true) return val === true ? { captured: true } : null; + return val; +}; + +const dvGetCaptureSequence = (formDraft) => { + let list = DV_PHOTO_CAPTURE_SEQUENCE.map((key) => dvPhotoItemByKey(key)).filter(Boolean); + if (formDraft?.spareTire === '无') { + list = list.filter((item) => item.key !== 'spare'); + } + return list; +}; + +const dvRequiredPhotosComplete = (photos, formDraft) => ( + dvGetCaptureSequence(formDraft) + .filter((item) => dvIsPhotoItemRequired(item)) + .every((item) => dvPhotoCaptured(photos, item.key)) +); + +const dvCountCapturedPhotos = (photos, formDraft) => ( + dvGetCaptureSequence(formDraft).filter((item) => dvPhotoCaptured(photos, item.key)).length +); + +const dvGetNextCaptureIndex = (photos, formDraft) => { + const seq = dvGetCaptureSequence(formDraft); + const idx = seq.findIndex((item) => !dvPhotoCaptured(photos, item.key)); + return idx >= 0 ? idx : seq.length; +}; + +const dvPhotoItemsByCategory = (categoryKey) => DV_PHOTO_ITEMS.filter((item) => item.category === categoryKey); + +const DV_PHOTO_REQUIRED_CATEGORIES = new Set(['body', 'chassis', 'tire']); +const dvIsPhotoItemRequired = (item) => DV_PHOTO_REQUIRED_CATEGORIES.has(item.category); + +/** 原型:交车照片演示图(按项固定 seed,避免加载失败) */ +const dvGetPhotoDemoUrl = (photoKey) => `https://picsum.photos/seed/oneos-dv-${encodeURIComponent(photoKey)}/480/480`; + +const dvSimulatePhotoUpload = (photoKey, cameraDraft, formDraft, row) => { + const loc = dvResolveDeliveryLocation(formDraft?.plateNo, row, formDraft?.deliveryLocation); + const watermarkTime = dvFormatOpsSignTime(); + const watermarkAddress = loc.address || loc.label || '未知地点'; + const baseUrl = cameraDraft?.photoUrl || dvGetPhotoDemoUrl(photoKey); + return { + photoUrl: `${baseUrl.split('?')[0]}?wm=1&t=${encodeURIComponent(watermarkTime)}&loc=${encodeURIComponent(watermarkAddress)}`, + watermarkTime, + watermarkAddress, + uploaded: true, + }; +}; + +/** 查看页:补齐已拍摄交车照片(车身/底盘/轮胎及瑕疵/其他) */ +const dvEnsureViewDeliveryPhotos = (row, formDraft) => { + const photos = { ...(formDraft?.deliveryPhotos || row?.deliveryPhotos || {}) }; + const ctx = formDraft || row; + dvGetCaptureSequence(ctx).forEach((item) => { + if (!dvPhotoCaptured(photos, item.key)) { + const upload = dvSimulatePhotoUpload(item.key, {}, formDraft, row); + photos[item.key] = { + captured: true, + photoUrl: upload.photoUrl, + uploaded: true, + watermarkTime: upload.watermarkTime, + watermarkAddress: upload.watermarkAddress, + ...(item.tread ? { treadDepth: '6.50' } : {}), + }; + } + }); + ['defect', 'other'].forEach((catKey) => { + const hasExtra = Object.keys(photos).some((key) => key.indexOf(`${catKey}_extra_`) === 0 && dvPhotoCaptured(photos, key)); + if (!hasExtra) { + const extraKey = `${catKey}_extra_1`; + const upload = dvSimulatePhotoUpload(extraKey, { photoUrl: dvGetPhotoDemoUrl(extraKey) }, formDraft, row); + photos[extraKey] = { + captured: true, + photoUrl: upload.photoUrl, + uploaded: true, + watermarkTime: upload.watermarkTime, + watermarkAddress: upload.watermarkAddress, + }; + } + }); + return photos; +}; + +const dvResolvePhotoUrl = (photoKey, record) => { + if (!photoKey) return ''; + const url = record?.photoUrl; + if (url && typeof url === 'string' && !url.includes('&sig=') && !url.includes('&extra=')) return url; + return dvGetPhotoDemoUrl(photoKey); +}; + +const dvBuildCategoryViewerItems = (categoryKey, photos, formDraft) => { + if (categoryKey === 'defect' || categoryKey === 'other') { + return Object.keys(photos || {}) + .filter((key) => key.indexOf(`${categoryKey}_extra_`) === 0 && dvPhotoCaptured(photos, key)) + .sort() + .map((key, idx) => { + const record = dvGetPhotoRecord(photos, key); + return { + key, + label: `照片${idx + 1}`, + photoUrl: dvResolvePhotoUrl(key, record), + treadDepth: record?.treadDepth || '', + }; + }); + } + return dvPhotoItemsByCategory(categoryKey) + .filter((item) => !(item.key === 'spare' && formDraft?.spareTire === '无')) + .filter((item) => dvPhotoCaptured(photos, item.key)) + .map((item) => { + const record = dvGetPhotoRecord(photos, item.key); + return { + key: item.key, + label: item.label, + photoUrl: dvResolvePhotoUrl(item.key, record), + treadDepth: record?.treadDepth || '', + }; + }); +}; + +const dvExtraPhotoKeys = (categoryKey, photos) => ( + Object.keys(photos || {}).filter((key) => key.indexOf(`${categoryKey}_extra_`) === 0 && dvPhotoCaptured(photos, key)) +); + +/** 交车表单步骤 */ const DV_FORM_STEPS = [ - { key: 'info', label: '交车信息' }, - { key: 'equip', label: '车辆信息' }, - { key: 'metrics', label: '交车数据' }, - { key: 'photos', label: '交车照片' }, - { key: 'confirm', label: '确认提交' }, + { key: 'vehicle', label: '车辆情况' }, + { key: 'inspection', label: '交车检查项' }, + { key: 'photos', label: '拍摄照片' }, ]; -const DV_PHOTO_SECTIONS = [ - { key: 'body', label: '车身照片' }, - { key: 'chassis', label: '底盘照片' }, - { key: 'tire', label: '轮胎照片' }, - { key: 'defect', label: '瑕疵照片' }, - { key: 'other', label: '其他照片' }, +/** 型号参数 · 仪表盘氢量单位(% / MPa) */ +const DV_MODEL_GAUGE_UNIT = { + '东风|DFH1180': 'MPa', + '福田|BJ1180': '%', + '现代|帕力安牌4.5吨冷链车': '%', + '苏龙|海格牌18吨双飞翼货车': '%', + '宇通|18吨双飞翼货车': 'MPa', + '福田|奥铃4.5吨冷藏车': '%', + '飞驰|49吨牵引车头': 'MPa', +}; + +const dvGetModelGaugeUnit = (brand, model) => { + const key = `${String(brand || '').trim()}|${String(model || '').trim()}`; + if (DV_MODEL_GAUGE_UNIT[key]) return DV_MODEL_GAUGE_UNIT[key]; + const m = String(model || ''); + if (/DFH1180|SX1180/.test(m)) return 'MPa'; + return '%'; +}; + +/** 交车检查单类别与项目(对齐 web 交车检查单) */ +const DV_INSPECTION_TIRE_CATEGORY = '轮胎检查'; +const DV_INSPECTION_TIRE_TREAD_DEMO = ['13.05', '13.22', '13.01', '13.47', '13.09', '13.36']; + +const DV_INSPECTION_SECTIONS = [ + { + category: '证件信息', + items: ['行驶证', '营运证', '加氢证', 'ETC设备', 'ETC卡', '前后车牌照', '通行证', 'GPS设备(服务中)'], + }, + { + category: '工具信息', + items: ['钥匙', '备胎', '三角木', '千斤顶', '工具包', '三角警示牌', '灭火器', '其他'], + }, + { + category: '外观检查', + items: [ + '检查玻璃无划痕、破裂', + '检查座椅无划痕、破损', + '检查车身漆面无划痕、变形', + '检查货箱反光贴完好', + '检查货箱防撞块完好', + '检查所有灯光完好', + '检查冷机工作(如有)', + '车辆清洗', + '其他', + ], + }, + { + category: DV_INSPECTION_TIRE_CATEGORY, + items: ['左前 (1轴)', '左后内 (2轴)', '左后外 (2轴)', '右前 (1轴)', '右后内 (2轴)', '右后外 (2轴)'], + tread: true, + }, ]; +const dvInspectionIsTireCategory = (category) => category === DV_INSPECTION_TIRE_CATEGORY; + +const dvBuildInspectionList = () => { + const list = []; + let tireIdx = 0; + DV_INSPECTION_SECTIONS.forEach((section, ci) => { + (section.items || []).forEach((item, ji) => { + const isTire = !!section.tread; + list.push({ + key: `ins-${ci}-${ji}`, + category: section.category, + item, + checked: item === '检查冷机工作(如有)' ? false : true, + treadDepth: isTire ? (DV_INSPECTION_TIRE_TREAD_DEMO[tireIdx++] || '6.50') : '', + remark: '', + }); + }); + }); + return list; +}; + +/** 车辆情况步骤:校验必填项是否已填写 */ +const dvValidateVehicleStep = (formDraft, row) => { + if (!formDraft?.plateNo) return { ok: false, message: '请先选择交车车辆' }; + const rearEquip = formDraft.rearEquip || dvGetRearEquipRecord(formDraft.plateNo, row); + if (rearEquip.hasAd) { + if (!(formDraft.adPhotoUploaded || rearEquip.adPhotoDone)) { + return { ok: false, message: '请拍摄车身广告照片' }; + } + if (!(formDraft.bigWordPhotoUploaded || rearEquip.bigWordPhotoDone)) { + return { ok: false, message: '请拍摄放大字照片' }; + } + } + const trainingDone = formDraft.driverTrainingDone || formDraft.driverTraining === '已完成'; + if (formDraft.driverTrainingPending) return { ok: false, message: '请等待司机微信扫码完成培训签字' }; + if (!trainingDone) return { ok: false, message: '请完成驾驶培训' }; + if (formDraft.deliveryMileage === '' || formDraft.deliveryH2 === '' || formDraft.deliveryElec === '') { + return { ok: false, message: '请填写里程、氢量与电量' }; + } + if (!dvVehicleHasGpsDevice(formDraft.plateNo)) { + const loc = formDraft.deliveryLocation; + if (!loc || loc.lat == null || loc.lng == null) { + return { ok: false, message: '请先获取交车位置' }; + } + } + return { ok: true }; +}; + +/** 交车检查项步骤:校验轮胎胎纹等必填项 */ +const dvValidateInspectionStep = (formDraft) => { + const list = formDraft?.inspectionList || []; + for (const row of list) { + if (!dvInspectionIsTireCategory(row.category)) continue; + if (!String(row.treadDepth || '').trim()) { + return { ok: false, message: `请填写${row.item}胎纹深度` }; + } + } + return { ok: true }; +}; + +const dvFormatMetric2 = (v, suffix) => { + if (v == null || v === '') return '—'; + const n = Number(v); + if (!Number.isFinite(n)) return '—'; + const text = n.toFixed(2); + return suffix ? `${text} ${suffix}` : text; +}; + +const dvFormatServiceFee = (v) => dvFormatMetric2(v, '元'); + +const dvParseMetric2 = (v) => { + const s = String(v ?? '').trim(); + if (!s) return null; + const n = parseFloat(s.replace(/,/g, '')); + if (!Number.isFinite(n)) return null; + return Math.round(n * 100) / 100; +}; + +const dvMetricInputChange = (raw) => { + if (raw === '' || raw === '-') return raw; + if (/^\d*\.?\d{0,2}$/.test(raw)) return raw; + return null; +}; + const DV_MOCK_ORDERS = [ { id: 'o1', expectedDate: '2025-02-28 至 2025-03-05', contractCode: 'LNZLHT 20260104001', projectName: '桐乡韵达租赁4.5T*10', customerName: '桐乡市丰韵快递有限责任公司', businessDept: '业务二部', businessOwner: '刘念念', taskSource: '替换车', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '平湖指定停车场', createTime: '2026-06-04 11:28', createBy: '赵小峰', vehicleList: [ { vehicleKey: 1, seq: 1, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123401', replaceOldPlate: '浙A88601F', plateNo: '', deliveryTime: '', deliveryPerson: '', deliveryStatus: '未开始', deliveryMileage: null, deliveryH2: null, deliveryH2Unit: '%', deliveryElec: null, hasAd: '', hasTailgate: '', spareTire: '', driverTraining: '' }, - { vehicleKey: 2, seq: 2, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123402', replaceOldPlate: '浙A88602F', plateNo: '浙F88601', deliveryTime: '2026-06-03 14:20', deliveryPerson: '张明辉', deliveryStatus: '待客户签章', deliveryMileage: 12500, deliveryH2: 18, deliveryH2Unit: '%', deliveryElec: 76, hasAd: '无', hasTailgate: '有', spareTire: '有', driverTraining: '已完成' }, + { vehicleKey: 2, seq: 2, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123402', replaceOldPlate: '浙A88602F', plateNo: '浙F88601', deliveryTime: '2026-06-03 14:20', deliveryPerson: '张明辉', deliveryStatus: '待客户签章', deliveryMileage: 12500, deliveryH2: 18, deliveryH2Unit: '%', deliveryElec: 76, serviceFee: 200, hasAd: '无', hasTailgate: '有', spareTire: '有', spareTirePhotoUploaded: true, spareTirePhotoUrl: DV_SPARE_TIRE_DEMO_PHOTO, spareTireTreadDepth: '5.2', driverTraining: '已完成', authorizedPersonId: 'ap1', authorizedPersonName: '李晓明', authorizedPersonPhone: '13800138001', signSent: true }, ], }, { id: 'o4', expectedDate: '2024-11-15', contractCode: 'LNZLHT2024111401', projectName: '聚德11月新增苏龙18T*2', customerName: '沈阳聚德物流有限公司', businessDept: '业务三部', businessOwner: '金可鹏', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴港区氢能停车场', createTime: '2024-11-15 15:05', createBy: '何苗苗', vehicleList: [ - { vehicleKey: 1, seq: 1, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774701', plateNo: '浙F80088', deliveryTime: '2026-06-02 16:00', deliveryPerson: '魏山', deliveryStatus: '待客户签章', deliveryMileage: 46200, deliveryH2: 21, deliveryH2Unit: '%', deliveryElec: 80, hasAd: '无', hasTailgate: '有', spareTire: '有', driverTraining: '已完成' }, - { vehicleKey: 2, seq: 2, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774702', plateNo: '沪A03802F', deliveryTime: '2025-11-20 09:30', deliveryPerson: '何苗苗', deliveryStatus: '已保存', deliveryMileage: null, deliveryH2: null, deliveryH2Unit: '%', deliveryElec: null, hasAd: '无', hasTailgate: '有', spareTire: '有', driverTraining: '已完成' }, + { vehicleKey: 1, seq: 1, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774701', plateNo: '浙F80088', deliveryTime: '2026-06-02 16:00', deliveryPerson: '魏山', deliveryStatus: '待客户签章', deliveryMileage: 46200, deliveryH2: 21, deliveryH2Unit: '%', deliveryElec: 80, hasAd: '无', hasTailgate: '有', spareTire: '有', spareTirePhotoUploaded: true, spareTirePhotoUrl: DV_SPARE_TIRE_DEMO_PHOTO, spareTireTreadDepth: '4.8', driverTraining: '已完成', authorizedPersonId: 'ap2', authorizedPersonName: '王芳', authorizedPersonPhone: '13900139002', signSent: true }, + { vehicleKey: 2, seq: 2, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774702', plateNo: '沪A03802F', deliveryTime: '2026-06-01 10:15', deliveryPerson: '魏山', deliveryStatus: '待重新签章', deliveryMileage: 38800, deliveryH2: 24, deliveryH2Unit: '%', deliveryElec: 72, hasAd: '无', hasTailgate: '有', spareTire: '有', spareTirePhotoUploaded: true, spareTirePhotoUrl: DV_SPARE_TIRE_DEMO_PHOTO, spareTireTreadDepth: '5.0', driverTraining: '已完成', authorizedPersonId: '', authorizedPersonName: '', authorizedPersonPhone: '', signSent: false }, ], }, { id: 'o5', expectedDate: '2025-02-15', contractCode: 'HT-ZL-2024-001', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', businessDept: '业务一部', businessOwner: '张经理', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '南湖科技大道停车场', createTime: '2025-02-10 09:00', createBy: '系统', vehicleList: [ - { vehicleKey: 1, seq: 1, vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', vin: 'LKLG7C4E4NA774759', plateNo: '京A12345', deliveryTime: '2025-02-15 10:30', deliveryPerson: '张三', deliveryStatus: '客户已签章', deliveryMileage: 12580, deliveryH2: 35, deliveryH2Unit: 'MPa', deliveryElec: 45, hasAd: '有', hasTailgate: '有', spareTire: '有', driverTraining: '已完成', vehicleReturned: true }, - { vehicleKey: 2, seq: 2, vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LKLG7C4E4NA774760', plateNo: '京C11111', deliveryTime: '2025-02-15 14:00', deliveryPerson: '李四', deliveryStatus: '客户已签章', deliveryMileage: 13200, deliveryH2: 68, deliveryH2Unit: '%', deliveryElec: 38, hasAd: '无', hasTailgate: '无', spareTire: '有', driverTraining: '已完成', vehicleReturned: false }, + { vehicleKey: 1, seq: 1, vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', vin: 'LKLG7C4E4NA774759', plateNo: '京A12345', deliveryTime: '2025-02-15 10:30', deliveryPerson: '张三', deliveryStatus: '客户已签章', deliveryMileage: 12580, deliveryH2: 35, deliveryH2Unit: 'MPa', deliveryElec: 45, serviceFee: 150, hasAd: '有', hasTailgate: '有', spareTire: '有', driverTraining: '已完成', customerSignTime: '2025-02-15 11:45', vehicleReturned: true, vehicleReturnTime: '2025-03-01 16:30' }, + { vehicleKey: 2, seq: 2, vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LKLG7C4E4NA774760', plateNo: '京C11111', deliveryTime: '2025-02-15 14:00', deliveryPerson: '李四', deliveryStatus: '客户已签章', deliveryMileage: 13200, deliveryH2: 68, deliveryH2Unit: '%', deliveryElec: 38, hasAd: '无', hasTailgate: '无', spareTire: '有', driverTraining: '已完成', customerSignTime: '2025-02-15 16:20', vehicleReturned: false }, ], }, ]; -const DV_IN_PROGRESS_STATUSES = ['未开始', '已保存', '待客户签章']; -const DV_STATUS_FILTER_OPTIONS = ['', '未开始', '已保存', '待客户签章']; +/** 原型:交车被授权人(客户方短信签章) */ +const DV_AUTHORIZED_PERSONS = [ + { id: 'ap1', name: '李晓明', phone: '13800138001' }, + { id: 'ap2', name: '王芳', phone: '13900139002' }, + { id: 'ap3', name: '赵强', phone: '13700137003' }, + { id: 'ap4', name: '陈静', phone: '13600136004' }, +]; + +const dvFindAuthorizedPerson = (id) => DV_AUTHORIZED_PERSONS.find((p) => p.id === id) || null; +const dvPersonInitial = (name) => { + const s = String(name || '').trim(); + return s ? s.slice(-1) : '?'; +}; + +const DV_IN_PROGRESS_STATUSES = ['未开始', '已保存', '待客户签章', '待重新签章']; +const DV_STATUS_FILTER_OPTIONS = ['', '未开始', '已保存', '待客户签章', '待重新签章']; const DV_LIST_TABS = [ { key: 'inProgress', short: '进行中', label: '进行中' }, { key: 'completed', short: '已完成', label: '已完成' }, { key: 'all', short: '全部', label: '全部任务' }, ]; const DV_EMPTY_MORE_FILTER = { customerName: '', projectName: '', dateStart: '', dateEnd: '' }; +const DV_EMPTY_FILTER_DRAFT = { status: '', ...DV_EMPTY_MORE_FILTER }; const dvIsHistoryStatus = (s) => s === '客户已签章'; const dvIsInProgressStatus = (s) => DV_IN_PROGRESS_STATUSES.indexOf(s || '未开始') >= 0; @@ -892,16 +1852,29 @@ const dvFormatExpectedDate = (expectedDate) => { return String(expectedDate).trim().replace(/\s*至\s*/g, ' - '); }; const dvDisplayActualTime = (t) => (t && String(t).trim() ? String(t).trim() : '—'); +const dvDisplayMinuteTime = (t) => { + const s = dvDisplayActualTime(t); + if (s === '—') return s; + const m = s.match(/^(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/); + return m ? m[1] : s; +}; +/** 运维人员完成 E签宝签字时间(提交签章时自动写入,不在表单中填写) */ +const dvFormatOpsSignTime = (date = new Date()) => { + const pad = (n) => String(n).padStart(2, '0'); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; +}; const dvVehicleDesc = (row) => [row.brand, row.model].filter(Boolean).join('·') || '—'; const dvStatusTag = (status) => { if (status === '客户已签章') return { text: status, cls: 'ok' }; if (status === '待客户签章') return { text: status, cls: 'info' }; + if (status === '待重新签章') return { text: status, cls: 'warn' }; if (status === '已保存') return { text: status, cls: 'warn' }; return { text: status || '未开始', cls: 'neutral' }; }; const dvCardStatusClass = (status) => { if (status === '客户已签章') return 'ok'; if (status === '待客户签章') return 'info'; + if (status === '待重新签章') return 'pending'; return 'pending'; }; const dvParseDateOnly = (value) => { @@ -959,35 +1932,95 @@ const dvFlattenOrders = (orders) => { deliveryH2: v.deliveryH2, deliveryH2Unit: v.deliveryH2Unit || '%', deliveryElec: v.deliveryElec, + deliveryRemark: v.deliveryRemark || '', + serviceFee: v.serviceFee, + deliveryLocation: v.deliveryLocation || null, hasAd: v.hasAd || '', hasTailgate: v.hasTailgate || '', spareTire: v.spareTire || '', + spareTirePhotoUploaded: !!v.spareTirePhotoUploaded, + spareTirePhotoUrl: v.spareTirePhotoUrl || '', + spareTireTreadDepth: v.spareTireTreadDepth || '', driverTraining: v.driverTraining || '', vehicleReturned: v.vehicleReturned, + vehicleReturnTime: v.vehicleReturnTime || '', + customerSignTime: v.customerSignTime || '', + inspectionList: v.inspectionList, + authorizedPersonId: v.authorizedPersonId || '', + authorizedPersonName: v.authorizedPersonName || '', + authorizedPersonPhone: v.authorizedPersonPhone || '', + signSent: !!v.signSent, }); }); }); return rows; }; -const dvBuildEmptyForm = (row) => ({ - plateNo: row.plateNo || '', - brand: row.brand || '', - model: row.model || '', - vin: row.vin || '', - hasAd: row.hasAd || '', - hasTailgate: row.hasTailgate || '', - spareTire: row.spareTire || '', - driverTraining: row.driverTraining || '', - deliveryMileage: row.deliveryMileage != null ? String(row.deliveryMileage) : '', - deliveryH2: row.deliveryH2 != null ? String(row.deliveryH2) : '', - deliveryH2Unit: row.deliveryH2Unit || '%', - deliveryElec: row.deliveryElec != null ? String(row.deliveryElec) : '', - deliveryTime: row.deliveryTime ? row.deliveryTime.replace(' ', 'T').slice(0, 16) : '', -}); +const dvBuildEmptyForm = (row) => { + const plateNo = row.plateNo || ''; + const rearEquip = dvGetRearEquipRecord(plateNo, row); + const heavy = dvIsHeavyVehicle(row.vehicleType, row.model); + const trainingDone = row.driverTraining === '已完成'; + const driverInfo = trainingDone ? dvMockDriverTrainingInfo(heavy) : { + ...DV_DRIVER_MANUAL_EMPTY, + }; + return { + plateNo, + brand: row.brand || '', + model: row.model || '', + vin: row.vin || '', + vehicleType: row.vehicleType || '', + hasAd: rearEquip.hasAd ? '有' : '无', + hasTailgate: rearEquip.hasTailgate ? '有' : '无', + spareTire: row.spareTire || '', + spareTirePhotoUploaded: !!row.spareTirePhotoUploaded, + spareTirePhotoUrl: row.spareTirePhotoUrl || '', + spareTireTreadDepth: row.spareTireTreadDepth || '', + rearEquip, + adPhotoUploaded: false, + bigWordPhotoUploaded: false, + driverTraining: row.driverTraining || '', + driverTrainingDone: trainingDone, + ...driverInfo, + deliveryMileage: row.deliveryMileage != null ? String(row.deliveryMileage) : '', + deliveryH2: row.deliveryH2 != null ? String(row.deliveryH2) : '', + deliveryH2Unit: row.deliveryH2Unit || dvGetModelGaugeUnit(row.brand, row.model), + deliveryElec: row.deliveryElec != null ? String(row.deliveryElec) : '', + serviceFee: row.serviceFee != null ? String(row.serviceFee) : '', + deliveryRemark: row.deliveryRemark || '', + inspectionList: Array.isArray(row.inspectionList) && row.inspectionList.length ? row.inspectionList : dvBuildInspectionList(), + deliveryPhotos: row.deliveryPhotos && typeof row.deliveryPhotos === 'object' ? { ...row.deliveryPhotos } : {}, + deliveryLocation: row.deliveryLocation || null, + authorizedPersonId: row.authorizedPersonId || '', + authorizedPersonName: row.authorizedPersonName || '', + authorizedPersonPhone: row.authorizedPersonPhone || '', + signSent: !!row.signSent, + }; +}; -const dvFormatH2 = (v, unit) => (v == null || v === '' ? '—' : `${v} ${unit || '%'}`); -const dvFormatMileage = (v) => (v == null || v === '' ? '—' : `${Number(v).toLocaleString()} km`); +const dvMergeVehicleIntoForm = (prev, vehicle, row) => { + const rearEquip = dvGetRearEquipRecord(vehicle.plateNo, row); + const brand = vehicle.brand || prev.brand || row.brand; + const model = vehicle.model || prev.model || row.model; + return { + ...prev, + plateNo: vehicle.plateNo, + brand, + model, + vin: vehicle.vin || prev.vin || row.vin, + vehicleType: row.vehicleType || prev.vehicleType, + hasAd: rearEquip.hasAd ? '有' : '无', + hasTailgate: rearEquip.hasTailgate ? '有' : '无', + rearEquip, + deliveryH2Unit: dvGetModelGaugeUnit(brand, model), + deliveryLocation: null, + adPhotoUploaded: false, + bigWordPhotoUploaded: false, + }; +}; + +const dvFormatH2 = (v, unit) => (v == null || v === '' ? '—' : `${dvFormatMetric2(v)} ${unit || '%'}`); +const dvFormatMileage = (v) => (v == null || v === '' ? '—' : `${dvFormatMetric2(v)} km`); const buildPickupDetail = (task) => { const isShanghai = task?.bizNo === 'TC-2026-0312'; @@ -2815,7 +3848,7 @@ const AnnualReviewModule = ({ onRegisterBack }) => { if (operateTask) { const t = operateTask; return ( -
请确认车辆与拍摄环境就绪,连拍将自动按顺序进行
+ {capturedCount > 0 ? ( +即将拍摄
+秒后自动开始 · 可点底部「立即拍摄」
+| 目标 | 说明 |
|---|---|
| 现场可办 | 移动端分步引导,降低漏填漏拍;支持断点续办(草稿保存、连拍续拍) |
| 数据对齐 | 检查单类别/项目与 web 交车检查单一致;氢量单位随车型参数自动匹配 |
| 合规留痕 | 照片自动上传打水印(时间、地点);E签宝双签(运维 + 客户被授权人) |
| 状态可追踪 | 未开始 → 已保存 → 待客户签章 → 客户已签章,列表可筛选 |
DeliveryModule),绿色主题与运维模块统一。表单内返回:先退出办理页 → 再退出交车模块。步骤条可点击切换,但「下一步」须通过当前步骤校验。
+| 状态 | 触发条件 | 用户可操作 | 编辑权限 |
|---|---|---|---|
| 未开始 | 新建任务,未保存过 | 去办理 | 可编辑 |
| 已保存 | 点击保存或步骤「下一步」自动保存 | 继续办理 | 可编辑 |
| 待客户签章 | 运维完成 E签宝 签字并发送签章文件 | 查看记录;清除签章后重发 | 只读(清除签章后可重选被授权人) |
| 客户已签章 | 被授权人短信完成签章 | 查看、下载 PDF | 只读 |
支持车牌、项目、客户名称模糊搜索。
+Hero 区展示客户名称、项目名称、交车区域、计划交车、交车地点、业务类型与状态 Tag。下方分 5 个编号卡片区域:
+ +必填。两种方式:
+选车后展示车牌、品牌型号、停车场。
+| 字段 | 必填 | 规则 |
|---|---|---|
| 车身广告及放大字 | 是 | 开关确认有/无;选「有」须拍摄车身广告照片 + 放大字照片 |
| 尾板 | 是 | 开关确认有/无 |
| 备胎 | 否 | 仅选择有/无;备胎照片在步骤三轮胎连拍末项拍摄 |
必填。两种模式二选一:
+司机完成签字后展示司机信息与证件缩略图,标记「已完成驾驶培训」。
+手动记录 · 司机微信端(仅需求说明):扫码后先阅读并同意安全培训文件(10 秒倒计时后可确定)→ 展示运维登记的司机信息与证件照片 → 点击「确认签字」唤起签名板 → 签字成功后提示培训成功并同步至安全培训记录。
+| 字段 | 必填 | 规则 |
|---|---|---|
| 里程 (km) | 是 | 2 位小数,无步进器 |
| 电量 (%) | 是 | 2 位小数 |
| 氢量 | 是 | 2 位小数;单位按车型仪表盘参数自动匹配(%/MPa),不可手动切换 |
| 送车服务费 | 否 | 2 位小数,单位元 |
| 备注 | 否 | 多行文本 |
不含交车时间字段(由签章提交时系统写入)。
+步骤一「下一步」校验:非 GPS 车辆须已获取定位。
+| 类别 | 控件类型 | 必填规则 |
|---|---|---|
| 轮胎(前左/前右/后左/后右/备胎) | 数字输入框 (mm) | 须填写胎纹深度;车辆配置「无备胎」时跳过备胎行 |
| 其余类别 | 开关(正常/异常) | 默认「正常」;可填备注(≤40 字) |
| 分类 | 拍摄项(按顺序) | 胎纹 OCR |
|---|---|---|
| 车身情况 | 仪表盘、车辆正面、左前方、左后方、右后方、右前方 | — |
| 底盘情况 | 正前方底部、左侧前方底部、左侧后方底部、右侧后方底部、右侧前方底部 | — |
| 轮胎情况 | 左前轮、左后轮(内/外)、右后轮(内/外)、右前轮、备胎(有备胎时) | 轮胎项须 OCR 胎纹,可编辑;OCR 失败禁止拍下一张 |
| 瑕疵情况 | 连拍完成后可补拍多张 | — |
| 其他情况 | 连拍完成后可补拍多张 | — |
watermarkTime、地点 watermarkAddress,取自交车位置解析结果)。
+| 交互 | 说明 |
|---|---|
| 拍照 / 相册 | 相机页底部「拍照」调用相机;「相册」从本地上传图片(识别车牌支持底部选择「拍照识别」「相册识别」) |
| 点击取景区域 | 在取景画面上点击任意位置,显示白色对焦框并以其为中心调整预览缩放原点 |
| 变焦控制 | 取景区右侧「− / 倍数 / +」按钮,支持 1.0×~3.0× 数码变焦,步进 0.1× |
| 提示文案 | 取景区底部展示「点击画面调焦」;完成拍摄后提示可隐藏 |
| 重新拍摄 | 点击「重新拍摄」后保留当前会话,重置对焦点与变焦倍数 |
+ | 触发点 | 校验函数 | 校验项 | 失败行为 |
|---|---|---|---|
| 步骤一「下一步」 | dvValidateVehicleStep | 车牌;有广告须拍照;驾驶培训完成;里程/氢量/电量;非 GPS 须定位 | message.warning,停留当前步 |
| 步骤二「下一步」 | dvValidateInspectionStep | 轮胎胎纹深度(无备胎跳过备胎行) | 同上 |
| 连拍下一张 | 相机页内校验 | 轮胎项 OCR 须成功(treadOcrOk) | 禁止继续 |
| 发送签章文件 | validateDeliveryBeforeSign | 车牌;里程/氢量/电量;必填照片齐全;被授权人已选 | 拦截并可能跳转照片步 |
| 手动记录培训 | generateDriverTrainingCode / refreshDriverTrainingStatus | 运维录入信息齐全后生成培训码;司机签字后刷新完成 | 拦截 |
persistDeliveryDraft(true) 静默保存,状态由「未开始」变为「已保存」(若尚未进入签章流程)。
+| 字段 | 类型 | 说明 |
|---|---|---|
| plateNo | string | 交车车牌 |
| rearEquip / hasAd / hasTailgate | object / string | 后装设备配置 |
| adPhotoUploaded / bigWordPhotoUploaded | boolean | 广告/放大字照片 |
| spareTire | '有' | '无' | 是否备胎 |
| driverTrainingDone / driverTraining | boolean / string | 驾驶培训状态 |
| deliveryMileage / deliveryH2 / deliveryElec | number|string | 交车数据 |
| deliveryH2Unit | '%' | 'MPa' | 氢量单位,随车型 |
| deliveryLocation | {lat,lng,address,source} | 交车位置 |
| inspectionList | array | 检查单行 {key,category,item,checked,treadDepth,remark} |
| deliveryPhotos | object | 照片记录 keyed by photoKey |
| authorizedPersonId/Name/Phone | string | 被授权人 |
| signSent | boolean | 是否已发送签章 |
| deliveryStatus | enum | 交车状态 |
| 依赖 | 用途 | 原型阶段 |
|---|---|---|
| 车辆 GPS | 在线车辆自动取坐标 | 按备车数据 onlineStatus 模拟 |
| 手机定位 | 离线车辆获取当前位置 | 模拟坐标写入 deliveryLocation |
| OCR 车牌 | 识别车牌匹配车辆 | 演示车牌轮询 |
| OCR 胎纹 | 轮胎/备胎胎纹深度 | 演示读数;空值模拟失败 |
| 文件上传 | 照片上传 + 水印 | simulate 上传,写入 watermark 字段 |
| E签宝 | 运维签字 + 客户短信签章 | 内嵌签字页 + 模拟客户签章按钮 |
| 微信扫码 | 驾驶培训视频 | 静态培训二维码 |
以下截图为小羚羚交车原型实际界面,与 V1.1 需求一致,供研发与测试对照。
+ +






