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 ( -
+
车辆信息
@@ -2836,7 +3869,7 @@ const AnnualReviewModule = ({ onRegisterBack }) => {
检测服务站信息
检测服务站 - setForm((f) => ({ ...f, station: e.target.value }))} placeholder="请选择" /> + setForm((f) => ({ ...f, station: e.target.value }))} placeholder="请选择" />
费用(¥) @@ -2907,6 +3940,60 @@ const AnnualReviewModule = ({ onRegisterBack }) => { ); }; +const DV_EQUIP_PHOTO_DEMO = { + ad: 'https://picsum.photos/seed/dv-ad-photo/400/300', + bigWord: 'https://picsum.photos/seed/dv-bigword-photo/400/300', +}; + +const DvCameraViewfinder = ({ + photoUrl, + alt = '', + placeholder = '相机取景中…', + zoom = 1, + onZoomChange, + focusPoint, + onFocusTap, + onImgError, + showFocusTip = true, +}) => { + const wrapRef = useRef(null); + const handleTap = (e) => { + if (!onFocusTap || !wrapRef.current) return; + const rect = wrapRef.current.getBoundingClientRect(); + if (!rect.width || !rect.height) return; + onFocusTap({ + x: Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)), + y: Math.max(0, Math.min(100, ((e.clientY - rect.top) / rect.height) * 100)), + }); + }; + const origin = focusPoint || { x: 50, y: 50 }; + return ( +
+
+ {photoUrl ? ( + {alt} + ) : ( + {placeholder} + )} +
+ {focusPoint ? ( + + ) : null} + {onZoomChange ? ( +
e.stopPropagation()} role="group" aria-label="调焦倍数"> + + {zoom.toFixed(1)}× + +
+ ) : null} + {showFocusTip ? 点击画面调焦 : null} +
+ ); +}; + const DeliveryModule = ({ onRegisterBack }) => { const [orders, setOrders] = useState(DV_MOCK_ORDERS); const [listFilter, setListFilter] = useState('inProgress'); @@ -2914,10 +4001,42 @@ const DeliveryModule = ({ onRegisterBack }) => { const [searchKey, setSearchKey] = useState(''); const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); const [moreFilter, setMoreFilter] = useState(DV_EMPTY_MORE_FILTER); - const [moreFilterDraft, setMoreFilterDraft] = useState(DV_EMPTY_MORE_FILTER); + const [filterDraft, setFilterDraft] = useState(DV_EMPTY_FILTER_DRAFT); const [activeRow, setActiveRow] = useState(null); - const [formStep, setFormStep] = useState(0); const [formDraft, setFormDraft] = useState(null); + const [vehiclePickOpen, setVehiclePickOpen] = useState(false); + const [vehiclePickSearch, setVehiclePickSearch] = useState(''); + const [vehiclePickParking, setVehiclePickParking] = useState('all'); + const [parkingSheetOpen, setParkingSheetOpen] = useState(false); + const [plateValidateModal, setPlateValidateModal] = useState(null); + const [clearSignConfirmOpen, setClearSignConfirmOpen] = useState(false); + const [spareTireCaptureOpen, setSpareTireCaptureOpen] = useState(false); + const [spareTireCaptureDraft, setSpareTireCaptureDraft] = useState({ photoUrl: '', treadDepth: '' }); + const [dvCameraFocus, setDvCameraFocus] = useState(null); + const [dvCameraZoom, setDvCameraZoom] = useState(1); + const [dvPhotoCamera, setDvPhotoCamera] = useState(null); + const [photoSourceSheet, setPhotoSourceSheet] = useState(null); + const [driverManualOpen, setDriverManualOpen] = useState(false); + const [driverManualStep, setDriverManualStep] = useState('form'); + const [driverETrainingOpen, setDriverETrainingOpen] = useState(false); + const [driverManualDraft, setDriverManualDraft] = useState(DV_DRIVER_MANUAL_EMPTY); + const [deliveryFormStep, setDeliveryFormStep] = useState('vehicle'); + const [photoCapturePhase, setPhotoCapturePhase] = useState('ready'); + const [photoCaptureMode, setPhotoCaptureMode] = useState('continuous'); + const [photoCaptureIndex, setPhotoCaptureIndex] = useState(0); + const [photoCountdown, setPhotoCountdown] = useState(3); + const [photoCameraDraft, setPhotoCameraDraft] = useState({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + const [photoViewer, setPhotoViewer] = useState(null); + const [deliverySignFlow, setDeliverySignFlow] = useState(null); + const [deliverySignSuccessCd, setDeliverySignSuccessCd] = useState(3); + const deliverySignTimerRef = useRef(null); + const photoViewerTouchRef = useRef({ x: 0, y: 0 }); + const ocrDemoIdxRef = useRef(0); + const spareTreadOcrIdxRef = useRef(0); + const photoCaptureBusyRef = useRef(false); + const photoTreadOcrIdxRef = useRef(0); + const deliveryLocAutoRef = useRef(''); + const [locationFetching, setLocationFetching] = useState(false); const rows = useMemo(() => dvFlattenOrders(orders), [orders]); @@ -2935,6 +4054,8 @@ const DeliveryModule = ({ onRegisterBack }) => { return n; }, [moreFilter]); + const activeFilterCount = useMemo(() => activeMoreFilterCount + (statusFilter ? 1 : 0), [activeMoreFilterCount, statusFilter]); + const filteredList = useMemo(() => { const q = searchKey.trim().toUpperCase(); const customerQ = moreFilter.customerName.trim(); @@ -2943,7 +4064,14 @@ const DeliveryModule = ({ onRegisterBack }) => { if (listFilter === 'inProgress') list = list.filter((r) => dvIsInProgressStatus(r.deliveryStatus)); if (listFilter === 'completed') list = list.filter((r) => dvIsHistoryStatus(r.deliveryStatus)); if (statusFilter) list = list.filter((r) => r.deliveryStatus === statusFilter); - if (q) list = list.filter((r) => dvDisplayPlate(r.plateNo).toUpperCase().includes(q)); + if (q) { + list = list.filter((r) => { + const plate = dvDisplayPlate(r.plateNo).toUpperCase(); + const customer = (r.customerName || '').toUpperCase(); + const project = (r.projectName || '').toUpperCase(); + return plate.includes(q) || customer.includes(q) || project.includes(q); + }); + } if (customerQ) list = list.filter((r) => (r.customerName || '').includes(customerQ)); if (projectQ) list = list.filter((r) => (r.projectName || '').includes(projectQ)); if (moreFilter.dateStart || moreFilter.dateEnd) { @@ -2952,11 +4080,31 @@ const DeliveryModule = ({ onRegisterBack }) => { return list; }, [rows, listFilter, statusFilter, searchKey, moreFilter]); - const readOnly = activeRow && (dvIsHistoryStatus(activeRow.deliveryStatus) || activeRow.deliveryStatus === '待客户签章'); - const plateOptions = useMemo(() => { - const addr = activeRow?.deliveryAddress || ''; - return DV_RESERVE_PLATES.filter((p) => !addr || p.parkingLot === addr || addr.indexOf(p.parkingLot.slice(0, 2)) >= 0); - }, [activeRow]); + const isHistoryView = !!(activeRow && dvIsHistoryStatus(activeRow.deliveryStatus)); + const isSignPendingView = !!(activeRow && activeRow.deliveryStatus === '待客户签章' && formDraft?.deliveryStatus !== '待重新签章'); + const isResignPendingView = !!(activeRow && (activeRow.deliveryStatus === '待重新签章' || formDraft?.deliveryStatus === '待重新签章')); + const readOnly = isHistoryView || isSignPendingView || isResignPendingView; + const canEditAuthorizedPerson = isResignPendingView; + + const vehiclePickList = useMemo(() => { + const q = vehiclePickSearch.trim().toUpperCase(); + let list = DV_DELIVERY_PICK_VEHICLES.filter((v) => v.vehicleStatus === '已备车'); + if (vehiclePickParking && vehiclePickParking !== 'all') { + list = list.filter((v) => v.parkingKey === vehiclePickParking); + } + if (q) list = list.filter((v) => v.plateNo.toUpperCase().includes(q)); + return list.sort((a, b) => { + const ab = dvGetReadinessMeta(a).blocked ? 1 : 0; + const bb = dvGetReadinessMeta(b).blocked ? 1 : 0; + if (ab !== bb) return ab - bb; + return a.plateNo.localeCompare(b.plateNo, 'zh-CN'); + }); + }, [vehiclePickSearch, vehiclePickParking]); + + const vehiclePickParkingLabel = useMemo(() => { + const found = DV_OPERATOR_PARKING_LOTS.find((p) => p.key === vehiclePickParking); + return found ? found.label : '全部停车场'; + }, [vehiclePickParking]); const patchRow = useCallback((rowId, patch) => { setOrders((prev) => prev.map((order) => { @@ -2972,17 +4120,61 @@ const DeliveryModule = ({ onRegisterBack }) => { })); }, []); - const openRow = useCallback((row) => { - setActiveRow(row); - setFormDraft(dvBuildEmptyForm(row)); - setFormStep(0); + const resetDvCameraAssist = useCallback(() => { + setDvCameraFocus(null); + setDvCameraZoom(1); }, []); + const clearDeliverySignTimer = useCallback(() => { + if (deliverySignTimerRef.current) { + window.clearTimeout(deliverySignTimerRef.current); + deliverySignTimerRef.current = null; + } + }, []); + + const resetDeliverySignFlow = useCallback(() => { + clearDeliverySignTimer(); + setDeliverySignFlow(null); + setDeliverySignSuccessCd(3); + }, [clearDeliverySignTimer]); + + const openRow = useCallback((row) => { + setActiveRow(row); + const viewOnly = row.deliveryStatus === '待客户签章' || row.deliveryStatus === '待重新签章' || row.deliveryStatus === '客户已签章'; + const resignPending = row.deliveryStatus === '待重新签章'; + let form = dvBuildEmptyForm(row); + if (viewOnly) { + form = { ...form, deliveryPhotos: dvEnsureViewDeliveryPhotos(row, form) }; + } + setFormDraft(form); + setDeliveryFormStep(resignPending ? 'photos' : 'vehicle'); + setPhotoCapturePhase(viewOnly ? 'done' : 'ready'); + setPhotoCaptureMode('continuous'); + setPhotoCaptureIndex(0); + setPhotoCountdown(3); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + resetDeliverySignFlow(); + }, [resetDeliverySignFlow]); + const closeForm = useCallback(() => { + resetDeliverySignFlow(); setActiveRow(null); setFormDraft(null); - setFormStep(0); - }, []); + setDeliveryFormStep('vehicle'); + setPhotoCapturePhase('ready'); + setPhotoCaptureMode('continuous'); + setPhotoCaptureIndex(0); + setPhotoCountdown(3); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + setPhotoViewer(null); + deliveryLocAutoRef.current = ''; + setVehiclePickOpen(false); + setVehiclePickSearch(''); + setVehiclePickParking('all'); + setParkingSheetOpen(false); + setPlateValidateModal(null); + setClearSignConfirmOpen(false); + }, [resetDeliverySignFlow]); const syncActiveRow = useCallback(() => { if (!activeRow) return; @@ -2992,299 +4184,2458 @@ const DeliveryModule = ({ onRegisterBack }) => { useEffect(() => { syncActiveRow(); }, [orders, syncActiveRow]); - const handlePlatePick = useCallback((plateNo) => { - const picked = DV_RESERVE_PLATES.find((p) => p.plateNo === plateNo); - setFormDraft((f) => ({ - ...f, - plateNo, - brand: picked?.brand || f.brand, - model: picked?.model || f.model, - vin: picked?.vin || f.vin, - })); + const handlePlatePick = useCallback((vehicle) => { + if (!vehicle) return; + const meta = dvGetReadinessMeta(vehicle); + if (!meta.canPick) { + message.warning(meta.label); + return; + } + deliveryLocAutoRef.current = ''; + setFormDraft((f) => dvMergeVehicleIntoForm(f, vehicle, activeRow)); + setVehiclePickOpen(false); + setVehiclePickSearch(''); + message.success(`已选择 ${vehicle.plateNo}`); + }, [activeRow]); + + const applyRecognizedVehicle = useCallback((vehicle) => { + deliveryLocAutoRef.current = ''; + setFormDraft((f) => dvMergeVehicleIntoForm(f, vehicle, activeRow)); + message.success(`已识别 ${vehicle.plateNo}`); + }, [activeRow]); + + const handleScanPickupCode = useCallback(() => { + if (!activeRow || !formDraft) return; + const heavy = dvIsHeavyVehicle(activeRow.vehicleType || formDraft.vehicleType, formDraft.model || activeRow.model); + const hide = message.loading('正在识别提车码…', 0); + window.setTimeout(() => { + hide(); + const info = dvMockDriverTrainingInfo(heavy); + setFormDraft((f) => ({ + ...f, + driverTrainingDone: true, + driverTraining: '已完成', + ...info, + })); + setDriverETrainingOpen(false); + message.success('驾驶培训已完成'); + }, 900); + }, [activeRow, formDraft]); + + const openDriverETraining = useCallback(() => { + setDriverETrainingOpen(true); }, []); - const handleSave = useCallback(() => { - if (!activeRow || !formDraft) return; - patchRow(activeRow.id, { - ...formDraft, - deliveryMileage: formDraft.deliveryMileage === '' ? null : Number(formDraft.deliveryMileage), - deliveryH2: formDraft.deliveryH2 === '' ? null : Number(formDraft.deliveryH2), - deliveryElec: formDraft.deliveryElec === '' ? null : Number(formDraft.deliveryElec), - deliveryTime: formDraft.deliveryTime ? formDraft.deliveryTime.replace('T', ' ') : '', - deliveryStatus: '已保存', - }); - message.success('交车单已保存(原型)'); - }, [activeRow, formDraft, patchRow]); + const openDriverManualRecord = useCallback(() => { + if (!formDraft) return; + const draft = { + driverPhone: formDraft.driverPhone || '', + driverName: formDraft.driverName || '', + driverIdNo: formDraft.driverIdNo || '', + driverIdFront: !!formDraft.driverIdFront, + driverIdBack: !!formDraft.driverIdBack, + driverLicenseFront: !!formDraft.driverLicenseFront, + driverLicenseBack: !!formDraft.driverLicenseBack, + driverQualification: !!formDraft.driverQualification, + driverFrontPhoto: !!formDraft.driverFrontPhoto, + driverIdFrontUrl: formDraft.driverIdFrontUrl || '', + driverIdBackUrl: formDraft.driverIdBackUrl || '', + driverLicenseFrontUrl: formDraft.driverLicenseFrontUrl || '', + driverLicenseBackUrl: formDraft.driverLicenseBackUrl || '', + driverQualificationUrl: formDraft.driverQualificationUrl || '', + driverFrontPhotoUrl: formDraft.driverFrontPhotoUrl || '', + }; + setDriverManualDraft(draft); + setDriverManualStep(formDraft.driverTrainingPending && formDraft.driverTrainingCodeGenerated ? 'qr' : 'form'); + setDriverManualOpen(true); + }, [formDraft]); - const handleSubmit = useCallback(() => { + const closeDriverManualRecord = useCallback(() => { + setDriverManualOpen(false); + setDriverManualStep('form'); + }, []); + + const uploadDriverManualPhoto = useCallback((field, url) => { + setDriverManualDraft((d) => ({ ...d, [field]: true, [`${field}Url`]: url })); + message.success('已上传(原型)'); + }, []); + + const generateDriverTrainingCode = useCallback(() => { if (!activeRow || !formDraft) return; - if (!formDraft.plateNo) { - message.warning('请先选择交车车牌'); - setFormStep(0); - return; - } - if (!formDraft.deliveryMileage || !formDraft.deliveryH2 || !formDraft.deliveryElec) { - message.warning('请填写交车里程、氢量与电量'); - setFormStep(2); + const heavy = dvIsHeavyVehicle(activeRow.vehicleType || formDraft.vehicleType, formDraft.model || activeRow.model); + const result = dvValidateDriverManualDraft(driverManualDraft, heavy); + if (!result.ok) { + message.warning(result.message); return; } + setFormDraft((f) => ({ + ...f, + ...driverManualDraft, + driverTrainingCodeGenerated: true, + driverTrainingPending: true, + driverTrainingDone: false, + driverTraining: '待司机确认', + })); + setDriverManualStep('qr'); + message.success('培训码已生成,请司机微信扫码完成培训'); + }, [activeRow, formDraft, driverManualDraft]); + + const refreshDriverTrainingStatus = useCallback(() => { + if (!activeRow || !formDraft) return; + const hide = message.loading('正在查询培训状态…', 0); + window.setTimeout(() => { + hide(); + setFormDraft((f) => ({ + ...f, + ...driverManualDraft, + driverTrainingPending: false, + driverTrainingCodeGenerated: true, + driverTrainingDone: true, + driverTraining: '已完成', + })); + closeDriverManualRecord(); + message.success('司机已完成培训签字,信息已同步至安全培训记录(原型)'); + }, 900); + }, [activeRow, formDraft, driverManualDraft, closeDriverManualRecord]); + + const runPlateRecognize = useCallback(() => { + const hide = message.loading('正在识别车牌…', 0); + const plate = DV_OCR_DEMO_PLATES[ocrDemoIdxRef.current % DV_OCR_DEMO_PLATES.length]; + ocrDemoIdxRef.current += 1; + window.setTimeout(() => { + hide(); + const vehicle = dvFindRecognizeVehicle(plate); + const result = dvValidateRecognizedPlate(vehicle); + if (result.ok && vehicle) { + applyRecognizedVehicle(vehicle); + return; + } + setPlateValidateModal({ plateNo: plate, messages: result.messages }); + }, 900); + }, [applyRecognizedVehicle]); + + const handleRecognizePlate = useCallback(() => { + setPhotoSourceSheet({ + title: '识别车牌号', + options: [ + { + key: 'camera', + label: '拍照识别', + desc: '调用相机拍摄车牌', + onSelect: () => { + message.info('调用相机拍摄车牌(原型)'); + window.setTimeout(() => runPlateRecognize(), 480); + }, + }, + { + key: 'album', + label: '相册识别', + desc: '从相册选择车牌照片', + onSelect: () => { + const hide = message.loading('正在打开相册…', 0); + window.setTimeout(() => { + hide(); + message.success('已从相册选择车牌照片(原型)'); + runPlateRecognize(); + }, 420); + }, + }, + ], + }); + }, [runPlateRecognize]); + + const renderPlateValidateModal = () => { + if (!plateValidateModal) return null; + const { plateNo, messages } = plateValidateModal; + return ( +
+ +
+
+ ); + }; + + const renderClearSignConfirmOverlay = () => { + if (!clearSignConfirmOpen) return null; + return ( +
+ + +
+
+
+ ); + }; + + const buildFormPatch = useCallback((draft) => ({ + ...draft, + deliveryMileage: dvParseMetric2(draft.deliveryMileage), + deliveryH2: dvParseMetric2(draft.deliveryH2), + deliveryElec: dvParseMetric2(draft.deliveryElec), + serviceFee: draft.serviceFee === '' ? null : dvParseMetric2(draft.serviceFee), + deliveryLocation: draft.deliveryLocation || null, + authorizedPersonId: draft.authorizedPersonId || '', + authorizedPersonName: draft.authorizedPersonName || '', + authorizedPersonPhone: draft.authorizedPersonPhone || '', + signSent: !!draft.signSent, + }), []); + + const completeDeliveryOpsSign = useCallback((draft) => { + if (!activeRow || !draft) return; patchRow(activeRow.id, { - ...formDraft, - deliveryMileage: Number(formDraft.deliveryMileage), - deliveryH2: Number(formDraft.deliveryH2), - deliveryElec: Number(formDraft.deliveryElec), - deliveryTime: formDraft.deliveryTime ? formDraft.deliveryTime.replace('T', ' ') : '2026-06-04 10:00', + ...buildFormPatch({ ...draft, signSent: true }), + deliveryTime: dvFormatOpsSignTime(), deliveryPerson: MOCK_USER, deliveryStatus: '待客户签章', }); - message.success('已提交,等待客户签章(原型)'); - closeForm(); - }, [activeRow, formDraft, patchRow, closeForm]); + setFormDraft((d) => (d ? { ...d, signSent: true, deliveryStatus: '待客户签章' } : d)); + }, [activeRow, patchRow, buildFormPatch]); + + const initPhotoCaptureOnEnter = useCallback((photos, draft) => { + const seq = dvGetCaptureSequence(draft); + const nextIdx = dvGetNextCaptureIndex(photos, draft); + const allDone = nextIdx >= seq.length; + const capturedCount = dvCountCapturedPhotos(photos, draft); + + setPhotoCaptureIndex(allDone ? 0 : nextIdx); + setPhotoCountdown(3); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + + if (allDone) { + setPhotoCaptureMode('continuous'); + setPhotoCapturePhase('done'); + return; + } + setPhotoCaptureMode('continuous'); + if (capturedCount > 0) { + setPhotoCapturePhase('countdown'); + return; + } + setPhotoCapturePhase('ready'); + }, []); + + const validateDeliveryBeforeSign = useCallback(() => { + if (!activeRow || !formDraft) return false; + if (!formDraft.plateNo) { + message.warning('请先选择交车车辆'); + return false; + } + if (formDraft.deliveryMileage === '' || formDraft.deliveryH2 === '' || formDraft.deliveryElec === '') { + message.warning('请填写里程、氢量与电量'); + return false; + } + const requiredPhotos = dvGetCaptureSequence(formDraft).filter((item) => dvIsPhotoItemRequired(item)); + const missingPhoto = requiredPhotos.find((item) => !dvPhotoCaptured(formDraft.deliveryPhotos, item.key)); + if (missingPhoto) { + message.warning(`请先完成交车照片连续拍摄(缺少:${missingPhoto.label})`); + setDeliveryFormStep('photos'); + initPhotoCaptureOnEnter(formDraft.deliveryPhotos || {}, formDraft); + return false; + } + if (!formDraft.authorizedPersonId) { + message.warning('请选择被授权人'); + return false; + } + return true; + }, [activeRow, formDraft, initPhotoCaptureOnEnter]); + + const handleAuthorizedPersonPick = useCallback((person) => { + if (!person || !canEditAuthorizedPerson) return; + setFormDraft((d) => ({ + ...d, + authorizedPersonId: person.id, + authorizedPersonName: person.name, + authorizedPersonPhone: person.phone, + })); + }, [canEditAuthorizedPerson]); + + const persistDeliveryDraft = useCallback((silent = true) => { + if (!activeRow || !formDraft) return false; + const nextStatus = formDraft.deliveryStatus === '待重新签章' + ? '待重新签章' + : (!formDraft.deliveryStatus || formDraft.deliveryStatus === '未开始') ? '已保存' : formDraft.deliveryStatus; + patchRow(activeRow.id, { + ...buildFormPatch(formDraft), + deliveryStatus: nextStatus, + }); + if (!silent) message.success('交车单已保存(原型)'); + return true; + }, [activeRow, formDraft, patchRow, buildFormPatch]); + + const handleSendSignDoc = useCallback(() => { + if (!validateDeliveryBeforeSign() || !activeRow || !formDraft || deliverySignFlow) return; + const nextDraft = { ...formDraft, signSent: true }; + setFormDraft(nextDraft); + if (!formDraft.signSent) { + const interimStatus = formDraft.deliveryStatus === '待重新签章' + ? '待重新签章' + : formDraft.deliveryStatus === '待客户签章' + ? '待客户签章' + : '已保存'; + patchRow(activeRow.id, { + ...buildFormPatch(nextDraft), + deliveryStatus: interimStatus, + }); + } + message.info('跳转E签宝完成运维人员签字后跳转成功页'); + setDeliverySignFlow('jumping'); + clearDeliverySignTimer(); + deliverySignTimerRef.current = window.setTimeout(() => { + deliverySignTimerRef.current = null; + completeDeliveryOpsSign(nextDraft); + setDeliverySignFlow('success'); + setDeliverySignSuccessCd(3); + }, 3000); + }, [validateDeliveryBeforeSign, activeRow, formDraft, deliverySignFlow, patchRow, buildFormPatch, clearDeliverySignTimer, completeDeliveryOpsSign]); + + useEffect(() => () => clearDeliverySignTimer(), [clearDeliverySignTimer]); + + useEffect(() => { + if (deliverySignFlow !== 'success') return undefined; + setDeliverySignSuccessCd(3); + const tick = window.setInterval(() => { + setDeliverySignSuccessCd((c) => (c > 1 ? c - 1 : 1)); + }, 1000); + const autoClose = window.setTimeout(() => closeForm(), 3000); + return () => { + window.clearInterval(tick); + window.clearTimeout(autoClose); + }; + }, [deliverySignFlow, closeForm]); + + const performClearSign = useCallback(() => { + if (!activeRow || !formDraft) return; + const cleared = { + ...formDraft, + signSent: false, + deliveryStatus: '待重新签章', + authorizedPersonId: '', + authorizedPersonName: '', + authorizedPersonPhone: '', + }; + setFormDraft(cleared); + patchRow(activeRow.id, { + ...buildFormPatch(cleared), + signSent: false, + deliveryStatus: '待重新签章', + }); + setDeliveryFormStep('photos'); + setPhotoCapturePhase('done'); + message.success('已清除签章,请重新选择被授权人后发起签章'); + }, [activeRow, formDraft, patchRow, buildFormPatch]); + + const handleClearSign = useCallback(() => { + if (!activeRow || !formDraft) return; + setClearSignConfirmOpen(true); + }, [activeRow, formDraft]); + + const handleDownloadSignFile = useCallback(() => { + message.success('下载签章 PDF(原型)'); + }, []); + + const handlePreviewSignFile = useCallback(() => { + message.info('预览签章 PDF(原型)'); + }, []); + + const handleSave = useCallback(() => { + persistDeliveryDraft(false); + }, [persistDeliveryDraft]); + + const handleVehicleNext = useCallback(() => { + const result = dvValidateVehicleStep(formDraft, activeRow); + if (!result.ok) { + message.warning(result.message); + return; + } + persistDeliveryDraft(true); + setDeliveryFormStep('inspection'); + }, [formDraft, activeRow, persistDeliveryDraft]); + + const handleInspectionNext = useCallback(() => { + const result = dvValidateInspectionStep(formDraft); + if (!result.ok) { + message.warning(result.message); + return; + } + persistDeliveryDraft(true); + const photos = formDraft?.deliveryPhotos || {}; + initPhotoCaptureOnEnter(photos, formDraft); + setDeliveryFormStep('photos'); + }, [formDraft, initPhotoCaptureOnEnter, persistDeliveryDraft]); + + const getCurrentCaptureItem = useCallback(() => { + const seq = dvGetCaptureSequence(formDraft); + return seq[photoCaptureIndex] || null; + }, [formDraft, photoCaptureIndex]); + + const simulateCameraShutter = useCallback(() => { + const item = getCurrentCaptureItem(); + if (!item) return; + let tread = ''; + let treadOcrOk = true; + if (item.tread) { + const ocrIdx = photoTreadOcrIdxRef.current % DV_SPARE_TREAD_OCR_DEMO.length; + tread = DV_SPARE_TREAD_OCR_DEMO[ocrIdx]; + photoTreadOcrIdxRef.current += 1; + treadOcrOk = !!String(tread || '').trim(); + } + setPhotoCameraDraft({ + photoUrl: dvGetPhotoDemoUrl(item.key), + treadDepth: tread, + treadOcrOk, + captured: true, + }); + if (item.tread) { + if (treadOcrOk) { + message.success(`已识别胎纹深度 ${tread} mm,可编辑后点击完成`); + } else { + message.warning('胎纹识别失败,请重新拍摄'); + } + } else { + message.success(`已拍摄「${item.label}」(原型)`); + } + }, [getCurrentCaptureItem]); + + const enterCameraPhase = useCallback(() => { + resetDvCameraAssist(); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + setPhotoCapturePhase('camera'); + }, [resetDvCameraAssist]); + + const advancePhotoCapture = useCallback(() => { + const seq = dvGetCaptureSequence(formDraft); + const item = seq[photoCaptureIndex]; + if (!item) return; + const upload = dvSimulatePhotoUpload(item.key, photoCameraDraft, formDraft, activeRow); + setFormDraft((d) => ({ + ...d, + deliveryPhotos: { + ...(d.deliveryPhotos || {}), + [item.key]: { + captured: true, + photoUrl: upload.photoUrl, + treadDepth: item.tread ? String(photoCameraDraft.treadDepth || '').trim() : '', + uploaded: true, + watermarkTime: upload.watermarkTime, + watermarkAddress: upload.watermarkAddress, + }, + }, + })); + if (photoCaptureMode === 'single') { + setPhotoCapturePhase('done'); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + return; + } + const nextIdx = photoCaptureIndex + 1; + if (nextIdx >= seq.length) { + setPhotoCapturePhase('done'); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + } else { + setPhotoCaptureIndex(nextIdx); + setPhotoCountdown(3); + setPhotoCapturePhase('countdown'); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + } + }, [formDraft, photoCaptureIndex, photoCameraDraft, photoCaptureMode, activeRow]); + + const startPhotoCaptureAtKey = useCallback((photoKey) => { + const seq = dvGetCaptureSequence(formDraft); + const idx = seq.findIndex((item) => item.key === photoKey); + if (idx < 0) return; + setPhotoCaptureMode('single'); + setPhotoCaptureIndex(idx); + setPhotoCountdown(3); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + setPhotoCapturePhase('countdown'); + }, [formDraft]); + + const startPhotoCapture = useCallback(() => { + const photos = formDraft?.deliveryPhotos || {}; + const seq = dvGetCaptureSequence(formDraft); + const nextIdx = dvGetNextCaptureIndex(photos, formDraft); + if (nextIdx >= seq.length) return; + setPhotoCaptureMode('continuous'); + setPhotoCaptureIndex(nextIdx); + setPhotoCountdown(3); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + setPhotoCapturePhase('countdown'); + }, [formDraft]); + + const confirmPhotoCameraCapture = useCallback(() => { + const item = getCurrentCaptureItem(); + if (!item) return; + if (!photoCameraDraft.captured || !photoCameraDraft.photoUrl) { + message.warning('请先拍照或从相册选择照片'); + return; + } + if (item.tread) { + if (!photoCameraDraft.treadOcrOk || !String(photoCameraDraft.treadDepth || '').trim()) { + message.warning('胎纹未识别成功,请重新拍摄后再继续'); + return; + } + } + advancePhotoCapture(); + }, [getCurrentCaptureItem, photoCameraDraft, advancePhotoCapture]); + + useEffect(() => { + if (photoCapturePhase !== 'countdown') return undefined; + if (photoCountdown <= 0) { + enterCameraPhase(); + return undefined; + } + const timer = window.setTimeout(() => setPhotoCountdown((c) => c - 1), 1000); + return () => window.clearTimeout(timer); + }, [photoCapturePhase, photoCountdown, enterCameraPhase]); + + const capturePhotoNow = useCallback(() => { + if (photoCapturePhase === 'countdown') { + enterCameraPhase(); + return; + } + if (photoCapturePhase === 'camera') { + confirmPhotoCameraCapture(); + } + }, [photoCapturePhase, enterCameraPhase, confirmPhotoCameraCapture]); + + const updateInspectionRow = useCallback((key, patch) => { + setFormDraft((d) => ({ + ...d, + inspectionList: (d.inspectionList || []).map((r) => (r.key === key ? { ...r, ...patch } : r)), + })); + }, []); + + const handleMetricFieldChange = useCallback((field, raw) => { + const next = dvMetricInputChange(raw); + if (next === null) return; + setFormDraft((d) => ({ ...d, [field]: next })); + }, []); + + const applyCurrentDeliveryLocation = useCallback((loc) => { + setFormDraft((d) => ({ + ...d, + deliveryLocation: { + lat: loc.lat, + lng: loc.lng, + address: loc.address || '当前定位', + source: 'current', + }, + })); + }, []); + + const handleGetCurrentLocation = useCallback((silent) => { + if (!formDraft?.plateNo || dvVehicleHasGpsDevice(formDraft.plateNo)) return; + setLocationFetching(true); + const finish = (loc, toast) => { + applyCurrentDeliveryLocation(loc); + setLocationFetching(false); + if (toast) message.success(toast); + }; + const fallback = () => finish({ + lat: 30.7286, + lng: 121.0125, + address: '当前定位(运维手机)', + }, silent ? null : '已获取当前定位(原型)'); + + if (typeof navigator !== 'undefined' && navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (pos) => finish({ + lat: Math.round(pos.coords.latitude * 10000) / 10000, + lng: Math.round(pos.coords.longitude * 10000) / 10000, + address: '当前定位(运维手机)', + }, silent ? null : '已获取当前定位'), + () => fallback(), + { enableHighAccuracy: true, timeout: 8000, maximumAge: 60000 } + ); + return; + } + fallback(); + }, [formDraft, applyCurrentDeliveryLocation]); + + useEffect(() => { + if (!formDraft?.plateNo || readOnly) return undefined; + if (dvVehicleHasGpsDevice(formDraft.plateNo)) return undefined; + if (formDraft.deliveryLocation?.source === 'current') return undefined; + if (deliveryLocAutoRef.current === formDraft.plateNo) return undefined; + deliveryLocAutoRef.current = formDraft.plateNo; + handleGetCurrentLocation(true); + return undefined; + }, [formDraft?.plateNo, formDraft?.deliveryLocation?.source, readOnly, handleGetCurrentLocation]); + + const openDeliveryPhotoViewer = useCallback((categoryKey, photoKey) => { + const photos = formDraft?.deliveryPhotos || {}; + const items = dvBuildCategoryViewerItems(categoryKey, photos, formDraft); + const index = items.findIndex((item) => item.key === photoKey); + if (index < 0) return; + setPhotoViewer({ + categoryKey, + index, + items, + categoryLabel: dvPhotoCategoryLabel(categoryKey), + }); + }, [formDraft]); + + useEffect(() => { + if (typeof window === 'undefined') return undefined; + window.__DV_CAPTURE__ = { + openDeliveryCameraDemo: () => { + if (!formDraft) return false; + setDeliveryFormStep('photos'); + setPhotoCaptureIndex(0); + setPhotoCapturePhase('camera'); + setPhotoCameraDraft({ + photoUrl: dvGetPhotoDemoUrl('dashboard'), + treadDepth: '', + treadOcrOk: false, + captured: true, + }); + setDvCameraFocus({ x: 42, y: 38 }); + setDvCameraZoom(1.4); + return true; + }, + }; + return () => { delete window.__DV_CAPTURE__; }; + }, [formDraft]); useEffect(() => { if (!onRegisterBack) return undefined; onRegisterBack(() => { + if (deliverySignFlow === 'success') { closeForm(); return true; } + if (photoViewer) { setPhotoViewer(null); return true; } + if (dvPhotoCamera) { setDvPhotoCamera(null); resetDvCameraAssist(); return true; } + if (spareTireCaptureOpen) { setSpareTireCaptureOpen(false); resetDvCameraAssist(); return true; } + if (photoSourceSheet) { setPhotoSourceSheet(null); return true; } + if (driverManualOpen) { + if (driverManualStep === 'qr') { setDriverManualStep('form'); return true; } + closeDriverManualRecord(); + return true; + } + if (driverETrainingOpen) { setDriverETrainingOpen(false); return true; } + if (plateValidateModal) { setPlateValidateModal(null); return true; } + if (clearSignConfirmOpen) { setClearSignConfirmOpen(false); return true; } + if (filterDrawerOpen) { setFilterDrawerOpen(false); return true; } + if (parkingSheetOpen) { setParkingSheetOpen(false); return true; } + if (vehiclePickOpen) { setVehiclePickOpen(false); setParkingSheetOpen(false); return true; } + if (activeRow && deliveryFormStep === 'photos' && (photoCapturePhase === 'countdown' || photoCapturePhase === 'camera')) { + if (photoCapturePhase === 'camera') { + resetDvCameraAssist(); + setPhotoCapturePhase('countdown'); + setPhotoCameraDraft({ photoUrl: '', treadDepth: '', treadOcrOk: false, captured: false }); + } else { + setPhotoCapturePhase('ready'); + setPhotoCountdown(3); + } + return true; + } if (activeRow) { closeForm(); return true; } return false; }); return () => onRegisterBack(null); - }, [activeRow, closeForm, onRegisterBack]); + }, [activeRow, vehiclePickOpen, parkingSheetOpen, filterDrawerOpen, plateValidateModal, clearSignConfirmOpen, photoSourceSheet, spareTireCaptureOpen, dvPhotoCamera, driverManualOpen, driverManualStep, driverETrainingOpen, deliveryFormStep, photoCapturePhase, photoViewer, deliverySignFlow, closeForm, closeDriverManualRecord, resetDvCameraAssist, onRegisterBack]); - const renderFormField = (label, value, editor) => ( -
- {label} - {readOnly || !editor ? {value || '—'} : editor} + const runAlbumPick = useCallback((applyPhoto, label) => { + const hide = message.loading('正在打开相册…', 0); + window.setTimeout(() => { + hide(); + applyPhoto(); + message.success(`已从相册选择${label}(原型)`); + }, 420); + }, []); + + const pickContinuousPhotoFromAlbum = useCallback(() => { + const item = getCurrentCaptureItem(); + if (!item) return; + runAlbumPick(() => { + let tread = ''; + let treadOcrOk = true; + if (item.tread) { + const ocrIdx = photoTreadOcrIdxRef.current % DV_SPARE_TREAD_OCR_DEMO.length; + tread = DV_SPARE_TREAD_OCR_DEMO[ocrIdx]; + photoTreadOcrIdxRef.current += 1; + treadOcrOk = !!String(tread || '').trim(); + } + setPhotoCameraDraft({ + photoUrl: dvGetPhotoDemoUrl(item.key), + treadDepth: tread, + treadOcrOk, + captured: true, + }); + if (item.tread && !treadOcrOk) { + message.warning('胎纹识别失败,请重新选择照片'); + } + }, `「${item.label}」`); + }, [getCurrentCaptureItem, runAlbumPick]); + + const renderPhotoCaptureActionBar = ({ + captured, + onShutter, + onAlbum, + onComplete, + completeDisabled = false, + }) => ( +
+ {!captured ? ( + <> + + + + ) : ( + <> + + + + + )}
); - const renderStepContent = () => { + const renderPhotoSourceSheet = () => { + if (!photoSourceSheet) return null; + return ( +
+ +
+
+ {photoSourceSheet.options.map((opt) => ( + + ))} +
+
+
+ ); + }; + + const renderRequiredTag = () => (!readOnly ? 必填 : null); + + const renderDriverTrainingQrBlock = ({ title, hint, qrSrc, boundDraft } = {}) => { + const d = boundDraft || null; + return ( +
+ {d ? ( +
+
司机姓名{d.driverName || '—'}
+
手机号{d.driverPhone || '—'}
+
身份证号{d.driverIdNo || '—'}
+
+ ) : null} +
+ 驾驶培训二维码 +
{title || '驾驶培训视频'}
+
{hint || '请司机使用微信扫描二维码,观看完整培训视频'}
+
+ 微信扫一扫 +
+
+
+ ); + }; + + const renderDriverDocThumb = (uploaded, url) => { + if (url) return ; + return uploaded ? '已上传' : '—'; + }; + + const renderDriverManualPhotoSlot = (label, uploaded, url, field, demoUrl, required) => ( +
+
+ {label} + {required ? renderRequiredTag() : null} +
+
{ + openDvPhotoCamera({ + key: field, + label, + demoUrl, + photoUrl: url, + onComplete: () => uploadDriverManualPhoto(field, demoUrl), + }); + }} + onKeyDown={(e) => e.key === 'Enter' && openDvPhotoCamera({ + key: field, + label, + demoUrl, + photoUrl: url, + onComplete: () => uploadDriverManualPhoto(field, demoUrl), + })} + > + {url ? {label} : (uploaded ? '已上传' : '拍照/相册')} +
+
+ ); + + const renderDriverETrainingPage = () => ( +
+
+
+
+
1电子培训
+
司机完成视频培训后,点击下方按钮扫描提车码
+ {renderDriverTrainingQrBlock({ + title: '驾驶培训视频', + hint: '请司机使用微信扫描二维码,观看完整培训视频', + })} +
+
+
+
+ +
+
+
+ ); + + const renderDriverManualPage = () => { + const d = driverManualDraft; + const heavy = dvIsHeavyVehicle(activeRow?.vehicleType || formDraft?.vehicleType, formDraft?.model || activeRow?.model); + const trainingCodeUrl = dvBuildDriverTrainingCodeUrl(d); + const trainingCodeQr = dvDriverTrainingCodeQrUrl(trainingCodeUrl); + + if (driverManualStep === 'qr') { + return ( +
+
+
+
+
1培训码
+
培训码已与司机信息绑定,请司机使用微信扫描二维码完成安全培训与签字
+ {renderDriverTrainingQrBlock({ + title: '安全培训码', + hint: '请司机使用微信扫描二维码;司机签字完成后可点击下方「刷新培训状态」', + qrSrc: trainingCodeQr, + boundDraft: d, + })} +
+
+
+
+ + +
+
+
+ ); + } + + return ( +
+
+
+
+
1司机信息
+
+
+ 手机号{renderRequiredTag()} + setDriverManualDraft((prev) => ({ ...prev, driverPhone: e.target.value }))} + placeholder="请输入" + /> +
+
+ 姓名{renderRequiredTag()} + setDriverManualDraft((prev) => ({ ...prev, driverName: e.target.value }))} + placeholder="请输入" + /> +
+
+ 身份证号{renderRequiredTag()} + setDriverManualDraft((prev) => ({ ...prev, driverIdNo: e.target.value }))} + placeholder="请输入" + /> +
+
+
+
+
2证件照片
+
支持拍照或从相册选择;重卡须上传从业资格证
+
+ {renderDriverManualPhotoSlot('身份证(正面)', d.driverIdFront, d.driverIdFrontUrl, 'driverIdFront', DV_DRIVER_DOC_DEMO.idFront, true)} + {renderDriverManualPhotoSlot('身份证(反面)', d.driverIdBack, d.driverIdBackUrl, 'driverIdBack', DV_DRIVER_DOC_DEMO.idBack, true)} + {renderDriverManualPhotoSlot('驾驶证(正面)', d.driverLicenseFront, d.driverLicenseFrontUrl, 'driverLicenseFront', DV_DRIVER_DOC_DEMO.licenseFront, true)} + {renderDriverManualPhotoSlot('驾驶证(反面)', d.driverLicenseBack, d.driverLicenseBackUrl, 'driverLicenseBack', DV_DRIVER_DOC_DEMO.licenseBack, true)} + {renderDriverManualPhotoSlot('司机正面照片', d.driverFrontPhoto, d.driverFrontPhotoUrl, 'driverFrontPhoto', DV_DRIVER_DOC_DEMO.portrait, true)} + {renderDriverManualPhotoSlot(`从业资格证${heavy ? '' : '(选填)'}`, d.driverQualification, d.driverQualificationUrl, 'driverQualification', DV_DRIVER_DOC_DEMO.qualification, heavy)} +
+
+
+
+
+ +
+ {renderDvPhotoCameraOverlay()} +
+
+ ); + }; + + const renderDvChipGroup = (field, options) => { + if (readOnly) { + const val = formDraft[field] || '—'; + return
{val}
; + } + return ( +
+ {options.map((opt) => ( + + ))} +
+ ); + }; + + const renderDeliverySignSuccessPage = () => { + const person = dvFindAuthorizedPerson(formDraft?.authorizedPersonId); + const personName = person?.name || formDraft?.authorizedPersonName || '—'; + return ( +
+
+
+
运维人员签字成功
+
+ 签章文件已发送,短信已推送至被授权人 {personName},等待客户完成 E签宝签章 +
+
{deliverySignSuccessCd} 秒后自动返回列表
+
+
+ +
+
+ ); + }; + + const renderSectionHead = (num, title, extra, required) => ( +
+ + {num} + {title} + {required ? renderRequiredTag() : null} + + {extra || null} +
+ ); + + const renderDeliveryLocationMap = (plateNo, row, formLocation) => { + const loc = dvResolveDeliveryLocation(plateNo, row, formLocation); + const pending = loc.source === 'pending'; + return ( +
+
+
+
+
+
+
+
+ {!pending ? ( + + + + ) : null} +
+ {loc.address} + {!pending ? {loc.lat.toFixed(4)}, {loc.lng.toFixed(4)} : null} +
+ 腾讯地图 +
+
+ ); + }; + + const renderEquipSwitch = (on, onChange, label) => ( +
+ {readOnly ? ( + {on ? '有' : '无'} + ) : ( + <> + {on ? '有' : '无'} +
+ ); + + const toggleRearEquipAd = useCallback((checked) => { + setFormDraft((d) => { + const base = d.rearEquip || dvGetRearEquipRecord(d.plateNo, activeRow); + return { + ...d, + hasAd: checked ? '有' : '无', + rearEquip: { + ...base, + hasAd: checked, + hasBigWord: checked, + ...(checked ? {} : { adPhotoDone: false, bigWordPhotoDone: false }), + }, + adPhotoUploaded: checked ? d.adPhotoUploaded : false, + bigWordPhotoUploaded: checked ? d.bigWordPhotoUploaded : false, + }; + }); + }, [activeRow]); + + const toggleRearEquipTailgate = useCallback((checked) => { + setFormDraft((d) => { + const base = d.rearEquip || dvGetRearEquipRecord(d.plateNo, activeRow); + return { + ...d, + hasTailgate: checked ? '有' : '无', + rearEquip: { ...base, hasTailgate: checked }, + }; + }); + }, [activeRow]); + + const handleSpareTireChange = useCallback((value) => { + setFormDraft((d) => { + const nextPhotos = { ...(d.deliveryPhotos || {}) }; + if (value !== '有') delete nextPhotos.spare; + return { + ...d, + spareTire: value, + deliveryPhotos: nextPhotos, + ...(value === '有' ? {} : { + spareTirePhotoUploaded: false, + spareTirePhotoUrl: '', + spareTireTreadDepth: '', + }), + }; + }); + }, []); + + const simulateSpareTireCapture = useCallback(() => { + const tread = DV_SPARE_TREAD_OCR_DEMO[spareTreadOcrIdxRef.current % DV_SPARE_TREAD_OCR_DEMO.length]; + spareTreadOcrIdxRef.current += 1; + setSpareTireCaptureDraft({ photoUrl: DV_SPARE_TIRE_DEMO_PHOTO, treadDepth: tread }); + message.success(`已识别胎纹深度 ${tread} mm,可编辑后点击完成`); + }, []); + + const openDvPhotoCamera = useCallback((session) => { + resetDvCameraAssist(); + setDvPhotoCamera({ + ...session, + photoUrl: session.photoUrl || '', + captured: !!session.photoUrl, + }); + if (!session.photoUrl) { + message.info(`拍摄${session.label}(支持拍照或相册)`); + } + }, [resetDvCameraAssist]); + + const shutterDvPhotoCamera = useCallback(() => { + if (!dvPhotoCamera) return; + resetDvCameraAssist(); + setDvPhotoCamera((prev) => (prev ? { ...prev, photoUrl: prev.demoUrl, captured: true } : prev)); + message.success(`已拍摄「${dvPhotoCamera.label}」(原型)`); + }, [dvPhotoCamera, resetDvCameraAssist]); + + const pickDvPhotoFromAlbum = useCallback(() => { + if (!dvPhotoCamera) return; + runAlbumPick(() => { + resetDvCameraAssist(); + setDvPhotoCamera((prev) => (prev ? { ...prev, photoUrl: prev.demoUrl, captured: true } : prev)); + }, `「${dvPhotoCamera.label}」`); + }, [dvPhotoCamera, resetDvCameraAssist, runAlbumPick]); + + const completeDvPhotoCamera = useCallback(() => { + if (!dvPhotoCamera?.captured || !dvPhotoCamera.photoUrl) { + message.warning('请先拍照或从相册选择照片'); + return; + } + dvPhotoCamera.onComplete?.(); + setDvPhotoCamera(null); + resetDvCameraAssist(); + }, [dvPhotoCamera, resetDvCameraAssist]); + + const openSpareTireCapture = useCallback((existing) => { + resetDvCameraAssist(); + if (existing?.spareTirePhotoUploaded) { + setSpareTireCaptureDraft({ + photoUrl: existing.spareTirePhotoUrl || DV_SPARE_TIRE_DEMO_PHOTO, + treadDepth: existing.spareTireTreadDepth || '', + }); + setSpareTireCaptureOpen(true); + return; + } + setSpareTireCaptureDraft({ photoUrl: '', treadDepth: '' }); + setSpareTireCaptureOpen(true); + message.info('拍摄备胎(支持拍照或相册)'); + }, [resetDvCameraAssist]); + + const pickSpareTireFromAlbum = useCallback(() => { + runAlbumPick(() => { + const tread = DV_SPARE_TREAD_OCR_DEMO[spareTreadOcrIdxRef.current % DV_SPARE_TREAD_OCR_DEMO.length]; + spareTreadOcrIdxRef.current += 1; + setSpareTireCaptureDraft({ photoUrl: DV_SPARE_TIRE_DEMO_PHOTO, treadDepth: tread }); + }, '备胎照片'); + }, [runAlbumPick]); + + const completeSpareTireCapture = useCallback(() => { + const tread = String(spareTireCaptureDraft.treadDepth || '').trim(); + if (!spareTireCaptureDraft.photoUrl) { + message.warning('请先拍照或从相册选择照片'); + return; + } + setFormDraft((d) => ({ + ...d, + spareTirePhotoUploaded: true, + spareTirePhotoUrl: spareTireCaptureDraft.photoUrl, + spareTireTreadDepth: tread, + })); + setSpareTireCaptureOpen(false); + }, [spareTireCaptureDraft]); + + const renderDeliveryStepBar = () => { + const stepOrder = DV_FORM_STEPS.map((s) => s.key); + const currentIdx = Math.max(0, stepOrder.indexOf(deliveryFormStep)); + const goStep = (key) => { + const targetIdx = stepOrder.indexOf(key); + if (targetIdx < 0) return; + + const enterPhotosStep = () => { + initPhotoCaptureOnEnter(formDraft?.deliveryPhotos || {}, formDraft); + setDeliveryFormStep('photos'); + }; + + if (readOnly) { + if (key === 'photos') initPhotoCaptureOnEnter(formDraft?.deliveryPhotos || {}, formDraft); + setDeliveryFormStep(key); + return; + } + + if (targetIdx <= currentIdx) { + if (key === 'photos') initPhotoCaptureOnEnter(formDraft?.deliveryPhotos || {}, formDraft); + setDeliveryFormStep(key); + return; + } + + if (targetIdx >= 1) { + const vehicleResult = dvValidateVehicleStep(formDraft, activeRow); + if (!vehicleResult.ok) { + message.warning(vehicleResult.message); + return; + } + } + if (targetIdx >= 2) { + const inspectionResult = dvValidateInspectionStep(formDraft); + if (!inspectionResult.ok) { + message.warning(inspectionResult.message); + return; + } + } + + if (key === 'photos') { + enterPhotosStep(); + return; + } + setDeliveryFormStep(key); + }; + return ( +
+
+ {DV_FORM_STEPS.map((s, i) => ( + + + {i < DV_FORM_STEPS.length - 1 ? ( + + ) : null} + + ))} +
+
+ ); + }; + + const renderDeliveryInspection = (f) => { + const list = f.inspectionList || []; + const categories = [...new Set(list.map((row) => row.category))]; + return ( +
+ {renderSectionHead(1, '交车检查单')} + {categories.map((cat) => ( +
+
{cat}
+ {list.filter((row) => row.category === cat).map((row) => ( +
+ {row.item} +
+ {dvInspectionIsTireCategory(row.category) ? ( + readOnly ? ( + {row.treadDepth ? `${row.treadDepth} mm` : '—'} + ) : ( + { + const next = dvMetricInputChange(e.target.value); + if (next !== null) updateInspectionRow(row.key, { treadDepth: next }); + }} + placeholder="mm" + aria-label={`${row.item}胎纹深度`} + /> + ) + ) : readOnly ? ( + {row.checked ? '正常' : '异常'} + ) : ( +
+
+ ))} +
+ ))} +
+ ); + }; + + const renderPhotoViewerOverlay = () => { + if (!photoViewer) return null; + const { items, index, categoryLabel } = photoViewer; + const current = items[index]; + if (!current) return null; + const goPrev = () => { + setPhotoViewer((v) => (v && v.index > 0 ? { ...v, index: v.index - 1 } : v)); + }; + const goNext = () => { + setPhotoViewer((v) => (v && v.index < v.items.length - 1 ? { ...v, index: v.index + 1 } : v)); + }; + const onTouchStart = (e) => { + const t = e.changedTouches?.[0] || e.touches?.[0]; + if (!t) return; + photoViewerTouchRef.current = { x: t.clientX, y: t.clientY }; + }; + const onTouchEnd = (e) => { + const t = e.changedTouches?.[0]; + if (!t) return; + const dx = t.clientX - photoViewerTouchRef.current.x; + const dy = t.clientY - photoViewerTouchRef.current.y; + if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy)) return; + if (dx > 0) goPrev(); + else goNext(); + }; + return ( +
+
+ +
+
{categoryLabel}
+
{current.label}
+
+ {index + 1}/{items.length} +
+
+ +
+ {current.label} { e.currentTarget.src = dvGetPhotoDemoUrl(current.key); }} + /> +
+ +
+
+ {current.treadDepth ?
胎纹 {current.treadDepth} mm
: null} +
左右滑动或点击箭头切换
+
+
+ ); + }; + + const renderPhotoCameraOverlay = () => { + if (photoCapturePhase !== 'camera' || deliveryFormStep !== 'photos') return null; + const item = getCurrentCaptureItem(); + if (!item) return null; + const seq = dvGetCaptureSequence(formDraft); + return ( +
+
+ {item.label} + {photoCaptureIndex + 1}/{seq.length} +
+
+ { e.currentTarget.src = dvGetPhotoDemoUrl(item.key); }} + showFocusTip={!photoCameraDraft.captured} + /> +
+ {item.tread && photoCameraDraft.captured ? ( +
+ {item.label}胎纹 +
+ { + const next = dvMetricInputChange(e.target.value); + if (next !== null) { + setPhotoCameraDraft((d) => ({ + ...d, + treadDepth: next, + treadOcrOk: !!String(next || '').trim(), + })); + } + }} + placeholder="—" + aria-label={`${item.label}胎纹深度`} + /> + mm +
+
+ ) : null} + {item.tread && photoCameraDraft.captured && !photoCameraDraft.treadOcrOk ? ( +
+ 胎纹未识别成功,请重新拍摄后再继续 +
+ ) : null} + {renderPhotoCaptureActionBar({ + captured: !!photoCameraDraft.captured, + onShutter: () => { resetDvCameraAssist(); simulateCameraShutter(); }, + onAlbum: pickContinuousPhotoFromAlbum, + onComplete: confirmPhotoCameraCapture, + })} +
+ ); + }; + + const renderSpareTireCaptureOverlay = () => { + if (!spareTireCaptureOpen) return null; + return ( +
+
+ +
+
+ 备胎胎纹 +
+ setSpareTireCaptureDraft((d) => ({ ...d, treadDepth: e.target.value }))} + placeholder="—" + /> + mm +
+
+ {renderPhotoCaptureActionBar({ + captured: !!spareTireCaptureDraft.photoUrl, + onShutter: () => { resetDvCameraAssist(); simulateSpareTireCapture(); }, + onAlbum: pickSpareTireFromAlbum, + onComplete: completeSpareTireCapture, + })} +
+ ); + }; + + const renderDvPhotoCameraOverlay = () => { + if (!dvPhotoCamera) return null; + return ( +
+
{dvPhotoCamera.label}
+
+ +
+ {renderPhotoCaptureActionBar({ + captured: !!dvPhotoCamera.captured, + onShutter: shutterDvPhotoCamera, + onAlbum: pickDvPhotoFromAlbum, + onComplete: completeDvPhotoCamera, + })} +
+ ); + }; + + const renderDeliveryFormPage = () => { if (!activeRow || !formDraft) return null; const r = activeRow; const f = formDraft; - if (formStep === 0) { - const st = dvStatusTag(r.deliveryStatus); - return ( - <> -
-
{r.customerName || '—'}
-
{r.projectName || '—'}
-
-
交车区域{r.deliveryRegion || '—'}
-
计划交车{dvFormatExpectedDate(r.expectedDate)}
-
-
- {r.bizType || '租赁'} - {r.replaceOldPlate ? 替换旧车 {r.replaceOldPlate} : null} -
-
-
-
- 交车信息 - {st.text} -
-
车牌下拉仅展示「已备车」且停车场在运维权限范围内的车辆。
-
- {renderFormField('业务类型', r.bizType, null)} - {renderFormField('任务来源', r.taskSource, null)} - {renderFormField('业务部门', r.businessDept, null)} - {renderFormField('业务负责人', r.businessOwner, null)} - {renderFormField('创建时间', r.createTime, null)} - {renderFormField('车牌号', dvDisplayPlate(f.plateNo), readOnly ? null : ( - - ))} - {renderFormField('品牌型号', `${f.brand || r.brand} · ${f.model || r.model}`, null)} - {renderFormField('VIN', f.vin || r.vin, readOnly ? null : ( - setFormDraft((d) => ({ ...d, vin: e.target.value }))} placeholder="车架号" /> - ))} -
-
- - ); - } - if (formStep === 1) { - const yesNoOpts = ['', '有', '无']; - const trainOpts = ['', '已完成', '未完成', '无需培训']; - return ( -
-
车辆信息
-
请核对广告、尾板、备胎及驾驶培训情况;照片上传说明见下一步。
-
- {renderFormField('车身广告', f.hasAd || '—', readOnly ? null : ( - - ))} - {renderFormField('尾板', f.hasTailgate || '—', readOnly ? null : ( - - ))} - {renderFormField('备胎', f.spareTire || '—', readOnly ? null : ( - - ))} - {renderFormField('驾驶培训', f.driverTraining || '—', readOnly ? null : ( - - ))} -
-
- ); - } - if (formStep === 2) { - return ( -
-
交车数据
-
- {renderFormField('交车时间', f.deliveryTime ? f.deliveryTime.replace('T', ' ') : (r.deliveryTime || '—'), readOnly ? null : ( - setFormDraft((d) => ({ ...d, deliveryTime: e.target.value }))} /> - ))} - {renderFormField('交车里程(km)', dvFormatMileage(f.deliveryMileage), readOnly ? null : ( - setFormDraft((d) => ({ ...d, deliveryMileage: e.target.value }))} placeholder="0" /> - ))} - {renderFormField('氢量', dvFormatH2(f.deliveryH2, f.deliveryH2Unit), readOnly ? null : ( - - setFormDraft((d) => ({ ...d, deliveryH2: e.target.value }))} placeholder="0" style={{ maxWidth: 80 }} /> - - - ))} - {renderFormField('电量(%)', f.deliveryElec ? `${f.deliveryElec}%` : '—', readOnly ? null : ( - setFormDraft((d) => ({ ...d, deliveryElec: e.target.value }))} placeholder="0" /> - ))} - {readOnly && r.deliveryPerson ? renderFormField('交车人', r.deliveryPerson, null) : null} -
-
- ); - } - if (formStep === 3) { - return ( -
-
交车照片
-
请按模块上传交车照片;支持拍照或相册,单张不超过 10MB(原型模拟)。
- {DV_PHOTO_SECTIONS.map((sec) => ( -
-
{sec.label}
-
- {[0, 1, 2].map((i) => ( -
!readOnly && message.info(`上传${sec.label}(原型)`)} - onKeyDown={(e) => e.key === 'Enter' && !readOnly && message.info(`上传${sec.label}(原型)`)} - > - {readOnly && sec.key === 'body' && i === 0 ? '已上传' : '+ 添加'} -
- ))} -
-
- ))} -
- ); - } const st = dvStatusTag(r.deliveryStatus); - return ( + const pickedVehicle = f.plateNo ? DV_DELIVERY_PICK_VEHICLES.find((v) => v.plateNo === f.plateNo) : null; + const rearEquip = f.rearEquip || dvGetRearEquipRecord(f.plateNo, r); + const showDriverPanel = f.driverTrainingDone || f.driverTraining === '已完成'; + const showDriverPending = !!(f.driverTrainingPending && !showDriverPanel); + + const renderDeliveryHero = () => ( +
+
{r.customerName || '—'}
+
{r.projectName || '—'}
+
+
交车区域{r.deliveryRegion || '—'}
+
计划交车{dvFormatExpectedDate(r.expectedDate)}
+ {r.deliveryAddress ?
交车地点{r.deliveryAddress}
: null} + {readOnly ? ( + <> +
交车人{r.deliveryPerson || '—'}
+
交车时间{dvDisplayActualTime(r.deliveryTime)}
+ + ) : null} +
+
+ {r.bizType || '租赁'} + {st.text} + {r.replaceOldPlate ? 替换旧车 {r.replaceOldPlate} : null} +
+
+ ); + + const renderVehicleStep = () => ( <> -
-
交车确认
-
{dvDisplayPlate(f.plateNo)}
-
{r.customerName || '—'}
-
-
项目{r.projectName || '—'}
-
交车区域{r.deliveryRegion || '—'}
-
状态{st.text}
-
-
-
-
交车摘要
-
-
里程/氢/电{dvFormatMileage(f.deliveryMileage)} · {dvFormatH2(f.deliveryH2, f.deliveryH2Unit)} · {f.deliveryElec ? `${f.deliveryElec}%` : '—'}
-
交车时间{dvDisplayActualTime(f.deliveryTime || r.deliveryTime)}
- {r.deliveryPerson ?
交车人{r.deliveryPerson}
: null} -
-
- {dvIsHistoryStatus(r.deliveryStatus) && ( -
-
E签宝签章文件
-
-
message.success('下载签章 PDF(原型)')} onKeyDown={(e) => e.key === 'Enter' && message.success('下载签章 PDF(原型)')}> - 交车确认单_已签章.pdf
点击预览或下载 + {renderDeliveryHero()} + +
+ {renderSectionHead(1, '选择车辆', !readOnly ? ( +
+ + +
+ ) : null)} + {(f.plateNo || readOnly) ? ( +
+
{dvDisplayPlate(f.plateNo)}
+
+ {f.brand || r.brand} · {f.model || r.model} + {pickedVehicle ? <>
{pickedVehicle.parkingLot} : null}
- {r.vehicleReturned != null && ( -
是否归还{r.vehicleReturned ? '已归还' : '未归还'}
- )} +
+ ) : null} +
+ + {f.plateNo ? ( + <> +
+ {renderSectionHead(2, '车辆配置')} +
+
+ 车身广告及放大字{renderRequiredTag()} + {renderEquipSwitch(rearEquip.hasAd, toggleRearEquipAd, '车身广告及放大字')} +
+ {rearEquip.hasAd ? ( +
+
+
车身广告照片
+
!readOnly && openDvPhotoCamera({ + key: 'equip-ad', + label: '车身广告照片', + demoUrl: DV_EQUIP_PHOTO_DEMO.ad, + photoUrl: f.adPhotoUploaded ? DV_EQUIP_PHOTO_DEMO.ad : '', + onComplete: () => setFormDraft((d) => ({ ...d, adPhotoUploaded: true })), + })} + onKeyDown={(e) => e.key === 'Enter' && !readOnly && openDvPhotoCamera({ + key: 'equip-ad', + label: '车身广告照片', + demoUrl: DV_EQUIP_PHOTO_DEMO.ad, + photoUrl: f.adPhotoUploaded ? DV_EQUIP_PHOTO_DEMO.ad : '', + onComplete: () => setFormDraft((d) => ({ ...d, adPhotoUploaded: true })), + })} + > + {f.adPhotoUploaded || (readOnly && rearEquip.adPhotoDone) ? '已拍摄' : '拍照/相册'} +
+
+
+
放大字照片
+
!readOnly && openDvPhotoCamera({ + key: 'equip-bigword', + label: '放大字照片', + demoUrl: DV_EQUIP_PHOTO_DEMO.bigWord, + photoUrl: f.bigWordPhotoUploaded ? DV_EQUIP_PHOTO_DEMO.bigWord : '', + onComplete: () => setFormDraft((d) => ({ ...d, bigWordPhotoUploaded: true })), + })} + onKeyDown={(e) => e.key === 'Enter' && !readOnly && openDvPhotoCamera({ + key: 'equip-bigword', + label: '放大字照片', + demoUrl: DV_EQUIP_PHOTO_DEMO.bigWord, + photoUrl: f.bigWordPhotoUploaded ? DV_EQUIP_PHOTO_DEMO.bigWord : '', + onComplete: () => setFormDraft((d) => ({ ...d, bigWordPhotoUploaded: true })), + })} + > + {f.bigWordPhotoUploaded || (readOnly && rearEquip.bigWordPhotoDone) ? '已拍摄' : '拍照/相册'} +
+
+
+ ) : null} +
+ 尾板{renderRequiredTag()} + {renderEquipSwitch(rearEquip.hasTailgate, toggleRearEquipTailgate, '尾板')} +
+
+ 备胎 + {renderEquipSwitch(f.spareTire === '有', (checked) => handleSpareTireChange(checked ? '有' : '无'), '备胎')}
- )} +
+ +
+ {renderSectionHead(3, '驾驶培训', showDriverPanel ? ( + 已完成驾驶培训 + ) : showDriverPending ? ( + 待司机确认 + ) : null, true)} + {!showDriverPanel && !showDriverPending && !readOnly ? ( +
+ + +
+ ) : null} + {showDriverPending && !readOnly ? ( +
+
培训码已生成,等待司机微信扫码阅读安全培训文件并签字确认
+ +
+ ) : null} + {showDriverPanel ? ( +
+
+
司机姓名
{f.driverName || '—'}
+
司机手机号
{f.driverPhone || '—'}
+
身份证号
{f.driverIdNo || '—'}
+
+
+
+
身份证(正面)
+
{renderDriverDocThumb(f.driverIdFront, f.driverIdFrontUrl)}
+
+
+
身份证(反面)
+
{renderDriverDocThumb(f.driverIdBack, f.driverIdBackUrl)}
+
+
+
驾驶证(正面)
+
{renderDriverDocThumb(f.driverLicenseFront, f.driverLicenseFrontUrl)}
+
+
+
驾驶证(反面)
+
{renderDriverDocThumb(f.driverLicenseBack, f.driverLicenseBackUrl)}
+
+
+
司机正面照片
+
{renderDriverDocThumb(f.driverFrontPhoto, f.driverFrontPhotoUrl)}
+
+
+
从业资格证
+
{renderDriverDocThumb(f.driverQualification, f.driverQualificationUrl)}
+
+
+
+ ) : null} +
+ +
+ {renderSectionHead(4, '交车数据', null, true)} + {readOnly ? ( +
+
里程
{dvFormatMileage(f.deliveryMileage)}
+
氢量
{dvFormatH2(f.deliveryH2, f.deliveryH2Unit)}
+
电量
{dvFormatMetric2(f.deliveryElec, '%')}
+
送车服务费
{dvFormatServiceFee(f.serviceFee)}
+ {f.deliveryRemark ?
备注
{f.deliveryRemark}
: null} + {r.deliveryPerson ?
交车人
{r.deliveryPerson}
: null} +
+ ) : ( +
+
+ + handleMetricFieldChange('deliveryMileage', e.target.value)} placeholder="0.00" /> +
+
+ + handleMetricFieldChange('deliveryElec', e.target.value)} placeholder="0.00" /> +
+
+ +
+ handleMetricFieldChange('deliveryH2', e.target.value)} placeholder="0.00" /> + {f.deliveryH2Unit || dvGetModelGaugeUnit(f.brand || r.brand, f.model || r.model)} +
+
+
+ +
+ handleMetricFieldChange('serviceFee', e.target.value)} placeholder="0.00" aria-label="送车服务费" /> + +
+
+
+ +