From df824ae71ce6e1fc3e95d0ddd8beaf79ed9fab72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=86=95?= Date: Wed, 10 Jun 2026 16:53:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=96=B0=E5=A2=9E=E8=BD=A6?= =?UTF-8?q?=E8=BE=86=E7=8A=B6=E6=80=81=E8=AF=B4=E6=98=8E=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BC=8F=E6=96=87=E6=A1=A3=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提供五维状态、关联规则、场景问答与 H5 手机布局,便于业务与运维查阅车辆状态口径。 Co-authored-by: Cursor --- ONEOS-web/车辆状态说明.jsx | 1122 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1122 insertions(+) create mode 100644 ONEOS-web/车辆状态说明.jsx diff --git a/ONEOS-web/车辆状态说明.jsx b/ONEOS-web/车辆状态说明.jsx new file mode 100644 index 0000000..d81f615 --- /dev/null +++ b/ONEOS-web/车辆状态说明.jsx @@ -0,0 +1,1122 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web · 车辆状态说明(响应式文档页) + +function vsEnsureViewport() { + try { + var meta = document.querySelector('meta[name="viewport"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.name = 'viewport'; + document.head.appendChild(meta); + } + meta.content = 'width=device-width, initial-scale=1, maximum-scale=5, viewport-fit=cover'; + } catch (e) { /* ignore */ } +} + +function vsDetectMobileLayout(rootEl) { + vsEnsureViewport(); + var ua = navigator.userAgent || ''; + var isPhoneUa = /iPhone|iPod|Android.+Mobile|Opera Mini|IEMobile|Windows Phone|webOS|BlackBerry/i.test(ua); + var isWx = /MicroMessenger/i.test(ua); + var vw = window.visualViewport && window.visualViewport.width + ? window.visualViewport.width + : window.innerWidth; + var docW = document.documentElement ? document.documentElement.clientWidth : 0; + var rootW = rootEl && rootEl.clientWidth ? rootEl.clientWidth : 0; + if (vw > 0 && vw <= 768) return true; + if (docW > 0 && docW <= 768) return true; + if (rootW > 0 && rootW <= 768) return true; + try { + if (window.matchMedia) { + if (window.matchMedia('(max-width: 768px)').matches) return true; + if (window.matchMedia('(max-device-width: 820px)').matches) return true; + if (window.matchMedia('(hover: none) and (pointer: coarse)').matches) return true; + } + } catch (e) { /* ignore */ } + var sw = 0; + if (window.screen) { + sw = Math.min(window.screen.width || 0, window.screen.height || 0); + if (sw > 0 && sw <= 820) return true; + } + var touch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); + if (touch && sw > 0 && sw <= 820) return true; + if (isPhoneUa || (isWx && touch)) return true; + return false; +} + +function vsApplyMobileEnv(isMobile) { + try { + if (!document.documentElement) return; + if (isMobile) document.documentElement.classList.add('vs-mobile-env'); + else document.documentElement.classList.remove('vs-mobile-env'); + } catch (e) { /* ignore */ } +} + +if (typeof document !== 'undefined') { + try { + vsEnsureViewport(); + vsApplyMobileEnv(vsDetectMobileLayout(document.getElementById('root'))); + } catch (e) { /* ignore */ } +} + +const Component = function (props) { + const { useState, useEffect, useLayoutEffect, useCallback, useMemo } = React; + const antd = window.antd || {}; + const Tag = antd.Tag; + const Input = antd.Input; + + const C_PRIMARY = '#0f766e'; + const C_PRIMARY_L = '#14b8a6'; + const C_PRIMARY_D = '#0d5c56'; + const C_PAGE = '#f1f5f9'; + const C_CARD = '#ffffff'; + const C_TEXT = '#0f172a'; + const C_MUTED = '#64748b'; + const C_LINE = 'rgba(15, 23, 42, 0.08)'; + const C_BLUE = '#2563eb'; + const C_AMBER = '#d97706'; + const C_VIOLET = '#7c3aed'; + const C_ROSE = '#e11d48'; + const C_GREEN = '#16a34a'; + const FONT = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Segoe UI", sans-serif'; + + const PAGE_STYLE = ` +.vs-root { font-family: ${FONT}; background: ${C_PAGE}; color: ${C_TEXT}; min-height: 100vh; line-height: 1.6; -webkit-font-smoothing: antialiased; } +.vs-skip { position: absolute; left: -9999px; top: 0; z-index: 9999; padding: 10px 16px; background: ${C_PRIMARY}; color: #fff; border-radius: 8px; } +.vs-skip:focus { left: 12px; top: 12px; outline: 3px solid #5eead4; outline-offset: 2px; } +.vs-hero { position: relative; overflow: hidden; background: linear-gradient(145deg, #0c1222 0%, #132337 42%, #0f4c47 100%); color: #fff; padding: 48px 24px 56px; } +.vs-hero::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse 80% 60% at 90% 10%, rgba(20,184,166,0.22), transparent 55%), radial-gradient(ellipse 50% 40% at 10% 90%, rgba(37,99,235,0.12), transparent 50%); pointer-events: none; } +.vs-hero::after { content: ''; position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); background-size: 32px 32px; mask-image: linear-gradient(180deg, rgba(0,0,0,0.5), transparent); pointer-events: none; } +.vs-hero-inner { position: relative; max-width: 1120px; margin: 0 auto; } +.vs-hero-tag { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 5px 12px; border-radius: 999px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.14); color: #a7f3d0; margin-bottom: 14px; letter-spacing: 0.04em; } +.vs-hero h1 { margin: 0 0 14px; font-size: clamp(26px, 5vw, 38px); font-weight: 800; letter-spacing: -0.03em; line-height: 1.2; } +.vs-hero-desc { margin: 0; font-size: clamp(14px, 2.5vw, 16px); color: rgba(255,255,255,0.76); max-width: 680px; line-height: 1.7; } +.vs-hero-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 32px; } +@media (max-width: 640px) { .vs-hero-kpis { grid-template-columns: repeat(2, 1fr); } } +.vs-kpi { background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12); border-radius: 14px; padding: 16px 18px; backdrop-filter: blur(8px); transition: transform 0.2s ease, border-color 0.2s ease; } +.vs-kpi:hover { transform: translateY(-2px); border-color: rgba(94,234,212,0.35); } +.vs-kpi-val { font-size: 26px; font-weight: 800; color: #5eead4; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; } +.vs-kpi-lbl { font-size: 12px; color: rgba(255,255,255,0.62); margin-top: 4px; } +.vs-shell { max-width: 1120px; margin: 0 auto; padding: 0 16px 72px; } +.vs-layout { display: grid; grid-template-columns: 220px 1fr; gap: 28px; margin-top: -28px; position: relative; z-index: 2; align-items: start; } +@media (max-width: 900px) { .vs-layout { grid-template-columns: 1fr; margin-top: 20px; } } +.vs-sidebar { position: sticky; top: 16px; } +@media (max-width: 900px) { .vs-sidebar { position: sticky; top: 0; z-index: 20; margin: 0 -16px; padding: 0 16px 8px; background: linear-gradient(180deg, ${C_PAGE} 85%, transparent); } } +.vs-nav-card { background: ${C_CARD}; border: 1px solid ${C_LINE}; border-radius: 16px; padding: 14px 10px; box-shadow: 0 4px 24px rgba(15,23,42,0.06); } +.vs-nav-label { font-size: 11px; font-weight: 700; color: ${C_MUTED}; letter-spacing: 0.08em; padding: 0 12px 8px; text-transform: uppercase; } +@media (max-width: 900px) { .vs-nav-label { display: none; } } +.vs-nav-list { display: flex; flex-direction: column; gap: 2px; } +@media (max-width: 900px) { .vs-nav-list { flex-direction: row; flex-wrap: nowrap; overflow-x: auto; gap: 8px; padding-bottom: 4px; -webkit-overflow-scrolling: touch; scrollbar-width: none; } + .vs-nav-list::-webkit-scrollbar { display: none; } } +.vs-nav-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; text-align: left; border: none; background: transparent; padding: 10px 12px; font-size: 13px; color: ${C_MUTED}; cursor: pointer; border-radius: 10px; transition: background 0.2s ease, color 0.2s ease; touch-action: manipulation; white-space: nowrap; } +@media (max-width: 900px) { .vs-nav-item { width: auto; flex-shrink: 0; padding: 10px 16px; background: ${C_CARD}; border: 1px solid ${C_LINE}; } } +.vs-nav-item:hover { color: ${C_PRIMARY}; background: rgba(15,118,110,0.06); } +.vs-nav-item:focus-visible { outline: 2px solid ${C_PRIMARY_L}; outline-offset: 2px; } +.vs-nav-item.active { color: ${C_PRIMARY_D}; font-weight: 600; background: linear-gradient(90deg, rgba(15,118,110,0.12), rgba(15,118,110,0.04)); } +@media (max-width: 900px) { .vs-nav-item.active { background: ${C_PRIMARY}; color: #fff; border-color: ${C_PRIMARY}; } } +.vs-nav-dot { width: 8px; height: 8px; border-radius: 50%; background: ${C_LINE}; flex-shrink: 0; transition: background 0.2s, transform 0.2s; } +.vs-nav-item.active .vs-nav-dot { background: ${C_PRIMARY_L}; transform: scale(1.2); } +@media (max-width: 900px) { .vs-nav-dot { display: none; } } +.vs-main { min-width: 0; display: flex; flex-direction: column; gap: 20px; } +.vs-panel { background: ${C_CARD}; border: 1px solid ${C_LINE}; border-radius: 16px; padding: 24px 26px; box-shadow: 0 1px 3px rgba(15,23,42,0.04); scroll-margin-top: 88px; transition: box-shadow 0.25s ease; } +.vs-panel:hover { box-shadow: 0 8px 30px rgba(15,23,42,0.06); } +.vs-panel-head { margin-bottom: 20px; } +.vs-panel-tag { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.1em; color: ${C_PRIMARY}; background: rgba(15,118,110,0.08); padding: 4px 10px; border-radius: 6px; margin-bottom: 10px; } +.vs-panel-title { margin: 0; font-size: clamp(18px, 3vw, 22px); font-weight: 800; letter-spacing: -0.02em; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } +.vs-panel-num { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 10px; background: linear-gradient(135deg, ${C_PRIMARY}, ${C_PRIMARY_L}); color: #fff; font-size: 14px; font-weight: 800; flex-shrink: 0; box-shadow: 0 4px 12px rgba(15,118,110,0.25); } +.vs-panel-desc { margin: 10px 0 0; font-size: 14px; color: ${C_MUTED}; max-width: 640px; } +.vs-bento { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; } +@media (max-width: 768px) { .vs-bento { grid-template-columns: 1fr 1fr; } } +.vs-bento-item { grid-column: span 2; background: ${C_PAGE}; border: 1px solid ${C_LINE}; border-radius: 14px; padding: 18px; position: relative; overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: default; } +.vs-bento-item:hover { transform: translateY(-3px); box-shadow: 0 12px 28px rgba(15,23,42,0.08); } +.vs-bento-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--accent); } +.vs-bento-item h4 { margin: 0 0 8px; font-size: 15px; font-weight: 700; } +.vs-bento-item p { margin: 0; font-size: 13px; color: ${C_MUTED}; line-height: 1.55; } +.vs-bento-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--accent-bg); display: flex; align-items: center; justify-content: center; margin-bottom: 12px; color: var(--accent); } +.vs-example { background: linear-gradient(135deg, #f0fdfa 0%, #ecfeff 100%); border: 1px solid #99f6e4; border-radius: 14px; padding: 18px 20px; margin-top: 20px; } +.vs-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } +.vs-callout { display: flex; gap: 12px; align-items: flex-start; background: #fffbeb; border: 1px solid #fde68a; border-radius: 12px; padding: 14px 16px; margin-top: 20px; font-size: 13px; color: #92400e; line-height: 1.6; } +.vs-callout-icon { flex-shrink: 0; width: 22px; height: 22px; border-radius: 6px; background: #fef3c7; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; color: #b45309; } +.vs-rule-grid { display: grid; gap: 10px; } +@media (min-width: 640px) { .vs-rule-grid { grid-template-columns: 1fr 1fr; } } +.vs-rule-card { background: ${C_PAGE}; border: 1px solid ${C_LINE}; border-radius: 12px; padding: 14px 16px; font-size: 13px; color: #334155; line-height: 1.65; display: flex; gap: 10px; } +.vs-rule-idx { flex-shrink: 0; width: 24px; height: 24px; border-radius: 8px; background: ${C_PRIMARY}; color: #fff; font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; } +.vs-timeline { display: flex; flex-direction: column; gap: 0; margin: 16px 0; position: relative; padding-left: 20px; } +.vs-timeline::before { content: ''; position: absolute; left: 7px; top: 8px; bottom: 8px; width: 2px; background: linear-gradient(180deg, ${C_PRIMARY_L}, ${C_LINE}); border-radius: 2px; } +.vs-timeline-step { position: relative; padding: 8px 0 8px 20px; font-size: 13px; font-weight: 500; color: #334155; } +.vs-timeline-step::before { content: ''; position: absolute; left: -17px; top: 14px; width: 10px; height: 10px; border-radius: 50%; background: ${C_CARD}; border: 2px solid ${C_PRIMARY_L}; box-shadow: 0 0 0 3px rgba(20,184,166,0.15); } +.vs-timeline-step:last-child::before { background: ${C_PRIMARY_L}; } +.vs-subtitle { font-size: 15px; font-weight: 700; margin: 24px 0 12px; color: ${C_TEXT}; } +.vs-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 12px 0; border-radius: 12px; border: 1px solid ${C_LINE}; box-shadow: inset 0 1px 0 rgba(255,255,255,0.8); } +.vs-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 480px; } +.vs-table th { background: #f8fafc; text-align: left; padding: 12px 16px; font-weight: 600; color: ${C_TEXT}; border-bottom: 1px solid ${C_LINE}; font-size: 12px; letter-spacing: 0.02em; } +.vs-table td { padding: 12px 16px; border-bottom: 1px solid ${C_LINE}; color: #334155; vertical-align: top; line-height: 1.55; } +.vs-table tbody tr { transition: background 0.15s ease; } +.vs-table tbody tr:hover { background: #f8fafc; } +.vs-table tbody tr:nth-child(even) { background: rgba(248,250,252,0.6); } +.vs-table tbody tr:nth-child(even):hover { background: #f1f5f9; } +.vs-table tr:last-child td { border-bottom: none; } +.vs-status-grid { display: grid; gap: 10px; } +@media (min-width: 640px) { .vs-status-grid { grid-template-columns: 1fr 1fr; } } +.vs-status-card { background: ${C_PAGE}; border: 1px solid ${C_LINE}; border-radius: 12px; padding: 16px 18px; transition: border-color 0.2s ease, box-shadow 0.2s ease; } +.vs-status-card:hover { border-color: rgba(15,118,110,0.25); box-shadow: 0 4px 16px rgba(15,23,42,0.05); } +.vs-status-card h3 { margin: 0 0 8px; font-size: 14px; font-weight: 700; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.vs-status-card p { margin: 0; font-size: 13px; color: #475569; line-height: 1.6; } +.vs-toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 16px; } +.vs-chips { display: flex; flex-wrap: wrap; gap: 8px; } +.vs-chip { min-height: 36px; padding: 0 14px; border-radius: 999px; border: 1px solid ${C_LINE}; background: ${C_CARD}; font-size: 13px; color: ${C_MUTED}; cursor: pointer; transition: all 0.2s ease; touch-action: manipulation; } +.vs-chip:hover { border-color: ${C_PRIMARY_L}; color: ${C_PRIMARY}; } +.vs-chip:focus-visible { outline: 2px solid ${C_PRIMARY_L}; outline-offset: 2px; } +.vs-chip.active { background: ${C_PRIMARY}; border-color: ${C_PRIMARY}; color: #fff; font-weight: 600; } +.vs-search { flex: 1; min-width: 180px; max-width: 280px; } +.vs-faq-list { display: flex; flex-direction: column; gap: 8px; } +.vs-faq { border: 1px solid ${C_LINE}; border-radius: 12px; overflow: hidden; background: ${C_PAGE}; transition: border-color 0.2s ease, box-shadow 0.2s ease; } +.vs-faq.is-open { border-color: rgba(15,118,110,0.3); box-shadow: 0 4px 16px rgba(15,118,110,0.08); background: ${C_CARD}; } +.vs-faq-head { width: 100%; min-height: 52px; text-align: left; border: none; background: transparent; padding: 14px 18px; font-size: 14px; font-weight: 600; cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 14px; color: ${C_TEXT}; touch-action: manipulation; } +.vs-faq-head:hover { background: rgba(15,118,110,0.04); } +.vs-faq-head:focus-visible { outline: 2px solid ${C_PRIMARY_L}; outline-offset: -2px; } +.vs-faq-q { display: flex; align-items: flex-start; gap: 12px; flex: 1; min-width: 0; } +.vs-faq-num { flex-shrink: 0; width: 26px; height: 26px; border-radius: 8px; background: rgba(15,118,110,0.1); color: ${C_PRIMARY}; font-size: 12px; font-weight: 800; display: flex; align-items: center; justify-content: center; } +.vs-faq.is-open .vs-faq-num { background: ${C_PRIMARY}; color: #fff; } +.vs-faq-chevron { flex-shrink: 0; width: 28px; height: 28px; border-radius: 8px; background: ${C_LINE}; display: flex; align-items: center; justify-content: center; transition: transform 0.25s ease, background 0.2s ease; font-size: 16px; color: ${C_MUTED}; } +.vs-faq.is-open .vs-faq-chevron { transform: rotate(180deg); background: rgba(15,118,110,0.12); color: ${C_PRIMARY}; } +.vs-faq-body { padding: 0 18px 16px 56px; font-size: 14px; color: #475569; line-height: 1.7; animation: vs-faq-in 0.25s ease; } +@keyframes vs-faq-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } +.vs-faq-actions { display: flex; gap: 8px; margin-bottom: 14px; } +.vs-text-btn { min-height: 36px; padding: 0 12px; border: 1px solid ${C_LINE}; border-radius: 8px; background: ${C_CARD}; font-size: 12px; color: ${C_MUTED}; cursor: pointer; touch-action: manipulation; } +.vs-text-btn:hover { color: ${C_PRIMARY}; border-color: ${C_PRIMARY_L}; } +.vs-text-btn:focus-visible { outline: 2px solid ${C_PRIMARY_L}; outline-offset: 2px; } +.vs-pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; line-height: 1.4; } +.vs-footer { text-align: center; padding: 28px 16px; font-size: 12px; color: ${C_MUTED}; border-top: 1px solid ${C_LINE}; max-width: 1120px; margin: 0 auto; } +.vs-top-btn { position: fixed; right: 20px; bottom: 24px; width: 48px; height: 48px; border-radius: 14px; border: none; background: ${C_PRIMARY}; color: #fff; font-size: 18px; cursor: pointer; box-shadow: 0 8px 24px rgba(15,118,110,0.35); z-index: 30; opacity: 0; pointer-events: none; transform: translateY(12px); transition: opacity 0.25s ease, transform 0.25s ease; touch-action: manipulation; } +.vs-top-btn.visible { opacity: 1; pointer-events: auto; transform: translateY(0); } +.vs-top-btn:hover { background: ${C_PRIMARY_D}; } +.vs-top-btn:focus-visible { outline: 3px solid #5eead4; outline-offset: 2px; } +.vs-empty { text-align: center; padding: 32px 16px; color: ${C_MUTED}; font-size: 14px; } +@media (prefers-reduced-motion: reduce) { + .vs-kpi, .vs-bento-item, .vs-status-card, .vs-faq-body, .vs-top-btn, .vs-nav-item, .vs-panel { transition: none !important; animation: none !important; } + .vs-kpi:hover, .vs-bento-item:hover { transform: none; } +} + +/* —— H5 手机布局(纯 CSS 媒体查询,不依赖 JS class)—— */ +.vs-m-header { display: none; } +.vs-m-drawer-mask { display: none; } +.vs-m-bottom-bar { display: none; } +.vs-m-card-table { display: none; } + +@media (max-width: 768px), (max-device-width: 820px), ((hover: none) and (pointer: coarse) and (max-width: 1024px)) { + .vs-root { max-width: 100vw; overflow-x: hidden; padding-bottom: env(safe-area-inset-bottom, 0px); } + .vs-m-header { + display: flex; align-items: center; justify-content: space-between; gap: 10px; + position: fixed; top: 0; left: 0; right: 0; z-index: 40; + height: calc(48px + env(safe-area-inset-top, 0px)); + padding: env(safe-area-inset-top, 0px) 12px 0 12px; + background: rgba(255,255,255,0.92); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid ${C_LINE}; box-shadow: 0 2px 12px rgba(15,23,42,0.06); + } + .vs-m-header-title { font-size: 15px; font-weight: 700; color: ${C_TEXT}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; } + .vs-m-header-sub { font-size: 11px; color: ${C_MUTED}; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .vs-m-header-btn { + flex-shrink: 0; min-width: 44px; min-height: 44px; padding: 0 12px; border-radius: 10px; + border: 1px solid ${C_LINE}; background: ${C_CARD}; font-size: 13px; font-weight: 600; color: ${C_PRIMARY}; + touch-action: manipulation; cursor: pointer; + } + .vs-m-header-btn:focus-visible { outline: 2px solid ${C_PRIMARY_L}; outline-offset: 2px; } + + .vs-hero { padding: calc(56px + env(safe-area-inset-top, 0px)) 16px 28px; } + .vs-hero h1 { font-size: 24px; } + .vs-hero-desc { font-size: 14px; line-height: 1.65; } + .vs-hero-kpis { grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 20px; } + .vs-kpi { padding: 12px 14px; border-radius: 12px; } + .vs-kpi-val { font-size: 22px; } + .vs-kpi:hover { transform: none; } + + .vs-shell { padding: 0 12px calc(80px + env(safe-area-inset-bottom, 0px)); } + .vs-layout { margin-top: 12px; gap: 12px; } + .vs-sidebar { display: none !important; } + + .vs-panel { + padding: 16px; border-radius: 14px; scroll-margin-top: calc(56px + env(safe-area-inset-top, 0px)); + box-shadow: none; + } + .vs-panel:hover { box-shadow: none; } + .vs-panel-num { width: 28px; height: 28px; font-size: 13px; border-radius: 8px; } + .vs-panel-title { font-size: 17px; gap: 10px; } + .vs-panel-desc { font-size: 13px; } + + .vs-bento { grid-template-columns: 1fr; gap: 10px; } + .vs-bento-item { grid-column: 1 / -1 !important; padding: 14px 16px; } + .vs-bento-item:hover { transform: none; box-shadow: none; } + + .vs-rule-grid { grid-template-columns: 1fr; } + .vs-status-grid { grid-template-columns: 1fr; } + .vs-toolbar { flex-direction: column; align-items: stretch; } + .vs-chips { overflow-x: auto; flex-wrap: nowrap; padding-bottom: 4px; -webkit-overflow-scrolling: touch; scrollbar-width: none; } + .vs-chips::-webkit-scrollbar { display: none; } + .vs-chip { flex-shrink: 0; } + .vs-search { max-width: none; width: 100%; } + + .vs-table-wrap { display: none !important; } + .vs-m-card-table { display: flex; flex-direction: column; gap: 10px; margin: 12px 0; } + + .vs-faq-body { padding: 0 14px 14px 14px; font-size: 13px; } + .vs-faq-head { padding: 12px 14px; font-size: 13px; min-height: 48px; } + .vs-faq-actions { overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; } + .vs-text-btn { flex-shrink: 0; } + + .vs-footer { padding: 20px 12px calc(16px + env(safe-area-inset-bottom, 0px)); font-size: 11px; line-height: 1.7; } + .vs-top-btn { + right: 14px; bottom: calc(72px + env(safe-area-inset-bottom, 0px)); + width: 44px; height: 44px; border-radius: 12px; font-size: 16px; + } + + .vs-m-bottom-bar { + display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 35; + background: rgba(255,255,255,0.96); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); + border-top: 1px solid ${C_LINE}; box-shadow: 0 -4px 20px rgba(15,23,42,0.06); + padding: 6px 8px calc(6px + env(safe-area-inset-bottom, 0px)); + justify-content: space-around; gap: 4px; + } + .vs-m-tab { + flex: 1; min-width: 0; max-width: 72px; display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: 3px; min-height: 48px; padding: 4px 2px; border: none; background: transparent; border-radius: 10px; + font-size: 10px; color: ${C_MUTED}; touch-action: manipulation; cursor: pointer; + } + .vs-m-tab-icon { font-size: 18px; line-height: 1; opacity: 0.75; } + .vs-m-tab.active { color: ${C_PRIMARY}; font-weight: 700; background: rgba(15,118,110,0.08); } + .vs-m-tab.active .vs-m-tab-icon { opacity: 1; } + .vs-m-tab:focus-visible { outline: 2px solid ${C_PRIMARY_L}; outline-offset: 1px; } + + .vs-m-drawer-mask.is-open { + display: block; position: fixed; inset: 0; z-index: 50; background: rgba(15,23,42,0.45); + animation: vs-fade-in 0.2s ease; + } + .vs-m-drawer { + position: fixed; left: 0; right: 0; bottom: 0; z-index: 51; + background: ${C_CARD}; border-radius: 16px 16px 0 0; + max-height: min(72vh, 520px); display: flex; flex-direction: column; + transform: translateY(100%); transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1); + padding-bottom: env(safe-area-inset-bottom, 0px); + box-shadow: 0 -8px 32px rgba(15,23,42,0.12); + } + .vs-m-drawer.is-open { transform: translateY(0); } + .vs-m-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px 10px; border-bottom: 1px solid ${C_LINE}; flex-shrink: 0; } + .vs-m-drawer-head h3 { margin: 0; font-size: 16px; font-weight: 700; } + .vs-m-drawer-close { min-width: 44px; min-height: 44px; border: none; background: ${C_PAGE}; border-radius: 10px; font-size: 20px; color: ${C_MUTED}; cursor: pointer; } + .vs-m-drawer-list { overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 8px 12px 16px; flex: 1; } + .vs-m-drawer-item { + display: flex; align-items: center; gap: 12px; width: 100%; min-height: 48px; padding: 12px 14px; + border: none; background: transparent; border-radius: 12px; text-align: left; font-size: 14px; + color: ${C_TEXT}; touch-action: manipulation; cursor: pointer; margin-bottom: 4px; + } + .vs-m-drawer-item.active { background: rgba(15,118,110,0.1); color: ${C_PRIMARY_D}; font-weight: 600; } + .vs-m-drawer-num { width: 26px; height: 26px; border-radius: 8px; background: ${C_PAGE}; font-size: 12px; font-weight: 700; color: ${C_MUTED}; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } + .vs-m-drawer-item.active .vs-m-drawer-num { background: ${C_PRIMARY}; color: #fff; } +} + +.vs-m-card-row { + background: ${C_PAGE}; border: 1px solid ${C_LINE}; border-radius: 12px; padding: 14px 16px; +} +.vs-m-card-row-label { font-size: 11px; font-weight: 700; color: ${C_MUTED}; letter-spacing: 0.04em; margin-bottom: 4px; } +.vs-m-card-row-value { font-size: 14px; color: #334155; line-height: 1.6; } +.vs-m-card-row-value strong { color: ${C_PRIMARY}; } + +@keyframes vs-fade-in { from { opacity: 0; } to { opacity: 1; } } + +/* JS / 设备 UA 兜底(WebView、微信、iframe 内预览) */ +html.vs-mobile-env .vs-root, +.vs-root--mobile { max-width: 100vw; overflow-x: hidden; padding-bottom: env(safe-area-inset-bottom, 0px); } +html.vs-mobile-env .vs-root .vs-m-header, +.vs-root--mobile .vs-m-header { + display: flex !important; align-items: center; justify-content: space-between; gap: 10px; + position: fixed; top: 0; left: 0; right: 0; z-index: 40; + height: calc(48px + env(safe-area-inset-top, 0px)); + padding: env(safe-area-inset-top, 0px) 12px 0 12px; + background: rgba(255,255,255,0.92); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid ${C_LINE}; box-shadow: 0 2px 12px rgba(15,23,42,0.06); +} +html.vs-mobile-env .vs-root .vs-m-bottom-bar, +.vs-root--mobile .vs-m-bottom-bar { + display: flex !important; position: fixed; left: 0; right: 0; bottom: 0; z-index: 35; + background: rgba(255,255,255,0.96); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); + border-top: 1px solid ${C_LINE}; box-shadow: 0 -4px 20px rgba(15,23,42,0.06); + padding: 6px 8px calc(6px + env(safe-area-inset-bottom, 0px)); + justify-content: space-around; gap: 4px; +} +html.vs-mobile-env .vs-root .vs-sidebar, +.vs-root--mobile .vs-sidebar { display: none !important; } +html.vs-mobile-env .vs-root .vs-table-wrap, +.vs-root--mobile .vs-table-wrap { display: none !important; } +html.vs-mobile-env .vs-root .vs-m-card-table, +.vs-root--mobile .vs-m-card-table { display: flex !important; flex-direction: column; gap: 10px; margin: 12px 0; } +html.vs-mobile-env .vs-root .vs-hero, +.vs-root--mobile .vs-hero { padding: calc(56px + env(safe-area-inset-top, 0px)) 16px 28px; } +html.vs-mobile-env .vs-root .vs-hero h1, +.vs-root--mobile .vs-hero h1 { font-size: 24px; } +html.vs-mobile-env .vs-root .vs-hero-kpis, +.vs-root--mobile .vs-hero-kpis { grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 20px; } +html.vs-mobile-env .vs-root .vs-shell, +.vs-root--mobile .vs-shell { padding: 0 12px calc(80px + env(safe-area-inset-bottom, 0px)); } +html.vs-mobile-env .vs-root .vs-layout, +.vs-root--mobile .vs-layout { margin-top: 12px; gap: 12px; } +html.vs-mobile-env .vs-root .vs-panel, +.vs-root--mobile .vs-panel { padding: 16px; border-radius: 14px; scroll-margin-top: calc(56px + env(safe-area-inset-top, 0px)); box-shadow: none; } +html.vs-mobile-env .vs-root .vs-bento, +.vs-root--mobile .vs-bento { grid-template-columns: 1fr; gap: 10px; } +html.vs-mobile-env .vs-root .vs-bento-item, +.vs-root--mobile .vs-bento-item { grid-column: 1 / -1 !important; padding: 14px 16px; } +html.vs-mobile-env .vs-root .vs-status-grid, +html.vs-mobile-env .vs-root .vs-rule-grid, +.vs-root--mobile .vs-status-grid, +.vs-root--mobile .vs-rule-grid { grid-template-columns: 1fr; } +html.vs-mobile-env .vs-root .vs-toolbar, +.vs-root--mobile .vs-toolbar { flex-direction: column; align-items: stretch; } +html.vs-mobile-env .vs-root .vs-search, +.vs-root--mobile .vs-search { max-width: none; width: 100%; } +html.vs-mobile-env .vs-root .vs-top-btn, +.vs-root--mobile .vs-top-btn { right: 14px; bottom: calc(72px + env(safe-area-inset-bottom, 0px)); width: 44px; height: 44px; } +html.vs-mobile-env .vs-root .vs-m-drawer-mask.is-open, +.vs-root--mobile .vs-m-drawer-mask.is-open { display: block !important; position: fixed; inset: 0; z-index: 50; background: rgba(15,23,42,0.45); } +`; + + const sectionNav = [ + { key: 'overview', label: '五维总览', icon: '◎' }, + { key: 'relation', label: '状态关联', icon: '↔' }, + { key: 'ops', label: '运营状态', icon: '◇' }, + { key: 'vehicle', label: '车辆状态', icon: '▣' }, + { key: 'outbound', label: '出库状态', icon: '↗' }, + { key: 'license', label: '证照状态', icon: '▤' }, + { key: 'insurance', label: '保险状态', icon: '◆' }, + { key: 'faq', label: '场景问答', icon: '?' }, + { key: 'cheatsheet', label: '速查表', icon: '✓' }, + ]; + + const mobileBottomTabs = [ + { key: 'overview', label: '总览', icon: '◎' }, + { key: 'relation', label: '关联', icon: '↔' }, + { key: 'ops', label: '运营', icon: '◇' }, + { key: 'vehicle', label: '车辆', icon: '▣' }, + { key: '__menu', label: '目录', icon: '☰' }, + ]; + + const dimIcons = { + ops: 'M4 6h16v4H4zm0 6h10v4H4zm0 6h14v4H4', + vehicle: 'M5 17h14l-1.5-5H6.5L5 17zm2.5-7l1-3h7l1 3M7 9V7h10v2', + outbound: 'M4 8h16v10H4zm4-4h8v4H8z', + license: 'M6 4h12v16H6zm3 4h6v2H9zm0 4h6v2H9z', + insurance: 'M12 3l8 4v6c0 4-3.5 7-8 8-4.5-1-8-4-8-8V7l8-4z', + }; + + const dimensions = [ + { key: 'ops', name: '运营状态', question: '这辆车现在处于什么业务阶段?', color: C_PRIMARY }, + { key: 'vehicle', name: '车辆状态', question: '是否被某笔业务「占位」?', color: C_BLUE }, + { key: 'outbound', name: '出库状态', question: '若已离开车队,是因何原因出库?', color: C_VIOLET }, + { key: 'license', name: '证照状态', question: '行驶证/沪牌等评是否有效?', color: C_AMBER }, + { key: 'insurance', name: '保险状态', question: '交强险/商业险是否齐全有效?', color: C_ROSE }, + ]; + + const opsStatuses = [ + { name: '租赁', desc: '车辆当前在租赁业务合同下,运维已完成交车,客户尚未还车。车辆不在库,属于对外出租中。', tag: '在外运营', tagColor: C_BLUE }, + { name: '自营', desc: '车辆当前在自营业务合同下,运维已完成交车,尚未还车。与「租赁」类似,业务类型为自营。', tag: '在外运营', tagColor: C_VIOLET }, + { name: '可运营', desc: '车辆在库;证照、保险均为正常。可随时备车,备车完成后可随时交车。', tag: '在库·可交车', tagColor: C_GREEN }, + { name: '待运营', desc: '车辆在库,但证照或保险至少一项异常。可以备车,但禁止交车,避免证照过期或无保险上路风险。', tag: '在库·禁交车', tagColor: C_AMBER }, + { name: '退出运营', desc: '车辆已退出运营体系。不再生成年审任务,不可备车、不可交车。适用于报废、三方退租、销售等历史车辆。(完整流程部分尚未上线,可联系数智部人工处理。)', tag: '历史车辆', tagColor: C_MUTED }, + ]; + + const vehicleStatuses = [ + { name: '待验车', desc: '采购/三方租用入库后尚未完成验车。', upcoming: true, group: 'stock' }, + { name: '未备车', desc: '尚未备车,可随时发起备车。', group: 'stock' }, + { name: '已备车', desc: '已完成备车并在备车库中,可随时交车(须证照/保险正常、运营状态允许)。', group: 'stock' }, + { name: '待交车', desc: '租赁合同已确定车牌,签约后占位,防止被其他业务误交。', group: 'stock' }, + { name: '已交车', desc: '运维 E 签宝交车签章完成即算已交车,无需等待客户签章。此时运营状态必为租赁或自营。', group: 'stock' }, + { name: '待还车', desc: '还车任务已生成,还车流程尚未完成。', group: 'stock' }, + { name: '呆滞车', desc: '将随呆滞车管理功能上线。', upcoming: true, group: 'flow' }, + { name: '报废中', desc: '报废流程已审批通过,尚未完成报废情况填写。', group: 'flow' }, + { name: '维修中', desc: '维修单未完成或未标记已修复;修复后恢复维修前车辆状态。', upcoming: true, group: 'flow' }, + { name: '销售中', desc: '销售流程已通过,尚未完成销售结果填写;完成后车辆状态为无、出库为销售出库。', upcoming: true, group: 'flow' }, + { name: '过户中', desc: '内部过户进行中,尚未完成信息填写与证件更新。', upcoming: true, group: 'flow' }, + { name: '替换中', desc: '替换车流程通过后新车标记;完成后新车为已交车。旧车永久替换则还车后未备车,临时替换仍已交车。', group: 'flow' }, + { name: '调拨中', desc: '调拨流程通过后标记;接收人完成接车后恢复调拨前状态。', group: 'flow' }, + { name: '异动中', desc: '异动流程通过后标记;操作人完成异动结束后恢复异动前状态。', group: 'flow' }, + { name: '三方退租中', desc: '三方退租已通过,尚未完成退租确认。', upcoming: true, group: 'flow' }, + { name: '无', desc: '报废/销售/三方退租完成后的终态,不再参与在库运营占位。', group: 'end' }, + ]; + + const vehicleFilters = [ + { key: 'all', label: '全部' }, + { key: 'stock', label: '在库流转' }, + { key: 'flow', label: '流程占用' }, + { key: 'upcoming', label: '暂未上线' }, + ]; + + const outboundStatuses = [ + { name: '无', meaning: '车辆未出库,仍在车队管理中', when: '默认;还车在库后' }, + { name: '三方退租出库', meaning: '外部租赁合同终止并完成退租确认', when: '三方退租流程完结' }, + { name: '销售出库', meaning: '车辆已销售离场', when: '销售结果填写完成' }, + { name: '报废出库', meaning: '车辆已报废离场', when: '报废结果填写完成' }, + ]; + + const licenseStatuses = [ + { name: '正常', desc: '行驶证检验有效期、沪牌车等评时间在有效期内。', tone: C_GREEN }, + { name: '异常', desc: '行驶证已过期,和/或沪牌车等评时间已到期。', tone: C_ROSE }, + { name: '无', desc: '车辆已出库,无需再维护证照信息。', tone: C_MUTED }, + ]; + + const insuranceStatuses = [ + { name: '正常', desc: '交强险、商业险均存在,且最新一条记录在有效期内。', tone: C_GREEN }, + { name: '异常', desc: '交强险或商业险缺失、已过期,或最新记录为停保/退保。', tone: C_ROSE }, + { name: '无', desc: '车辆已退出运营,无需再维护保险。', tone: C_MUTED }, + ]; + + const relationRules = [ + '运营状态为「租赁/自营」表示已交车在客户处,不再交车;「可运营/待运营」为在库待调度;「退出运营」不可备车交车。', + '车辆状态为「已备车」或合法「待交车」等才允许交车;维修中、调拨中、异动中等流程占用时,其他业务不可占用该车。', + '证照、保险均须为「正常」才能交车;任一项「异常」时,在库车辆运营状态体现为「待运营」——可备车,不可交车。', + '退出运营的车辆不再生成年审任务,且不可备车、不可交车。', + ]; + + const flowRecoveries = [ + { from: '维修中', to: '维修标记「已修复」后,恢复维修前车辆状态' }, + { from: '调拨中', to: '接收方完成调拨接车后,恢复调拨前车辆状态' }, + { from: '异动中', to: '操作人完成异动结束后,恢复异动前车辆状态' }, + { from: '替换中(新车)', to: '替换完成后变为已交车;旧车永久替换还车后未备车,临时替换仍已交车' }, + { from: '报废/销售/三方退租中', to: '完成结果确认后,车辆状态为无,配合出库状态与退出运营' }, + ]; + + const lifecycleSteps = [ + '采购/租用入库', '待验车', '未备车', '已备车', '待交车', '已交车', '待还车', '回库', + ]; + + const faqs = [ + { q: '车在库里,为什么不让交车?', a: '请依次查看:① 运营状态是否为「待运营」(证照或保险异常);② 车辆状态是否为「已备车」或合法「待交车」;③ 是否处于维修中、调拨中、异动中等流程占用。任一不满足都可能禁止交车。' }, + { q: '合同已签、车牌已定,别的业务还能用这辆车吗?', a: '不能。车辆状态为「待交车」时会占用该车,直到交车完成变为「已交车」,防止误交给其他客户。' }, + { q: '运维 E 签宝交车签章后,客户还没签,算交车了吗?', a: '算。系统以运维交车签章完成为准,车辆状态变为「已交车」,运营状态变为「租赁」或「自营」。' }, + { q: '还车任务已建但客户还没还,车算什么状态?', a: '车辆状态为「待还车」;运营状态仍为「租赁」或「自营」,直到还车流程完成。' }, + { q: '还车完成后,为什么有的是「可运营」有的是「待运营」?', a: '还车后车辆回库。证照、保险均正常 →「可运营」;证照或保险有异常 →「待运营」,须先处理再交车。' }, + { q: '调拨中的车,对方还没接,能拿去交车吗?', a: '不能。调拨流程占用为「调拨中」,需等接收方完成接车流程后,车辆才恢复调拨前状态。' }, + { q: '维修中的车修好了,状态怎么变?', a: '维修结果标记为「已修复」后,车辆状态恢复为进入维修前的状态(如已备车、未备车等)。' }, + { q: '替换车完成后,新旧车分别是什么状态?', a: '新车:替换完成后为「已交车」,运营为租赁/自营。旧车:永久替换则还车后「未备车」;临时替换则仍为「已交车」。' }, + { q: '报废/销售/三方退租后,系统里还能看到哪些状态?', a: '车辆状态「无」、出库状态为对应出库类型、运营状态「退出运营」、证照/保险「无」。该车不再参与备车、交车、年审。' }, + { q: '有些状态显示但菜单里没有对应功能?', a: '待验车、呆滞车、销售、过户、三方退租、完整报废流程等尚未全部上线。当前异常处理可联系数智部人工协助。' }, + ]; + + const cheatSheet = [ + { action: '备车', condition: '运营:可运营或待运营(非退出运营);车辆状态:须为「未备车」' }, + { action: '交车', condition: '运营:可运营;车辆:已备车或待交车;证照/保险:正常;非各类「××中」占用' }, + { action: '还车', condition: '运营:租赁或自营;生成还车任务后 → 待还车' }, + { action: '调拨', condition: '流程通过 → 调拨中;接车完成 → 恢复原车辆状态' }, + { action: '退出车队', condition: '走完报废/销售/三方退租 → 无 + 出库 + 退出运营' }, + ]; + + const [activeNav, setActiveNav] = useState('overview'); + const [openFaq, setOpenFaq] = useState(0); + const [faqExpandAll, setFaqExpandAll] = useState(false); + const [showTop, setShowTop] = useState(false); + const [vehicleFilter, setVehicleFilter] = useState('all'); + const [vehicleSearch, setVehicleSearch] = useState(''); + var mountRoot = props && props.container ? props.container : null; + const [isMobile, setIsMobile] = useState(function () { + try { return vsDetectMobileLayout(mountRoot); } catch (e) { return false; } + }); + const [navDrawerOpen, setNavDrawerOpen] = useState(false); + + vsEnsureViewport(); + + const activeNavLabel = useMemo(function () { + var found = sectionNav.filter(function (s) { return s.key === activeNav; })[0]; + return found ? found.label : ''; + }, [activeNav]); + + const filteredVehicleStatuses = useMemo(function () { + var q = String(vehicleSearch || '').trim().toLowerCase(); + return vehicleStatuses.filter(function (item) { + if (vehicleFilter === 'upcoming' && !item.upcoming) return false; + if (vehicleFilter === 'stock' && item.group !== 'stock') return false; + if (vehicleFilter === 'flow' && item.group !== 'flow') return false; + if (q && item.name.toLowerCase().indexOf(q) < 0 && item.desc.toLowerCase().indexOf(q) < 0) return false; + return true; + }); + }, [vehicleSearch, vehicleFilter]); + + const scrollTo = useCallback(function (key) { + setActiveNav(key); + var el = document.getElementById('vs-' + key); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, []); + + const scrollToSection = useCallback(function (key) { + setNavDrawerOpen(false); + scrollTo(key); + }, [scrollTo]); + + useLayoutEffect(function () { + var styleId = 'vs-page-styles'; + var styleEl = document.getElementById(styleId); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = styleId; + document.head.appendChild(styleEl); + } + styleEl.textContent = PAGE_STYLE; + + vsEnsureViewport(); + var rootEl = mountRoot || document.getElementById('root'); + function apply() { + var mobile = vsDetectMobileLayout(rootEl); + setIsMobile(mobile); + vsApplyMobileEnv(mobile); + } + apply(); + var mqList = []; + if (window.matchMedia) { + ['(max-width: 768px)', '(max-device-width: 820px)', '(hover: none) and (pointer: coarse)'].forEach(function (q) { + var mq = window.matchMedia(q); + mqList.push(mq); + if (mq.addEventListener) mq.addEventListener('change', apply); + else if (mq.addListener) mq.addListener(apply); + }); + } + window.addEventListener('resize', apply); + window.addEventListener('orientationchange', apply); + if (window.visualViewport) { + window.visualViewport.addEventListener('resize', apply); + } + var ro = null; + if (rootEl && window.ResizeObserver) { + ro = new ResizeObserver(apply); + ro.observe(rootEl); + } + var t1 = setTimeout(apply, 100); + var t2 = setTimeout(apply, 500); + return function () { + clearTimeout(t1); + clearTimeout(t2); + mqList.forEach(function (mq) { + if (mq.removeEventListener) mq.removeEventListener('change', apply); + else if (mq.removeListener) mq.removeListener(apply); + }); + window.removeEventListener('resize', apply); + window.removeEventListener('orientationchange', apply); + if (window.visualViewport) { + window.visualViewport.removeEventListener('resize', apply); + } + if (ro) ro.disconnect(); + }; + }, [mountRoot]); + + useEffect(function () { + if (!navDrawerOpen) return; + var prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return function () { document.body.style.overflow = prev; }; + }, [navDrawerOpen]); + + useEffect(function () { + var sections = sectionNav.map(function (s) { return document.getElementById('vs-' + s.key); }).filter(Boolean); + if (!sections.length) return; + var observer = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + setActiveNav(entry.target.id.replace('vs-', '')); + } + }); + }, + { rootMargin: '-15% 0px -55% 0px', threshold: 0 } + ); + sections.forEach(function (el) { observer.observe(el); }); + return function () { observer.disconnect(); }; + }, []); + + useEffect(function () { + function onScroll() { setShowTop(window.scrollY > 480); } + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + return function () { window.removeEventListener('scroll', onScroll); }; + }, []); + + const renderPill = function (text, color) { + return React.createElement('span', { + className: 'vs-pill', + style: { background: color + '14', color: color, border: '1px solid ' + color + '33' } + }, text); + }; + + const renderSvgIcon = function (d, color) { + return React.createElement('svg', { + width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', + stroke: color || 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', + 'aria-hidden': true + }, React.createElement('path', { d: d })); + }; + + const renderPanelHead = function (num, tag, title, desc) { + return React.createElement('div', { className: 'vs-panel-head' }, + React.createElement('span', { className: 'vs-panel-tag' }, tag), + React.createElement('h2', { className: 'vs-panel-title' }, + React.createElement('span', { className: 'vs-panel-num' }, num), + title + ), + desc ? React.createElement('p', { className: 'vs-panel-desc' }, desc) : null + ); + }; + + const renderMobileCardTable = function (rows, columns, rowKey) { + return React.createElement('div', { className: 'vs-m-card-table' }, + (rows || []).map(function (row, i) { + var rk = rowKey ? row[rowKey] : i; + return React.createElement('div', { key: rk, className: 'vs-m-card-row' }, + columns.map(function (col, ci) { + var val = row[col.key]; + return React.createElement('div', { + key: col.key, + style: { marginBottom: ci < columns.length - 1 ? 10 : 0 } + }, + React.createElement('div', { className: 'vs-m-card-row-label' }, col.label), + React.createElement('div', { className: 'vs-m-card-row-value' }, + col.strong ? React.createElement('strong', null, val) : val + ) + ); + }) + ); + }) + ); + }; + + const renderFlowTable = function () { + return React.createElement(React.Fragment, null, + React.createElement('div', { className: 'vs-table-wrap' }, + React.createElement('table', { className: 'vs-table' }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { width: 148 } }, '占用状态'), + React.createElement('th', null, '结束后') + ) + ), + React.createElement('tbody', null, + flowRecoveries.map(function (r) { + return React.createElement('tr', { key: r.from }, + React.createElement('td', null, React.createElement('strong', null, r.from)), + React.createElement('td', null, r.to) + ); + }) + ) + ) + ), + renderMobileCardTable(flowRecoveries, [ + { key: 'from', label: '占用状态', strong: true }, + { key: 'to', label: '结束后' } + ], 'from') + ); + }; + + const renderOutboundTable = function () { + return React.createElement(React.Fragment, null, + React.createElement('div', { className: 'vs-table-wrap' }, + React.createElement('table', { className: 'vs-table' }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', null, '状态'), + React.createElement('th', null, '含义'), + React.createElement('th', null, '何时产生') + ) + ), + React.createElement('tbody', null, + outboundStatuses.map(function (r) { + return React.createElement('tr', { key: r.name }, + React.createElement('td', null, React.createElement('strong', null, r.name)), + React.createElement('td', null, r.meaning), + React.createElement('td', null, r.when) + ); + }) + ) + ) + ), + renderMobileCardTable(outboundStatuses, [ + { key: 'name', label: '状态', strong: true }, + { key: 'meaning', label: '含义' }, + { key: 'when', label: '何时产生' } + ], 'name') + ); + }; + + const renderCheatTable = function () { + return React.createElement(React.Fragment, null, + React.createElement('div', { className: 'vs-table-wrap' }, + React.createElement('table', { className: 'vs-table' }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: { width: 100 } }, '操作'), + React.createElement('th', null, '须满足(摘要)') + ) + ), + React.createElement('tbody', null, + cheatSheet.map(function (r) { + return React.createElement('tr', { key: r.action }, + React.createElement('td', null, React.createElement('strong', { style: { color: C_PRIMARY } }, r.action)), + React.createElement('td', null, r.condition) + ); + }) + ) + ) + ), + renderMobileCardTable(cheatSheet, [ + { key: 'action', label: '操作', strong: true }, + { key: 'condition', label: '须满足(摘要)' } + ], 'action') + ); + }; + + return React.createElement('div', { className: 'vs-root' + (isMobile ? ' vs-root--mobile' : ''), 'data-layout': isMobile ? 'mobile' : 'desktop' }, + React.createElement('a', { href: '#vs-overview', className: 'vs-skip' }, '跳到正文'), + + React.createElement('header', { className: 'vs-m-header' }, + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, + React.createElement('div', { className: 'vs-m-header-title' }, '车辆状态说明'), + React.createElement('div', { className: 'vs-m-header-sub' }, activeNavLabel || '浏览中') + ), + React.createElement('button', { + type: 'button', + className: 'vs-m-header-btn', + onClick: function () { setNavDrawerOpen(true); }, + 'aria-label': '打开目录' + }, '目录') + ), + + React.createElement('div', { + className: 'vs-m-drawer-mask' + (navDrawerOpen ? ' is-open' : ''), + onClick: function () { setNavDrawerOpen(false); }, + role: 'presentation' + }, + React.createElement('div', { + className: 'vs-m-drawer' + (navDrawerOpen ? ' is-open' : ''), + onClick: function (e) { e.stopPropagation(); }, + role: 'dialog', + 'aria-modal': true, + 'aria-label': '文档目录' + }, + React.createElement('div', { className: 'vs-m-drawer-head' }, + React.createElement('h3', null, '章节目录'), + React.createElement('button', { + type: 'button', + className: 'vs-m-drawer-close', + onClick: function () { setNavDrawerOpen(false); }, + 'aria-label': '关闭目录' + }, '×') + ), + React.createElement('div', { className: 'vs-m-drawer-list' }, + sectionNav.map(function (item, idx) { + return React.createElement('button', { + key: item.key, + type: 'button', + className: 'vs-m-drawer-item' + (activeNav === item.key ? ' active' : ''), + onClick: function () { scrollToSection(item.key); } + }, + React.createElement('span', { className: 'vs-m-drawer-num' }, idx + 1), + item.label + ); + }) + ) + ) + ), + + React.createElement('header', { className: 'vs-hero' }, + React.createElement('div', { className: 'vs-hero-inner' }, + React.createElement('span', { className: 'vs-hero-tag' }, 'ONE-OS · 车辆管理'), + React.createElement('h1', null, '车辆状态说明'), + React.createElement('p', { className: 'vs-hero-desc' }, + '五类状态组合描述一辆车。读懂关联关系,快速判断能否备车、交车,以及流程占用时的恢复规则。' + ), + React.createElement('div', { className: 'vs-hero-kpis' }, + [{ v: '5', l: '状态维度' }, { v: '16', l: '车辆状态' }, { v: '5', l: '运营状态' }, { v: '10', l: '场景问答' }].map(function (k) { + return React.createElement('div', { key: k.l, className: 'vs-kpi' }, + React.createElement('div', { className: 'vs-kpi-val' }, k.v), + React.createElement('div', { className: 'vs-kpi-lbl' }, k.l) + ); + }) + ) + ) + ), + + React.createElement('div', { className: 'vs-shell' }, + React.createElement('div', { className: 'vs-layout' }, + React.createElement('aside', { className: 'vs-sidebar' }, + React.createElement('nav', { className: 'vs-nav-card', 'aria-label': '文档目录' }, + React.createElement('div', { className: 'vs-nav-label' }, '目录'), + React.createElement('div', { className: 'vs-nav-list' }, + sectionNav.map(function (item) { + return React.createElement('button', { + key: item.key, + type: 'button', + className: 'vs-nav-item' + (activeNav === item.key ? ' active' : ''), + onClick: function () { scrollTo(item.key); }, + 'aria-current': activeNav === item.key ? 'true' : undefined + }, + React.createElement('span', { className: 'vs-nav-dot' }), + item.label + ); + }) + ) + ) + ), + + React.createElement('main', { className: 'vs-main', id: 'vs-main-content' }, + + React.createElement('section', { id: 'vs-overview', className: 'vs-panel' }, + renderPanelHead('1', 'OVERVIEW', '五维总览', '同一辆车会同时带有五类状态,需要组合理解,不能只看其中一项。'), + React.createElement('div', { className: 'vs-bento' }, + dimensions.map(function (d) { + return React.createElement('div', { + key: d.key, + className: 'vs-bento-item', + style: { '--accent': d.color, '--accent-bg': d.color + '18' } + }, + React.createElement('div', { className: 'vs-bento-icon' }, renderSvgIcon(dimIcons[d.key], d.color)), + React.createElement('h4', null, d.name), + React.createElement('p', null, d.question) + ); + }) + ), + React.createElement('div', { className: 'vs-example' }, + React.createElement('strong', { style: { fontSize: 13, color: C_PRIMARY } }, '组合示例'), + React.createElement('p', { style: { margin: '8px 0 0', fontSize: 13, color: '#334155' } }, + '一辆在客户处正常使用的租赁车,可能同时显示为:' + ), + React.createElement('div', { className: 'vs-tags' }, + renderPill('运营 = 租赁', C_PRIMARY), + renderPill('车辆 = 已交车', C_BLUE), + renderPill('出库 = 无', C_MUTED), + renderPill('证照 = 正常', C_GREEN), + renderPill('保险 = 正常', C_GREEN) + ) + ), + React.createElement('div', { className: 'vs-callout' }, + React.createElement('span', { className: 'vs-callout-icon' }, '!'), + React.createElement('span', null, + React.createElement('strong', null, '提示:'), + '若证照或保险异常,即使车在库里显示「待运营」,也不能交车(通常仍可备车)。' + ) + ) + ), + + React.createElement('section', { id: 'vs-relation', className: 'vs-panel' }, + renderPanelHead('2', 'RELATION', '状态关联与生命周期', '理解交车门槛、在库流转与流程占用的恢复规则。'), + React.createElement('div', { className: 'vs-subtitle' }, '2.1 决定「能不能交车」'), + React.createElement('div', { className: 'vs-rule-grid' }, + relationRules.map(function (t, i) { + return React.createElement('div', { key: i, className: 'vs-rule-card' }, + React.createElement('span', { className: 'vs-rule-idx' }, i + 1), + React.createElement('span', null, t) + ); + }) + ), + React.createElement('div', { className: 'vs-subtitle' }, '2.2 典型生命周期'), + React.createElement('div', { className: 'vs-timeline' }, + lifecycleSteps.map(function (step) { + return React.createElement('div', { key: step, className: 'vs-timeline-step' }, step); + }) + ), + React.createElement('p', { style: { fontSize: 13, color: C_MUTED, margin: '0 0 4px' } }, + '交车签章后运营状态变为租赁/自营;还车完成后车辆状态回未备车,运营状态回可运营或待运营(视证照/保险)。' + ), + React.createElement('div', { className: 'vs-subtitle' }, '2.3 流程占用与恢复'), + renderFlowTable() + ), + + React.createElement('section', { id: 'vs-ops', className: 'vs-panel' }, + renderPanelHead('3', 'OPS STATUS', '运营状态', '界定车辆当前处于什么业务情况下,共 5 种。'), + React.createElement('div', { className: 'vs-status-grid' }, + opsStatuses.map(function (item) { + return React.createElement('div', { key: item.name, className: 'vs-status-card' }, + React.createElement('h3', null, item.name, renderPill(item.tag, item.tagColor)), + React.createElement('p', null, item.desc) + ); + }) + ), + React.createElement('div', { className: 'vs-callout' }, + React.createElement('span', { className: 'vs-callout-icon' }, 'i'), + React.createElement('span', null, + React.createElement('strong', null, '记忆要点:'), + '「可运营」「待运营」= 在库;「租赁」「自营」= 已交车在客户处;「退出运营」= 已离开运营体系。' + ) + ) + ), + + React.createElement('section', { id: 'vs-vehicle', className: 'vs-panel' }, + renderPanelHead('4', 'VEHICLE STATUS', '车辆状态', '预占位状态,避免多笔业务同时占用同一台车。'), + React.createElement('div', { className: 'vs-toolbar' }, + React.createElement('div', { className: 'vs-chips' }, + vehicleFilters.map(function (f) { + return React.createElement('button', { + key: f.key, + type: 'button', + className: 'vs-chip' + (vehicleFilter === f.key ? ' active' : ''), + onClick: function () { setVehicleFilter(f.key); } + }, f.label); + }) + ), + Input ? React.createElement(Input, { + className: 'vs-search', + allowClear: true, + placeholder: '搜索状态名称…', + value: vehicleSearch, + onChange: function (e) { setVehicleSearch(e.target.value); }, + 'aria-label': '搜索车辆状态' + }) : React.createElement('input', { + className: 'vs-search', + type: 'search', + placeholder: '搜索状态名称…', + value: vehicleSearch, + onChange: function (e) { setVehicleSearch(e.target.value); }, + 'aria-label': '搜索车辆状态', + style: { padding: '8px 12px', borderRadius: 8, border: '1px solid ' + C_LINE, fontSize: 13 } + }) + ), + filteredVehicleStatuses.length === 0 + ? React.createElement('div', { className: 'vs-empty' }, '没有匹配的状态,请调整筛选或搜索关键词。') + : React.createElement('div', { className: 'vs-status-grid' }, + filteredVehicleStatuses.map(function (item) { + return React.createElement('div', { key: item.name, className: 'vs-status-card' }, + React.createElement('h3', null, + item.name, + item.upcoming ? renderPill('暂未上线', C_AMBER) : null + ), + React.createElement('p', null, item.desc) + ); + }) + ) + ), + + React.createElement('section', { id: 'vs-outbound', className: 'vs-panel' }, + renderPanelHead('5', 'OUTBOUND', '出库状态', '定义车辆因何种原因离开车队。在库车辆出库状态为「无」。'), + renderOutboundTable(), + React.createElement('div', { className: 'vs-callout' }, + React.createElement('span', { className: 'vs-callout-icon' }, '→'), + React.createElement('span', null, + React.createElement('strong', null, '出库后:'), + '车辆状态一般为「无」,运营状态「退出运营」,证照/保险「无」。' + ) + ) + ), + + React.createElement('section', { id: 'vs-license', className: 'vs-panel' }, + renderPanelHead('6', 'LICENSE', '证照状态', '规避行驶证、沪牌等评等到期仍上路的风险。'), + React.createElement('div', { className: 'vs-status-grid' }, + licenseStatuses.map(function (item) { + return React.createElement('div', { key: item.name, className: 'vs-status-card' }, + React.createElement('h3', null, item.name, renderPill(item.name, item.tone)), + React.createElement('p', null, item.desc) + ); + }) + ), + React.createElement('div', { className: 'vs-callout' }, + React.createElement('span', { className: 'vs-callout-icon' }, '↔'), + React.createElement('span', null, + React.createElement('strong', null, '关联:'), + '证照异常 → 在库车辆运营状态为「待运营」→ 可备车,不可交车。' + ) + ) + ), + + React.createElement('section', { id: 'vs-insurance', className: 'vs-panel' }, + renderPanelHead('7', 'INSURANCE', '保险状态', '确保交车前交强险、商业险齐全有效。'), + React.createElement('div', { className: 'vs-status-grid' }, + insuranceStatuses.map(function (item) { + return React.createElement('div', { key: item.name, className: 'vs-status-card' }, + React.createElement('h3', null, item.name, renderPill(item.name, item.tone)), + React.createElement('p', null, item.desc) + ); + }) + ), + React.createElement('div', { className: 'vs-callout' }, + React.createElement('span', { className: 'vs-callout-icon' }, '↔'), + React.createElement('span', null, + React.createElement('strong', null, '关联:'), + '保险异常 → 在库车辆运营状态为「待运营」→ 可备车,不可交车。停保/退保仍算异常。' + ) + ) + ), + + React.createElement('section', { id: 'vs-faq', className: 'vs-panel' }, + renderPanelHead('8', 'FAQ', '常见场景问答', '点击问题展开答案;支持全部展开或收起。'), + React.createElement('div', { className: 'vs-faq-actions' }, + React.createElement('button', { + type: 'button', + className: 'vs-text-btn', + onClick: function () { setFaqExpandAll(false); setOpenFaq(0); } + }, '展开首条'), + React.createElement('button', { + type: 'button', + className: 'vs-text-btn', + onClick: function () { setFaqExpandAll(true); } + }, '全部展开'), + React.createElement('button', { + type: 'button', + className: 'vs-text-btn', + onClick: function () { setFaqExpandAll(false); setOpenFaq(-1); } + }, '全部收起') + ), + React.createElement('div', { className: 'vs-faq-list' }, + faqs.map(function (item, i) { + var open = faqExpandAll || openFaq === i; + return React.createElement('div', { key: i, className: 'vs-faq' + (open ? ' is-open' : '') }, + React.createElement('button', { + type: 'button', + className: 'vs-faq-head', + onClick: function () { setFaqExpandAll(false); setOpenFaq(open ? -1 : i); }, + 'aria-expanded': open + }, + React.createElement('span', { className: 'vs-faq-q' }, + React.createElement('span', { className: 'vs-faq-num' }, i + 1), + React.createElement('span', null, item.q) + ), + React.createElement('span', { className: 'vs-faq-chevron', 'aria-hidden': true }, '⌄') + ), + open ? React.createElement('div', { className: 'vs-faq-body' }, item.a) : null + ); + }) + ) + ), + + React.createElement('section', { id: 'vs-cheatsheet', className: 'vs-panel' }, + renderPanelHead('9', 'CHEATSHEET', '操作速查表', '备车、交车、还车等操作的前置条件摘要。'), + renderCheatTable() + ) + ) + ) + ), + + React.createElement('footer', { className: 'vs-footer' }, + '文档版本 V1.0 · 依据 ONE-OS 车辆管理业务规则整理 · 部分流程尚未上线以实际系统为准 · 疑问请联系数智部或产品负责人' + ), + + React.createElement('nav', { className: 'vs-m-bottom-bar', 'aria-label': '快捷导航' }, + mobileBottomTabs.map(function (tab) { + var isMenu = tab.key === '__menu'; + var isActive = !isMenu && activeNav === tab.key; + return React.createElement('button', { + key: tab.key, + type: 'button', + className: 'vs-m-tab' + (isActive || (isMenu && navDrawerOpen) ? ' active' : ''), + onClick: function () { + if (isMenu) setNavDrawerOpen(function (v) { return !v; }); + else scrollToSection(tab.key); + }, + 'aria-current': isActive ? 'page' : undefined + }, + React.createElement('span', { className: 'vs-m-tab-icon', 'aria-hidden': true }, tab.icon), + tab.label + ); + }) + ), + + React.createElement('button', { + type: 'button', + className: 'vs-top-btn' + (showTop ? ' visible' : ''), + onClick: function () { window.scrollTo({ top: 0, behavior: 'smooth' }); }, + 'aria-label': '回到顶部' + }, '↑') + ); +}; + +export default Component;