迭代 ONE-OS 原型导航:VoltAgent 视觉改版、密码门禁与侧栏按业务线切换;车辆氢费明细标注目录同步至 v1.5。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-09 18:34:31 +08:00
parent 6ccbbef86a
commit 824512a224
7 changed files with 1445 additions and 574 deletions

View File

@@ -295,7 +295,7 @@ export function syncPrototypeNavRegistry(options = {}) {
let bumped = 0;
for (const prototypeId of targetIds) {
if (prototypeId === NAV_PROTOTYPE_ID) continue;
if (prototypeId === NAV_PROTOTYPE_ID && options.prototype !== NAV_PROTOTYPE_ID) continue;
const title = titleMap.get(prototypeId) || prototypeId;
const prototypeDir = path.join(projectRoot, 'src/prototypes', prototypeId);
if (!fs.existsSync(prototypeDir)) {

View File

@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "oneos-prototype-nav",
"pageId": "nav",
"updatedAt": 1783589987167,
"updatedAt": 1783600000000,
"nodes": [
{
"id": "opn-hero",
@@ -51,37 +51,6 @@
},
"markdownMap": {},
"directory": {
"nodes": [
{
"type": "folder",
"id": "oneos-project-nav",
"title": "ONE-OS 原型导航",
"defaultExpanded": true,
"children": [
{
"type": "link",
"id": "nav-link-oneos-prototype-nav",
"title": "原型导航(当前)",
"href": "/prototypes/oneos-prototype-nav",
"target": "self"
},
{
"type": "folder",
"id": "nav-folder-1782874576229-r8atbu",
"title": "OneOS",
"defaultExpanded": true,
"children": [
{
"type": "link",
"id": "nav-link-oneos-web-workbench",
"title": "工作台",
"href": "/prototypes/oneos-web-workbench",
"target": "self"
}
]
}
]
}
]
"nodes": []
}
}

View File

@@ -3,15 +3,24 @@
* ONE-OS 原型目录导航:按菜单分组快速进入各功能原型
*/
import './style.css';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
ArrowRight,
Compass,
ArrowUpRight,
Clock3,
ExternalLink,
FileCode2,
History,
Layers,
LayoutGrid,
Lock,
Search,
X,
Zap,
} from 'lucide-react';
import {
countNavLinks,
@@ -29,13 +38,49 @@ import {
type RecentUpdateEntry,
} from './nav-data';
function linkMeta(link: NavLinkItem, record: PrototypeRegistryRecord | null): string | undefined {
const parts: string[] = [];
if (link.version || record?.version) parts.push(link.version || record?.version || '');
if (link.lastUpdatedLabel) parts.push(`${link.lastUpdatedLabel}更新`);
function formatRecordUpdatedAt(record: PrototypeRegistryRecord | null): string | undefined {
if (record?.lastUpdated) {
const date = new Date(record.lastUpdated);
if (!Number.isNaN(date.getTime())) {
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
}
}
const latest = record?.changelog?.[0];
if (latest?.summary && parts.length < 2) parts.push(latest.summary);
return parts.filter(Boolean).join(' · ') || undefined;
if (latest) {
const time = latest.time.length === 5 ? `${latest.time}:00` : latest.time;
return `${latest.date.replace(/-/g, '/')} ${time}`;
}
return undefined;
}
function linkMeta(record: PrototypeRegistryRecord | null): string | undefined {
const updatedAt = formatRecordUpdatedAt(record);
if (updatedAt) return `${updatedAt} 更新`;
return undefined;
}
function sidebarSectionLabel(section: NavSection): string {
if (section.title === '小羚羚') return '小羚羚「小程序」';
return section.title;
}
function sectionIdFromHash(): string | null {
const match = window.location.hash.match(/^#opn-section-(.+)$/u);
return match ? match[1] : null;
}
function resolveInitialSectionId(sections: NavSection[]): string {
const fromHash = sectionIdFromHash();
if (fromHash && sections.some((section) => section.id === fromHash)) return fromHash;
return sections[0]?.id || '';
}
const NEW_TAB_LINK_PROPS = {
@@ -43,6 +88,99 @@ const NEW_TAB_LINK_PROPS = {
rel: 'noopener noreferrer',
} as const;
const AUTH_STORAGE_KEY = 'oneos-prototype-nav-auth';
const NAV_ACCESS_PASSWORD = 'lingniu';
function readAuthSession(): boolean {
try {
return sessionStorage.getItem(AUTH_STORAGE_KEY) === 'ok';
} catch {
return false;
}
}
function writeAuthSession(): void {
try {
sessionStorage.setItem(AUTH_STORAGE_KEY, 'ok');
} catch {
/* ignore quota / private mode */
}
}
function PasswordGate({ onUnlock }: { onUnlock: () => void }) {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [submitting, setSubmitting] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
setSubmitting(true);
setError('');
window.setTimeout(() => {
if (password === NAV_ACCESS_PASSWORD) {
writeAuthSession();
onUnlock();
return;
}
setError('密码不正确,请重试');
setSubmitting(false);
inputRef.current?.focus();
inputRef.current?.select();
}, 120);
};
return (
<div className="opn-gate" data-annotation-id="opn-password-gate">
<div className="opn-gate-card" role="dialog" aria-modal="true" aria-labelledby="opn-gate-title">
<span className="opn-gate-icon" aria-hidden>
<Lock size={22} strokeWidth={1.75} />
</span>
<p className="opn-gate-eyebrow">Restricted Access</p>
<h1 id="opn-gate-title" className="opn-gate-title"></h1>
<p className="opn-gate-lead">使访</p>
<form className="opn-gate-form" onSubmit={handleSubmit}>
<div className="opn-gate-field">
<label className="opn-gate-label" htmlFor="opn-gate-password">
访
</label>
<input
ref={inputRef}
id="opn-gate-password"
className={`opn-gate-input${error ? ' opn-gate-input--error' : ''}`}
type="password"
value={password}
onChange={(event) => {
setPassword(event.target.value);
if (error) setError('');
}}
placeholder="请输入密码"
autoComplete="current-password"
aria-invalid={Boolean(error)}
aria-describedby={error ? 'opn-gate-error' : undefined}
disabled={submitting}
/>
</div>
{error && (
<p id="opn-gate-error" className="opn-gate-error" role="alert">
{error}
</p>
)}
<button type="submit" className="opn-gate-submit" disabled={submitting || !password.trim()}>
{submitting ? '验证中…' : '进入导航'}
</button>
</form>
</div>
</div>
);
}
function NavLinkCard({
link,
meta,
@@ -54,7 +192,9 @@ function NavLinkCard({
}) {
const Icon = iconForLink(link.title);
const record = getPrototypeRegistryRecord(link.prototypeId);
const metaText = meta ?? linkMeta(link, record);
const metaText = meta ?? linkMeta(record);
const version = link.version || record?.version;
return (
<button
type="button"
@@ -64,13 +204,16 @@ function NavLinkCard({
data-annotation-id={`opn-link-${link.prototypeId}-${link.id}`}
>
<span className="opn-link-card-icon" aria-hidden>
<Icon size={20} />
<Icon size={18} strokeWidth={1.75} />
</span>
<span className="opn-link-card-body">
<span className="opn-link-card-title-row">
<span className="opn-link-card-title">{link.title}</span>
{version && <span className="opn-link-card-version">{version}</span>}
</span>
{metaText && <span className="opn-link-card-meta">{metaText}</span>}
</span>
<ArrowRight size={18} className="opn-link-card-arrow" aria-hidden />
<ArrowUpRight size={16} className="opn-link-card-arrow" aria-hidden />
</button>
);
}
@@ -87,10 +230,10 @@ function SubGroupPanel({
<div className="opn-subgroup" data-annotation-id={`opn-group-${group.id}`}>
<header className="opn-subgroup-header">
<span className="opn-subgroup-icon" aria-hidden>
<Icon size={18} />
<Icon size={16} strokeWidth={1.75} />
</span>
<h3>{group.title}</h3>
<span className="opn-subgroup-count">{group.links.length} </span>
<span className="opn-subgroup-count">{group.links.length}</span>
</header>
<div className="opn-link-grid">
{group.links.map((link) => (
@@ -109,6 +252,9 @@ function SectionBlock({
onSelectLink: (link: NavLinkItem) => void;
}) {
const Icon = section.icon;
const linkCount = section.links.length
+ section.subGroups.reduce((sum, group) => sum + group.links.length, 0);
return (
<section
className="opn-section"
@@ -118,11 +264,14 @@ function SectionBlock({
<header className="opn-section-header">
<div className="opn-section-heading">
<span className="opn-section-icon" aria-hidden>
<Icon size={22} />
<Icon size={20} strokeWidth={1.75} />
</span>
<div>
<div className="opn-section-title-row">
<h2>{section.title}</h2>
<p>{section.description}</p>
<span className="opn-section-badge">{linkCount}</span>
</div>
{section.description && <p>{section.description}</p>}
</div>
</div>
</header>
@@ -146,9 +295,19 @@ function SectionBlock({
);
}
function ChangelogEntryRow({ entry, isLatest }: { entry: PrototypeChangelogEntry; isLatest?: boolean }) {
function ChangelogEntryRow({
entry,
isLatest,
}: {
entry: PrototypeChangelogEntry;
isLatest?: boolean;
}) {
return (
<li className={`opn-changelog-item${isLatest ? ' opn-changelog-item--latest' : ''}`}>
<div className="opn-changelog-rail" aria-hidden>
<span className="opn-changelog-dot" />
</div>
<div className="opn-changelog-content">
<div className="opn-changelog-item-head">
<span className="opn-changelog-version">{entry.version}</span>
<time className="opn-changelog-time" dateTime={`${entry.date}T${entry.time}`}>
@@ -161,9 +320,9 @@ function ChangelogEntryRow({ entry, isLatest }: { entry: PrototypeChangelogEntry
{entry.files.length > 0 && (
<details className="opn-changelog-files">
<summary>
{entry.files.length}
<FileCode2 size={14} aria-hidden />
<span className="opn-changelog-files-count">{entry.files.length}</span>
</summary>
<ul>
{entry.files.map((file) => (
@@ -172,6 +331,7 @@ function ChangelogEntryRow({ entry, isLatest }: { entry: PrototypeChangelogEntry
</ul>
</details>
)}
</div>
</li>
);
}
@@ -186,6 +346,7 @@ function ChangelogDetailPanel({
const record = getPrototypeRegistryRecord(link.prototypeId);
const changelog = record?.changelog || [];
const Icon = iconForLink(link.title);
const panelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
@@ -194,6 +355,7 @@ function ChangelogDetailPanel({
document.addEventListener('keydown', onKeyDown);
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
window.requestAnimationFrame(() => panelRef.current?.focus());
return () => {
document.removeEventListener('keydown', onKeyDown);
document.body.style.overflow = previousOverflow;
@@ -203,27 +365,29 @@ function ChangelogDetailPanel({
return (
<div className="opn-detail-overlay" role="presentation" onClick={onClose}>
<div
ref={panelRef}
className="opn-detail-panel"
role="dialog"
aria-modal="true"
aria-labelledby="opn-detail-title"
tabIndex={-1}
onClick={(event) => event.stopPropagation()}
data-annotation-id={`opn-detail-${link.prototypeId}`}
>
<header className="opn-detail-header">
<div className="opn-detail-heading">
<span className="opn-detail-icon" aria-hidden>
<Icon size={22} />
<Icon size={20} strokeWidth={1.75} />
</span>
<div>
<p className="opn-detail-eyebrow">Prototype Changelog</p>
<h2 id="opn-detail-title">{link.title}</h2>
<p className="opn-detail-subtitle">
{record?.version || link.version || 'v1.0'}
<span className="opn-detail-version">{record?.version || link.version || 'v1.0'}</span>
{record?.lastUpdated && (
<>
{' '}
·
{' '}
<span className="opn-detail-dot" aria-hidden>·</span>
<Clock3 size={13} aria-hidden />
{new Date(record.lastUpdated).toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
@@ -235,7 +399,7 @@ function ChangelogDetailPanel({
</p>
</div>
</div>
<div className="opn-detail-actions">
<div className="opn-detail-toolbar">
<a
className="opn-detail-open-btn"
href={link.href}
@@ -243,7 +407,7 @@ function ChangelogDetailPanel({
aria-label={`查看 ${link.title} 原型(在新标签页打开)`}
>
<ExternalLink size={16} aria-hidden />
<ExternalLink size={14} strokeWidth={1.75} aria-hidden />
</a>
<button
type="button"
@@ -251,7 +415,7 @@ function ChangelogDetailPanel({
onClick={onClose}
aria-label="关闭变更日志"
>
<X size={20} aria-hidden />
<X size={18} aria-hidden />
</button>
</div>
</header>
@@ -259,17 +423,23 @@ function ChangelogDetailPanel({
<div className="opn-detail-body">
<section className="opn-detail-section" aria-labelledby="opn-changelog-title">
<header className="opn-detail-section-header">
<History size={18} aria-hidden />
<History size={16} aria-hidden />
<h3 id="opn-changelog-title"></h3>
</header>
{changelog.length > 0 ? (
<ol className="opn-changelog-list">
{changelog.map((entry, index) => (
<ChangelogEntryRow key={`${entry.version}-${entry.date}-${entry.time}`} entry={entry} isLatest={index === 0} />
<ChangelogEntryRow
key={`${entry.version}-${entry.date}-${entry.time}`}
entry={entry}
isLatest={index === 0}
/>
))}
</ol>
) : (
<p className="opn-detail-empty"></p>
<p className="opn-detail-empty">
</p>
)}
</section>
</div>
@@ -289,13 +459,15 @@ function RecentUpdatesPanel({
return (
<section className="opn-updates" data-annotation-id="opn-recent-updates" aria-labelledby="opn-updates-title">
<header className="opn-updates-header">
<div>
<p className="opn-updates-eyebrow">Activity</p>
<h2 id="opn-updates-title"></h2>
<p></p>
</div>
<p></p>
</header>
<ol className="opn-updates-list">
{updates.map((item) => (
<li key={`${item.prototypeId}-${item.version}-${item.date}-${item.time}`} className="opn-updates-item">
<div className="opn-updates-main">
<button
type="button"
className="opn-updates-link"
@@ -307,16 +479,19 @@ function RecentUpdatesPanel({
version: item.version,
})}
>
{item.title}
</button>
<span className="opn-updates-link-main">
<span className="opn-updates-link-title">{item.title}</span>
<span className="opn-updates-summary">{item.summary}</span>
</span>
<span className="opn-updates-side">
<span className="opn-updates-version">{item.version}</span>
</div>
<p className="opn-updates-summary">{item.summary}</p>
<p className="opn-updates-meta">
<span className="opn-updates-meta">
{item.date}
{' '}
{item.time}
</p>
</span>
</span>
</button>
</li>
))}
</ol>
@@ -325,6 +500,7 @@ function RecentUpdatesPanel({
}
export default function OneosPrototypeNavApp() {
const [isAuthed, setIsAuthed] = useState(readAuthSession);
const sections = useMemo(() => loadNavSections(), []);
const recentUpdates = useMemo(() => loadRecentUpdates(), []);
const registryUpdatedAt = useMemo(() => getRegistryUpdatedAt(), []);
@@ -332,6 +508,8 @@ export default function OneosPrototypeNavApp() {
const totalCount = useMemo(() => countNavLinks(sections), [sections]);
const [query, setQuery] = useState('');
const [selectedLink, setSelectedLink] = useState<NavLinkItem | null>(null);
const [activeSectionId, setActiveSectionId] = useState(() => resolveInitialSectionId(loadNavSections()));
const searchRef = useRef<HTMLInputElement>(null);
const handleSelectLink = useCallback((link: NavLinkItem) => {
setSelectedLink(link);
@@ -341,6 +519,37 @@ export default function OneosPrototypeNavApp() {
setSelectedLink(null);
}, []);
const handleSelectSection = useCallback((sectionId: string) => {
setActiveSectionId(sectionId);
window.history.replaceState(null, '', `#opn-section-${sectionId}`);
document.getElementById('opn-main')?.scrollTo({ top: 0 });
}, []);
useEffect(() => {
const onHashChange = () => {
const fromHash = sectionIdFromHash();
if (fromHash && sections.some((section) => section.id === fromHash)) {
setActiveSectionId(fromHash);
}
};
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
}, [sections]);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
const tag = (event.target as HTMLElement | null)?.tagName;
const isTyping = tag === 'INPUT' || tag === 'TEXTAREA' || (event.target as HTMLElement)?.isContentEditable;
if (isTyping || selectedLink) return;
if (event.key === '/') {
event.preventDefault();
searchRef.current?.focus();
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [selectedLink]);
const normalizedQuery = query.trim().toLowerCase();
const filteredLinks = useMemo(() => {
if (!normalizedQuery) return [];
@@ -353,7 +562,7 @@ export default function OneosPrototypeNavApp() {
}, [allLinks, normalizedQuery]);
const visibleSections = useMemo(() => {
if (!normalizedQuery) return sections;
if (normalizedQuery) {
const matchedIds = new Set(filteredLinks.map((link) => link.prototypeId));
return sections
.map((section) => {
@@ -368,38 +577,42 @@ export default function OneosPrototypeNavApp() {
return { ...section, links, subGroups };
})
.filter((section): section is NavSection => Boolean(section));
}, [sections, normalizedQuery, filteredLinks]);
}
const activeSection = sections.find((section) => section.id === activeSectionId) || sections[0];
return activeSection ? [activeSection] : [];
}, [sections, normalizedQuery, filteredLinks, activeSectionId]);
if (!isAuthed) {
return (
<div className="opn-page opn-page--gate" data-annotation-id="opn-page">
<PasswordGate onUnlock={() => setIsAuthed(true)} />
</div>
);
}
return (
<div className="opn-page" data-annotation-id="opn-page">
<a className="opn-skip" href="#opn-main"></a>
<header className="opn-hero" data-annotation-id="opn-hero">
<div className="opn-hero-inner">
<p className="opn-eyebrow">
<Compass size={16} aria-hidden />
ONE-OS ·
</p>
<h1></h1>
<p className="opn-lead">
</p>
<div className="opn-hero-meta">
<span className="opn-stat">
<Layers size={16} aria-hidden />
<header className="opn-topbar" data-annotation-id="opn-topbar">
<div className="opn-topbar-inner">
<div className="opn-brand">
<span className="opn-brand-mark" aria-hidden>
<Zap size={14} strokeWidth={2.25} />
</span>
<span className="opn-brand-text">ONE-OS · Prototype Hub</span>
</div>
<div className="opn-topbar-stats">
<span className="opn-topbar-stat">
<LayoutGrid size={14} aria-hidden />
{totalCount}
{' '}
</span>
<span className="opn-stat">
{sections.length}
{' '}
</span>
{registryUpdatedAt && (
<span className="opn-stat opn-stat--muted">
<span className="opn-topbar-stat opn-topbar-stat--muted">
{' '}
{new Date(registryUpdatedAt).toLocaleString('zh-CN', {
month: 'numeric',
@@ -410,16 +623,27 @@ export default function OneosPrototypeNavApp() {
</span>
)}
</div>
</div>
</header>
<section className="opn-hero" data-annotation-id="opn-hero">
<div className="opn-hero-inner">
<p className="opn-eyebrow">Prototype Directory</p>
<h1>
<span className="opn-hero-accent">OneOS</span>
</h1>
<label className="opn-search" data-annotation-id="opn-search">
<Search size={18} aria-hidden />
<Search size={17} aria-hidden />
<input
ref={searchRef}
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="搜索菜单或原型名称…"
aria-label="搜索原型"
/>
{!query && <kbd className="opn-search-kbd" aria-hidden>/</kbd>}
{query && (
<button
type="button"
@@ -427,29 +651,32 @@ export default function OneosPrototypeNavApp() {
onClick={() => setQuery('')}
aria-label="清除搜索"
>
<X size={16} aria-hidden />
<X size={15} aria-hidden />
</button>
)}
</label>
</div>
</header>
</section>
<div className="opn-layout">
<div className={`opn-layout${normalizedQuery ? ' opn-layout--search' : ''}`}>
{!normalizedQuery && (
<aside className="opn-sidebar" aria-label="分组目录">
<p className="opn-sidebar-title"></p>
<p className="opn-sidebar-title">Directory</p>
<nav className="opn-sidebar-nav">
{sections.map((section) => {
const Icon = section.icon;
const isActive = activeSectionId === section.id;
return (
<a
<button
key={section.id}
className="opn-sidebar-link"
href={`#opn-section-${section.id}`}
type="button"
className={`opn-sidebar-link${isActive ? ' opn-sidebar-link--active' : ''}`}
onClick={() => handleSelectSection(section.id)}
aria-current={isActive ? 'page' : undefined}
>
<Icon size={16} aria-hidden />
{section.title}
</a>
<Icon size={15} strokeWidth={1.75} aria-hidden />
<span>{sidebarSectionLabel(section)}</span>
</button>
);
})}
</nav>
@@ -461,7 +688,7 @@ export default function OneosPrototypeNavApp() {
<p className="opn-search-result" role="status">
{' '}
{filteredLinks.length}
<strong>{filteredLinks.length}</strong>
{' '}
{query.trim()}
@@ -478,7 +705,11 @@ export default function OneosPrototypeNavApp() {
</div>
) : (
visibleSections.map((section) => (
<SectionBlock key={section.id} section={section} onSelectLink={handleSelectLink} />
<SectionBlock
key={section.id}
section={section}
onSelectLink={handleSelectLink}
/>
))
)}

View File

@@ -95,7 +95,7 @@ const GROUP_ICONS: Record<string, LucideIcon> = {
const SECTION_META: Record<string, { icon: LucideIcon; description: string }> = {
OneOS: {
icon: Layers,
description: '按业务菜单分组,进入各功能模块的可交互原型。',
description: '',
},
: {
icon: Smartphone,

View File

@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-07-09T09:39:48.227Z",
"updatedAt": "2026-07-09T10:33:56.938Z",
"prototypes": {
"oneos-web-workbench": {
"title": "工作台",
@@ -975,10 +975,10 @@
},
"vehicle-h2-fee-ledger": {
"title": "车辆氢费明细",
"version": "v1.4",
"revision": 4,
"lastUpdated": "2026-07-09T09:39:46.953Z",
"contentHash": "1515eef3f81e6a77708c09e77ee238e52fd5f0a33fd2c8301050cafe089d4881",
"version": "v1.5",
"revision": 5,
"lastUpdated": "2026-07-09T10:01:49.887Z",
"contentHash": "ffe27c79a960ac86f3ecfe0ebe9d976c0a51a30c51fe2773439297e02b86dee3",
"trackedFiles": [
".spec/PRD.md",
".spec/prototype-comments.json",
@@ -987,6 +987,19 @@
"index.tsx"
],
"changelog": [
{
"version": "v1.5",
"date": "2026-07-09",
"time": "18:01",
"summary": "更新需求说明与标注",
"files": [
".spec/PRD.md",
".spec/prototype-comments.json",
"globals.ts",
"H2LedgerPage.jsx",
"index.tsx"
]
},
{
"version": "v1.4",
"date": "2026-07-09",
@@ -1221,9 +1234,80 @@
"vrsV2Data.js"
],
"changelog": []
},
"oneos-prototype-nav": {
"title": "原型导航",
"version": "v1.1",
"revision": 1,
"lastUpdated": "2026-07-09T10:33:56.745Z",
"contentHash": "564fe84bcf20fddeb53e953a70dc3eb8fe07806d767fecb8c1597c6d1ebd4b21",
"trackedFiles": [
".spec/prototype-comments.json",
"index.tsx",
"nav-data.ts",
"style.css",
"xll-nav-menu.json",
"xll-nav-source.json"
],
"changelog": [
{
"version": "v1.1",
"date": "2026-07-09",
"time": "18:31",
"summary": "VoltAgent 视觉改版、密码门禁与侧栏按业务线切换、批注体验优化",
"files": [
"index.tsx",
"style.css",
"nav-data.ts",
".spec/prototype-comments.json"
]
},
{
"version": "v1.0",
"date": "2026-07-09",
"time": "17:52",
"summary": "原型导航与版本追踪上线",
"files": [
"index.tsx",
"style.css",
"nav-data.ts",
"nav-menu.json",
"prototype-registry.json"
]
}
]
}
},
"recentUpdates": [
{
"prototypeId": "oneos-prototype-nav",
"title": "原型导航",
"version": "v1.1",
"date": "2026-07-09",
"time": "18:31",
"summary": "VoltAgent 视觉改版、密码门禁与侧栏按业务线切换、批注体验优化",
"files": [
"index.tsx",
"style.css",
"nav-data.ts",
".spec/prototype-comments.json"
]
},
{
"prototypeId": "vehicle-h2-fee-ledger",
"title": "车辆氢费明细",
"version": "v1.5",
"date": "2026-07-09",
"time": "18:01",
"summary": "更新需求说明与标注",
"files": [
".spec/PRD.md",
".spec/prototype-comments.json",
"globals.ts",
"H2LedgerPage.jsx",
"index.tsx"
]
},
{
"prototypeId": "payment-records",
"title": "收款记录",
@@ -1988,50 +2072,6 @@
"components/RiskTagDrawer.tsx",
"components/RiskTagForm.tsx"
]
},
{
"prototypeId": "vehicle-management",
"title": "车辆资产",
"version": "v1.3",
"date": "2026-07-09",
"time": "17:39",
"summary": "更新需求说明与标注",
"files": [
".spec/prototype-comments.json",
".spec/prototype-review.md",
".spec/requirements-prd-detail.md",
".spec/requirements-prd-list.md",
".spec/ui-review.md",
"annotation-source.json",
"components/DateRangeCalendarPanel.tsx",
"components/DateRangeFilterField.tsx",
"components/DetailAccidentRecordsTab.tsx",
"components/DetailAnnualReviewRecordsTab.tsx",
"components/DetailBasicInfoTab.tsx",
"components/DetailExpireField.tsx"
]
},
{
"prototypeId": "vehicle-management",
"title": "车辆资产",
"version": "v1.2",
"date": "2026-07-09",
"time": "17:38",
"summary": "更新需求说明与标注",
"files": [
".spec/prototype-comments.json",
".spec/prototype-review.md",
".spec/requirements-prd-detail.md",
".spec/requirements-prd-list.md",
".spec/ui-review.md",
"annotation-source.json",
"components/DateRangeCalendarPanel.tsx",
"components/DateRangeFilterField.tsx",
"components/DetailAccidentRecordsTab.tsx",
"components/DetailAnnualReviewRecordsTab.tsx",
"components/DetailBasicInfoTab.tsx",
"components/DetailExpireField.tsx"
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "vehicle-h2-fee-ledger",
"pageId": "list",
"updatedAt": 1783589987168,
"updatedAt": 1783588824941,
"nodes": [
{
"id": "vh2-header",
@@ -300,13 +300,6 @@
"title": "ONE-OS 原型导航",
"defaultExpanded": true,
"children": [
{
"type": "link",
"id": "nav-link-oneos-prototype-nav",
"title": "原型导航",
"href": "/prototypes/oneos-prototype-nav",
"target": "self"
},
{
"type": "folder",
"id": "nav-folder-1782874576229-r8atbu",
@@ -321,6 +314,112 @@
"target": "self"
}
]
},
{
"type": "link",
"id": "nav-link-lease-business-line-overview",
"title": "业务条线说明",
"href": "/prototypes/lease-business-line-overview",
"target": "self"
},
{
"type": "folder",
"id": "nav-folder-prototypes-oneos-web",
"title": "ONE-OS Web 端",
"defaultExpanded": true,
"children": [
{
"type": "link",
"id": "nav-link-oneos-web-login",
"title": "登录",
"href": "/prototypes/oneos-web-login",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-vehicle-asset",
"title": "车辆管理",
"href": "/prototypes/oneos-web-vehicle-asset",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-contract-template",
"title": "合同模板管理",
"href": "/prototypes/oneos-web-contract-template",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-help-center",
"title": "帮助中心",
"href": "/prototypes/oneos-web-help-center",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-finance",
"title": "财务管理",
"href": "/prototypes/oneos-web-finance",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-procurement",
"title": "采购管理",
"href": "/prototypes/oneos-web-procurement",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-lease-contract",
"title": "车辆租赁合同",
"href": "/prototypes/oneos-web-lease-contract",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-h2-station",
"title": "加氢站管理",
"href": "/prototypes/oneos-web-h2-station",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-data-analysis",
"title": "数据分析",
"href": "/prototypes/oneos-web-data-analysis",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-ledger-data",
"title": "台账数据",
"href": "/prototypes/oneos-web-ledger-data",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-business",
"title": "业务管理",
"href": "/prototypes/oneos-web-business",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-ops",
"title": "运维管理",
"href": "/prototypes/oneos-web-ops",
"target": "self"
},
{
"type": "link",
"id": "nav-link-oneos-web-requirements",
"title": "需求说明",
"href": "/prototypes/oneos-web-requirements",
"target": "self"
}
]
}
]
},