迭代 ONE-OS 原型导航:VoltAgent 视觉改版、密码门禁与侧栏按业务线切换;车辆氢费明细标注目录同步至 v1.5。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -295,7 +295,7 @@ export function syncPrototypeNavRegistry(options = {}) {
|
|||||||
|
|
||||||
let bumped = 0;
|
let bumped = 0;
|
||||||
for (const prototypeId of targetIds) {
|
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 title = titleMap.get(prototypeId) || prototypeId;
|
||||||
const prototypeDir = path.join(projectRoot, 'src/prototypes', prototypeId);
|
const prototypeDir = path.join(projectRoot, 'src/prototypes', prototypeId);
|
||||||
if (!fs.existsSync(prototypeDir)) {
|
if (!fs.existsSync(prototypeDir)) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"prototypeName": "oneos-prototype-nav",
|
"prototypeName": "oneos-prototype-nav",
|
||||||
"pageId": "nav",
|
"pageId": "nav",
|
||||||
"updatedAt": 1783589987167,
|
"updatedAt": 1783600000000,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": "opn-hero",
|
"id": "opn-hero",
|
||||||
@@ -51,37 +51,6 @@
|
|||||||
},
|
},
|
||||||
"markdownMap": {},
|
"markdownMap": {},
|
||||||
"directory": {
|
"directory": {
|
||||||
"nodes": [
|
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,24 @@
|
|||||||
* ONE-OS 原型目录导航:按菜单分组快速进入各功能原型
|
* ONE-OS 原型目录导航:按菜单分组快速进入各功能原型
|
||||||
*/
|
*/
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowUpRight,
|
||||||
Compass,
|
Clock3,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
FileCode2,
|
||||||
History,
|
History,
|
||||||
Layers,
|
LayoutGrid,
|
||||||
|
Lock,
|
||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
countNavLinks,
|
countNavLinks,
|
||||||
@@ -29,13 +38,49 @@ import {
|
|||||||
type RecentUpdateEntry,
|
type RecentUpdateEntry,
|
||||||
} from './nav-data';
|
} from './nav-data';
|
||||||
|
|
||||||
function linkMeta(link: NavLinkItem, record: PrototypeRegistryRecord | null): string | undefined {
|
function formatRecordUpdatedAt(record: PrototypeRegistryRecord | null): string | undefined {
|
||||||
const parts: string[] = [];
|
if (record?.lastUpdated) {
|
||||||
if (link.version || record?.version) parts.push(link.version || record?.version || '');
|
const date = new Date(record.lastUpdated);
|
||||||
if (link.lastUpdatedLabel) parts.push(`${link.lastUpdatedLabel}更新`);
|
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];
|
const latest = record?.changelog?.[0];
|
||||||
if (latest?.summary && parts.length < 2) parts.push(latest.summary);
|
if (latest) {
|
||||||
return parts.filter(Boolean).join(' · ') || undefined;
|
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 = {
|
const NEW_TAB_LINK_PROPS = {
|
||||||
@@ -43,6 +88,99 @@ const NEW_TAB_LINK_PROPS = {
|
|||||||
rel: 'noopener noreferrer',
|
rel: 'noopener noreferrer',
|
||||||
} as const;
|
} 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({
|
function NavLinkCard({
|
||||||
link,
|
link,
|
||||||
meta,
|
meta,
|
||||||
@@ -54,7 +192,9 @@ function NavLinkCard({
|
|||||||
}) {
|
}) {
|
||||||
const Icon = iconForLink(link.title);
|
const Icon = iconForLink(link.title);
|
||||||
const record = getPrototypeRegistryRecord(link.prototypeId);
|
const record = getPrototypeRegistryRecord(link.prototypeId);
|
||||||
const metaText = meta ?? linkMeta(link, record);
|
const metaText = meta ?? linkMeta(record);
|
||||||
|
const version = link.version || record?.version;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -64,13 +204,16 @@ function NavLinkCard({
|
|||||||
data-annotation-id={`opn-link-${link.prototypeId}-${link.id}`}
|
data-annotation-id={`opn-link-${link.prototypeId}-${link.id}`}
|
||||||
>
|
>
|
||||||
<span className="opn-link-card-icon" aria-hidden>
|
<span className="opn-link-card-icon" aria-hidden>
|
||||||
<Icon size={20} />
|
<Icon size={18} strokeWidth={1.75} />
|
||||||
</span>
|
</span>
|
||||||
<span className="opn-link-card-body">
|
<span className="opn-link-card-body">
|
||||||
<span className="opn-link-card-title">{link.title}</span>
|
<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>}
|
{metaText && <span className="opn-link-card-meta">{metaText}</span>}
|
||||||
</span>
|
</span>
|
||||||
<ArrowRight size={18} className="opn-link-card-arrow" aria-hidden />
|
<ArrowUpRight size={16} className="opn-link-card-arrow" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,10 +230,10 @@ function SubGroupPanel({
|
|||||||
<div className="opn-subgroup" data-annotation-id={`opn-group-${group.id}`}>
|
<div className="opn-subgroup" data-annotation-id={`opn-group-${group.id}`}>
|
||||||
<header className="opn-subgroup-header">
|
<header className="opn-subgroup-header">
|
||||||
<span className="opn-subgroup-icon" aria-hidden>
|
<span className="opn-subgroup-icon" aria-hidden>
|
||||||
<Icon size={18} />
|
<Icon size={16} strokeWidth={1.75} />
|
||||||
</span>
|
</span>
|
||||||
<h3>{group.title}</h3>
|
<h3>{group.title}</h3>
|
||||||
<span className="opn-subgroup-count">{group.links.length} 个原型</span>
|
<span className="opn-subgroup-count">共{group.links.length}个原型</span>
|
||||||
</header>
|
</header>
|
||||||
<div className="opn-link-grid">
|
<div className="opn-link-grid">
|
||||||
{group.links.map((link) => (
|
{group.links.map((link) => (
|
||||||
@@ -109,6 +252,9 @@ function SectionBlock({
|
|||||||
onSelectLink: (link: NavLinkItem) => void;
|
onSelectLink: (link: NavLinkItem) => void;
|
||||||
}) {
|
}) {
|
||||||
const Icon = section.icon;
|
const Icon = section.icon;
|
||||||
|
const linkCount = section.links.length
|
||||||
|
+ section.subGroups.reduce((sum, group) => sum + group.links.length, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="opn-section"
|
className="opn-section"
|
||||||
@@ -118,11 +264,14 @@ function SectionBlock({
|
|||||||
<header className="opn-section-header">
|
<header className="opn-section-header">
|
||||||
<div className="opn-section-heading">
|
<div className="opn-section-heading">
|
||||||
<span className="opn-section-icon" aria-hidden>
|
<span className="opn-section-icon" aria-hidden>
|
||||||
<Icon size={22} />
|
<Icon size={20} strokeWidth={1.75} />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<h2>{section.title}</h2>
|
<div className="opn-section-title-row">
|
||||||
<p>{section.description}</p>
|
<h2>{section.title}</h2>
|
||||||
|
<span className="opn-section-badge">{linkCount}个原型页</span>
|
||||||
|
</div>
|
||||||
|
{section.description && <p>{section.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -146,32 +295,43 @@ function SectionBlock({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChangelogEntryRow({ entry, isLatest }: { entry: PrototypeChangelogEntry; isLatest?: boolean }) {
|
function ChangelogEntryRow({
|
||||||
|
entry,
|
||||||
|
isLatest,
|
||||||
|
}: {
|
||||||
|
entry: PrototypeChangelogEntry;
|
||||||
|
isLatest?: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<li className={`opn-changelog-item${isLatest ? ' opn-changelog-item--latest' : ''}`}>
|
<li className={`opn-changelog-item${isLatest ? ' opn-changelog-item--latest' : ''}`}>
|
||||||
<div className="opn-changelog-item-head">
|
<div className="opn-changelog-rail" aria-hidden>
|
||||||
<span className="opn-changelog-version">{entry.version}</span>
|
<span className="opn-changelog-dot" />
|
||||||
<time className="opn-changelog-time" dateTime={`${entry.date}T${entry.time}`}>
|
</div>
|
||||||
{entry.date}
|
<div className="opn-changelog-content">
|
||||||
{' '}
|
<div className="opn-changelog-item-head">
|
||||||
{entry.time}
|
<span className="opn-changelog-version">{entry.version}</span>
|
||||||
</time>
|
<time className="opn-changelog-time" dateTime={`${entry.date}T${entry.time}`}>
|
||||||
|
{entry.date}
|
||||||
|
{' '}
|
||||||
|
{entry.time}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
<p className="opn-changelog-summary">{entry.summary}</p>
|
||||||
|
{entry.files.length > 0 && (
|
||||||
|
<details className="opn-changelog-files">
|
||||||
|
<summary>
|
||||||
|
<FileCode2 size={14} aria-hidden />
|
||||||
|
变更文件
|
||||||
|
<span className="opn-changelog-files-count">{entry.files.length}</span>
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
{entry.files.map((file) => (
|
||||||
|
<li key={file}>{file}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="opn-changelog-summary">{entry.summary}</p>
|
|
||||||
{entry.files.length > 0 && (
|
|
||||||
<details className="opn-changelog-files">
|
|
||||||
<summary>
|
|
||||||
变更文件(
|
|
||||||
{entry.files.length}
|
|
||||||
)
|
|
||||||
</summary>
|
|
||||||
<ul>
|
|
||||||
{entry.files.map((file) => (
|
|
||||||
<li key={file}>{file}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -186,6 +346,7 @@ function ChangelogDetailPanel({
|
|||||||
const record = getPrototypeRegistryRecord(link.prototypeId);
|
const record = getPrototypeRegistryRecord(link.prototypeId);
|
||||||
const changelog = record?.changelog || [];
|
const changelog = record?.changelog || [];
|
||||||
const Icon = iconForLink(link.title);
|
const Icon = iconForLink(link.title);
|
||||||
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
@@ -194,6 +355,7 @@ function ChangelogDetailPanel({
|
|||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
const previousOverflow = document.body.style.overflow;
|
const previousOverflow = document.body.style.overflow;
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
window.requestAnimationFrame(() => panelRef.current?.focus());
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
document.body.style.overflow = previousOverflow;
|
document.body.style.overflow = previousOverflow;
|
||||||
@@ -203,27 +365,29 @@ function ChangelogDetailPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="opn-detail-overlay" role="presentation" onClick={onClose}>
|
<div className="opn-detail-overlay" role="presentation" onClick={onClose}>
|
||||||
<div
|
<div
|
||||||
|
ref={panelRef}
|
||||||
className="opn-detail-panel"
|
className="opn-detail-panel"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="opn-detail-title"
|
aria-labelledby="opn-detail-title"
|
||||||
|
tabIndex={-1}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
data-annotation-id={`opn-detail-${link.prototypeId}`}
|
data-annotation-id={`opn-detail-${link.prototypeId}`}
|
||||||
>
|
>
|
||||||
<header className="opn-detail-header">
|
<header className="opn-detail-header">
|
||||||
<div className="opn-detail-heading">
|
<div className="opn-detail-heading">
|
||||||
<span className="opn-detail-icon" aria-hidden>
|
<span className="opn-detail-icon" aria-hidden>
|
||||||
<Icon size={22} />
|
<Icon size={20} strokeWidth={1.75} />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
|
<p className="opn-detail-eyebrow">Prototype Changelog</p>
|
||||||
<h2 id="opn-detail-title">{link.title}</h2>
|
<h2 id="opn-detail-title">{link.title}</h2>
|
||||||
<p className="opn-detail-subtitle">
|
<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 && (
|
{record?.lastUpdated && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
<span className="opn-detail-dot" aria-hidden>·</span>
|
||||||
· 最近更新
|
<Clock3 size={13} aria-hidden />
|
||||||
{' '}
|
|
||||||
{new Date(record.lastUpdated).toLocaleString('zh-CN', {
|
{new Date(record.lastUpdated).toLocaleString('zh-CN', {
|
||||||
month: 'numeric',
|
month: 'numeric',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -235,7 +399,7 @@ function ChangelogDetailPanel({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="opn-detail-actions">
|
<div className="opn-detail-toolbar">
|
||||||
<a
|
<a
|
||||||
className="opn-detail-open-btn"
|
className="opn-detail-open-btn"
|
||||||
href={link.href}
|
href={link.href}
|
||||||
@@ -243,7 +407,7 @@ function ChangelogDetailPanel({
|
|||||||
aria-label={`查看 ${link.title} 原型(在新标签页打开)`}
|
aria-label={`查看 ${link.title} 原型(在新标签页打开)`}
|
||||||
>
|
>
|
||||||
查看原型
|
查看原型
|
||||||
<ExternalLink size={16} aria-hidden />
|
<ExternalLink size={14} strokeWidth={1.75} aria-hidden />
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -251,7 +415,7 @@ function ChangelogDetailPanel({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="关闭变更日志"
|
aria-label="关闭变更日志"
|
||||||
>
|
>
|
||||||
<X size={20} aria-hidden />
|
<X size={18} aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -259,17 +423,23 @@ function ChangelogDetailPanel({
|
|||||||
<div className="opn-detail-body">
|
<div className="opn-detail-body">
|
||||||
<section className="opn-detail-section" aria-labelledby="opn-changelog-title">
|
<section className="opn-detail-section" aria-labelledby="opn-changelog-title">
|
||||||
<header className="opn-detail-section-header">
|
<header className="opn-detail-section-header">
|
||||||
<History size={18} aria-hidden />
|
<History size={16} aria-hidden />
|
||||||
<h3 id="opn-changelog-title">变更日志与历史记录</h3>
|
<h3 id="opn-changelog-title">变更日志与历史记录</h3>
|
||||||
</header>
|
</header>
|
||||||
{changelog.length > 0 ? (
|
{changelog.length > 0 ? (
|
||||||
<ol className="opn-changelog-list">
|
<ol className="opn-changelog-list">
|
||||||
{changelog.map((entry, index) => (
|
{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>
|
</ol>
|
||||||
) : (
|
) : (
|
||||||
<p className="opn-detail-empty">暂无自动记录的变更日志。原型更新并保存后,将在此展示版本说明与历史记录。</p>
|
<p className="opn-detail-empty">
|
||||||
|
暂无自动记录的变更日志。原型更新并保存后,将在此展示版本说明与历史记录。
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,34 +459,39 @@ function RecentUpdatesPanel({
|
|||||||
return (
|
return (
|
||||||
<section className="opn-updates" data-annotation-id="opn-recent-updates" aria-labelledby="opn-updates-title">
|
<section className="opn-updates" data-annotation-id="opn-recent-updates" aria-labelledby="opn-updates-title">
|
||||||
<header className="opn-updates-header">
|
<header className="opn-updates-header">
|
||||||
<h2 id="opn-updates-title">最近更新</h2>
|
<div>
|
||||||
<p>点击条目查看变更说明与历史记录;右上角可在新标签页打开原型</p>
|
<p className="opn-updates-eyebrow">Activity</p>
|
||||||
|
<h2 id="opn-updates-title">最近更新</h2>
|
||||||
|
</div>
|
||||||
|
<p>点击条目查看完整变更说明;右上角可在新标签页打开原型</p>
|
||||||
</header>
|
</header>
|
||||||
<ol className="opn-updates-list">
|
<ol className="opn-updates-list">
|
||||||
{updates.map((item) => (
|
{updates.map((item) => (
|
||||||
<li key={`${item.prototypeId}-${item.version}-${item.date}-${item.time}`} className="opn-updates-item">
|
<li key={`${item.prototypeId}-${item.version}-${item.date}-${item.time}`} className="opn-updates-item">
|
||||||
<div className="opn-updates-main">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="opn-updates-link"
|
||||||
className="opn-updates-link"
|
onClick={() => onSelectLink({
|
||||||
onClick={() => onSelectLink({
|
id: item.prototypeId,
|
||||||
id: item.prototypeId,
|
title: item.title,
|
||||||
title: item.title,
|
href: `/prototypes/${item.prototypeId}`,
|
||||||
href: `/prototypes/${item.prototypeId}`,
|
prototypeId: item.prototypeId,
|
||||||
prototypeId: item.prototypeId,
|
version: item.version,
|
||||||
version: item.version,
|
})}
|
||||||
})}
|
>
|
||||||
>
|
<span className="opn-updates-link-main">
|
||||||
{item.title}
|
<span className="opn-updates-link-title">{item.title}</span>
|
||||||
</button>
|
<span className="opn-updates-summary">{item.summary}</span>
|
||||||
<span className="opn-updates-version">{item.version}</span>
|
</span>
|
||||||
</div>
|
<span className="opn-updates-side">
|
||||||
<p className="opn-updates-summary">{item.summary}</p>
|
<span className="opn-updates-version">{item.version}</span>
|
||||||
<p className="opn-updates-meta">
|
<span className="opn-updates-meta">
|
||||||
{item.date}
|
{item.date}
|
||||||
{' '}
|
{' '}
|
||||||
{item.time}
|
{item.time}
|
||||||
</p>
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -325,6 +500,7 @@ function RecentUpdatesPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OneosPrototypeNavApp() {
|
export default function OneosPrototypeNavApp() {
|
||||||
|
const [isAuthed, setIsAuthed] = useState(readAuthSession);
|
||||||
const sections = useMemo(() => loadNavSections(), []);
|
const sections = useMemo(() => loadNavSections(), []);
|
||||||
const recentUpdates = useMemo(() => loadRecentUpdates(), []);
|
const recentUpdates = useMemo(() => loadRecentUpdates(), []);
|
||||||
const registryUpdatedAt = useMemo(() => getRegistryUpdatedAt(), []);
|
const registryUpdatedAt = useMemo(() => getRegistryUpdatedAt(), []);
|
||||||
@@ -332,6 +508,8 @@ export default function OneosPrototypeNavApp() {
|
|||||||
const totalCount = useMemo(() => countNavLinks(sections), [sections]);
|
const totalCount = useMemo(() => countNavLinks(sections), [sections]);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [selectedLink, setSelectedLink] = useState<NavLinkItem | null>(null);
|
const [selectedLink, setSelectedLink] = useState<NavLinkItem | null>(null);
|
||||||
|
const [activeSectionId, setActiveSectionId] = useState(() => resolveInitialSectionId(loadNavSections()));
|
||||||
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleSelectLink = useCallback((link: NavLinkItem) => {
|
const handleSelectLink = useCallback((link: NavLinkItem) => {
|
||||||
setSelectedLink(link);
|
setSelectedLink(link);
|
||||||
@@ -341,6 +519,37 @@ export default function OneosPrototypeNavApp() {
|
|||||||
setSelectedLink(null);
|
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 normalizedQuery = query.trim().toLowerCase();
|
||||||
const filteredLinks = useMemo(() => {
|
const filteredLinks = useMemo(() => {
|
||||||
if (!normalizedQuery) return [];
|
if (!normalizedQuery) return [];
|
||||||
@@ -353,53 +562,57 @@ export default function OneosPrototypeNavApp() {
|
|||||||
}, [allLinks, normalizedQuery]);
|
}, [allLinks, normalizedQuery]);
|
||||||
|
|
||||||
const visibleSections = useMemo(() => {
|
const visibleSections = useMemo(() => {
|
||||||
if (!normalizedQuery) return sections;
|
if (normalizedQuery) {
|
||||||
const matchedIds = new Set(filteredLinks.map((link) => link.prototypeId));
|
const matchedIds = new Set(filteredLinks.map((link) => link.prototypeId));
|
||||||
return sections
|
return sections
|
||||||
.map((section) => {
|
.map((section) => {
|
||||||
const links = section.links.filter((link) => matchedIds.has(link.prototypeId));
|
const links = section.links.filter((link) => matchedIds.has(link.prototypeId));
|
||||||
const subGroups = section.subGroups
|
const subGroups = section.subGroups
|
||||||
.map((group) => ({
|
.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
links: group.links.filter((link) => matchedIds.has(link.prototypeId)),
|
links: group.links.filter((link) => matchedIds.has(link.prototypeId)),
|
||||||
}))
|
}))
|
||||||
.filter((group) => group.links.length > 0);
|
.filter((group) => group.links.length > 0);
|
||||||
if (!links.length && !subGroups.length) return null;
|
if (!links.length && !subGroups.length) return null;
|
||||||
return { ...section, links, subGroups };
|
return { ...section, links, subGroups };
|
||||||
})
|
})
|
||||||
.filter((section): section is NavSection => Boolean(section));
|
.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 (
|
return (
|
||||||
<div className="opn-page" data-annotation-id="opn-page">
|
<div className="opn-page" data-annotation-id="opn-page">
|
||||||
<a className="opn-skip" href="#opn-main">跳到导航内容</a>
|
<a className="opn-skip" href="#opn-main">跳到导航内容</a>
|
||||||
|
|
||||||
<header className="opn-hero" data-annotation-id="opn-hero">
|
<header className="opn-topbar" data-annotation-id="opn-topbar">
|
||||||
<div className="opn-hero-inner">
|
<div className="opn-topbar-inner">
|
||||||
<p className="opn-eyebrow">
|
<div className="opn-brand">
|
||||||
<Compass size={16} aria-hidden />
|
<span className="opn-brand-mark" aria-hidden>
|
||||||
ONE-OS · 原型中心
|
<Zap size={14} strokeWidth={2.25} />
|
||||||
</p>
|
</span>
|
||||||
<h1>原型导航</h1>
|
<span className="opn-brand-text">ONE-OS · Prototype Hub</span>
|
||||||
<p className="opn-lead">
|
</div>
|
||||||
按系统菜单分组浏览全部可交互原型;点击卡片先查看变更说明,再决定是否打开原型页面。
|
<div className="opn-topbar-stats">
|
||||||
</p>
|
<span className="opn-topbar-stat">
|
||||||
|
<LayoutGrid size={14} aria-hidden />
|
||||||
<div className="opn-hero-meta">
|
|
||||||
<span className="opn-stat">
|
|
||||||
<Layers size={16} aria-hidden />
|
|
||||||
{totalCount}
|
{totalCount}
|
||||||
{' '}
|
{' '}
|
||||||
个原型入口
|
原型
|
||||||
</span>
|
|
||||||
<span className="opn-stat">
|
|
||||||
{sections.length}
|
|
||||||
{' '}
|
|
||||||
个一级分组
|
|
||||||
</span>
|
</span>
|
||||||
{registryUpdatedAt && (
|
{registryUpdatedAt && (
|
||||||
<span className="opn-stat opn-stat--muted">
|
<span className="opn-topbar-stat opn-topbar-stat--muted">
|
||||||
注册表
|
同步
|
||||||
{' '}
|
{' '}
|
||||||
{new Date(registryUpdatedAt).toLocaleString('zh-CN', {
|
{new Date(registryUpdatedAt).toLocaleString('zh-CN', {
|
||||||
month: 'numeric',
|
month: 'numeric',
|
||||||
@@ -410,16 +623,27 @@ export default function OneosPrototypeNavApp() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<label className="opn-search" data-annotation-id="opn-search">
|
||||||
<Search size={18} aria-hidden />
|
<Search size={17} aria-hidden />
|
||||||
<input
|
<input
|
||||||
|
ref={searchRef}
|
||||||
type="search"
|
type="search"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
placeholder="搜索菜单或原型名称…"
|
placeholder="搜索菜单或原型名称…"
|
||||||
aria-label="搜索原型"
|
aria-label="搜索原型"
|
||||||
/>
|
/>
|
||||||
|
{!query && <kbd className="opn-search-kbd" aria-hidden>/</kbd>}
|
||||||
{query && (
|
{query && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -427,29 +651,32 @@ export default function OneosPrototypeNavApp() {
|
|||||||
onClick={() => setQuery('')}
|
onClick={() => setQuery('')}
|
||||||
aria-label="清除搜索"
|
aria-label="清除搜索"
|
||||||
>
|
>
|
||||||
<X size={16} aria-hidden />
|
<X size={15} aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</section>
|
||||||
|
|
||||||
<div className="opn-layout">
|
<div className={`opn-layout${normalizedQuery ? ' opn-layout--search' : ''}`}>
|
||||||
{!normalizedQuery && (
|
{!normalizedQuery && (
|
||||||
<aside className="opn-sidebar" aria-label="分组目录">
|
<aside className="opn-sidebar" aria-label="分组目录">
|
||||||
<p className="opn-sidebar-title">快速跳转</p>
|
<p className="opn-sidebar-title">Directory</p>
|
||||||
<nav className="opn-sidebar-nav">
|
<nav className="opn-sidebar-nav">
|
||||||
{sections.map((section) => {
|
{sections.map((section) => {
|
||||||
const Icon = section.icon;
|
const Icon = section.icon;
|
||||||
|
const isActive = activeSectionId === section.id;
|
||||||
return (
|
return (
|
||||||
<a
|
<button
|
||||||
key={section.id}
|
key={section.id}
|
||||||
className="opn-sidebar-link"
|
type="button"
|
||||||
href={`#opn-section-${section.id}`}
|
className={`opn-sidebar-link${isActive ? ' opn-sidebar-link--active' : ''}`}
|
||||||
|
onClick={() => handleSelectSection(section.id)}
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
<Icon size={16} aria-hidden />
|
<Icon size={15} strokeWidth={1.75} aria-hidden />
|
||||||
{section.title}
|
<span>{sidebarSectionLabel(section)}</span>
|
||||||
</a>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -461,7 +688,7 @@ export default function OneosPrototypeNavApp() {
|
|||||||
<p className="opn-search-result" role="status">
|
<p className="opn-search-result" role="status">
|
||||||
找到
|
找到
|
||||||
{' '}
|
{' '}
|
||||||
{filteredLinks.length}
|
<strong>{filteredLinks.length}</strong>
|
||||||
{' '}
|
{' '}
|
||||||
个匹配「
|
个匹配「
|
||||||
{query.trim()}
|
{query.trim()}
|
||||||
@@ -478,7 +705,11 @@ export default function OneosPrototypeNavApp() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
visibleSections.map((section) => (
|
visibleSections.map((section) => (
|
||||||
<SectionBlock key={section.id} section={section} onSelectLink={handleSelectLink} />
|
<SectionBlock
|
||||||
|
key={section.id}
|
||||||
|
section={section}
|
||||||
|
onSelectLink={handleSelectLink}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const GROUP_ICONS: Record<string, LucideIcon> = {
|
|||||||
const SECTION_META: Record<string, { icon: LucideIcon; description: string }> = {
|
const SECTION_META: Record<string, { icon: LucideIcon; description: string }> = {
|
||||||
OneOS: {
|
OneOS: {
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
description: '按业务菜单分组,进入各功能模块的可交互原型。',
|
description: '',
|
||||||
},
|
},
|
||||||
小羚羚: {
|
小羚羚: {
|
||||||
icon: Smartphone,
|
icon: Smartphone,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-07-09T09:39:48.227Z",
|
"updatedAt": "2026-07-09T10:33:56.938Z",
|
||||||
"prototypes": {
|
"prototypes": {
|
||||||
"oneos-web-workbench": {
|
"oneos-web-workbench": {
|
||||||
"title": "工作台",
|
"title": "工作台",
|
||||||
@@ -975,10 +975,10 @@
|
|||||||
},
|
},
|
||||||
"vehicle-h2-fee-ledger": {
|
"vehicle-h2-fee-ledger": {
|
||||||
"title": "车辆氢费明细",
|
"title": "车辆氢费明细",
|
||||||
"version": "v1.4",
|
"version": "v1.5",
|
||||||
"revision": 4,
|
"revision": 5,
|
||||||
"lastUpdated": "2026-07-09T09:39:46.953Z",
|
"lastUpdated": "2026-07-09T10:01:49.887Z",
|
||||||
"contentHash": "1515eef3f81e6a77708c09e77ee238e52fd5f0a33fd2c8301050cafe089d4881",
|
"contentHash": "ffe27c79a960ac86f3ecfe0ebe9d976c0a51a30c51fe2773439297e02b86dee3",
|
||||||
"trackedFiles": [
|
"trackedFiles": [
|
||||||
".spec/PRD.md",
|
".spec/PRD.md",
|
||||||
".spec/prototype-comments.json",
|
".spec/prototype-comments.json",
|
||||||
@@ -987,6 +987,19 @@
|
|||||||
"index.tsx"
|
"index.tsx"
|
||||||
],
|
],
|
||||||
"changelog": [
|
"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",
|
"version": "v1.4",
|
||||||
"date": "2026-07-09",
|
"date": "2026-07-09",
|
||||||
@@ -1221,9 +1234,80 @@
|
|||||||
"vrsV2Data.js"
|
"vrsV2Data.js"
|
||||||
],
|
],
|
||||||
"changelog": []
|
"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": [
|
"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",
|
"prototypeId": "payment-records",
|
||||||
"title": "收款记录",
|
"title": "收款记录",
|
||||||
@@ -1988,50 +2072,6 @@
|
|||||||
"components/RiskTagDrawer.tsx",
|
"components/RiskTagDrawer.tsx",
|
||||||
"components/RiskTagForm.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
@@ -5,7 +5,7 @@
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"prototypeName": "vehicle-h2-fee-ledger",
|
"prototypeName": "vehicle-h2-fee-ledger",
|
||||||
"pageId": "list",
|
"pageId": "list",
|
||||||
"updatedAt": 1783589987168,
|
"updatedAt": 1783588824941,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": "vh2-header",
|
"id": "vh2-header",
|
||||||
@@ -300,13 +300,6 @@
|
|||||||
"title": "ONE-OS 原型导航",
|
"title": "ONE-OS 原型导航",
|
||||||
"defaultExpanded": true,
|
"defaultExpanded": true,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "link",
|
|
||||||
"id": "nav-link-oneos-prototype-nav",
|
|
||||||
"title": "原型导航",
|
|
||||||
"href": "/prototypes/oneos-prototype-nav",
|
|
||||||
"target": "self"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "folder",
|
"type": "folder",
|
||||||
"id": "nav-folder-1782874576229-r8atbu",
|
"id": "nav-folder-1782874576229-r8atbu",
|
||||||
@@ -321,6 +314,112 @@
|
|||||||
"target": "self"
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user