Sync OneOS workspace with new prototypes, annotations, and Gitea remote fix.
Add vehicle-h2-fee-ledger, customer-management, lease and self-operated ledgers, annotation sources, agent skills, and vite annotation runtime support. Update vehicle management, contract templates, and lease contract flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
65
.agents/skills/axhub-annotation-standalone/SKILL.md
Normal file
65
.agents/skills/axhub-annotation-standalone/SKILL.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: axhub-annotation-standalone
|
||||
description: Use when adding @axhub/annotation to standalone React apps, plain HTML pages, Vite prototypes, or other web hosts that need annotation markers, directories, Markdown notes, or state controls.
|
||||
---
|
||||
|
||||
# Axhub Annotation Standalone
|
||||
|
||||
在独立 Web 项目中使用 `@axhub/annotation` 时,用这个技能。它只说明运行时接入:页面已有标注数据,只需要展示 marker、标注面板、目录和状态控件。
|
||||
|
||||
## 参考案例
|
||||
|
||||
| 宿主 | 使用方式 | 文件 |
|
||||
| --- | --- | --- |
|
||||
| React | `AnnotationViewer` 组件 | `references/react-example.tsx` |
|
||||
| 普通 HTML / DOM | `createAnnotationViewer` | `references/html-example.html` + `references/html-example.ts` |
|
||||
| 数据源 | `AnnotationSourceDocument` JSON | `references/annotation-source.json` |
|
||||
|
||||
## 接入前提
|
||||
|
||||
- 安装 `@axhub/annotation`,并确保项目里有 React 18 / ReactDOM 18。
|
||||
- 使用能导入 ESM/TS/JSON 的构建工具,例如 Vite。
|
||||
- 标注数据使用一份 `AnnotationSourceDocument`,静态 import 或由宿主 loader 返回。
|
||||
- 被标注元素优先加稳定属性,例如 `data-annotation-id`。
|
||||
|
||||
## React 接入
|
||||
|
||||
- 挂载 `AnnotationViewer`。
|
||||
- 多页面时传 `options.currentPageId`。
|
||||
- 目录 `route` 在 `options.onDirectoryRoute` 中交给宿主切页。
|
||||
- 状态标注用 `useProtoDevState()` 读取 `controls` 值。
|
||||
- 发布为带源码 HTML 后,发布产物会注入 `sourceReference`,指向 `source/manifest.json`;宿主接入代码不需要生成它。
|
||||
|
||||
## 普通 HTML 接入
|
||||
|
||||
- 用 `createAnnotationViewer()` 创建运行时。
|
||||
- 用 `getCurrentPageId` 返回当前页面。
|
||||
- 切页后调用 `viewer.refresh()`。
|
||||
- 状态控件可订阅 `window.__AXHUB_PROTO_DEV__`,从 `getState()` 读值并更新 DOM。
|
||||
|
||||
## 数据要点
|
||||
|
||||
- `directory.nodes` 放 `folder` / `route` / `markdown` / `link`,不需要 `locator`。
|
||||
- `type: "markdown"` 目录文档可以直接写 `markdown` 正文。
|
||||
- 需要把目录文档拆成 `.md` 文件时,可以写 `markdownPath`,例如 `docs/prd-03-status.md`;这是构建侧约定,运行时仍读取内联后的 `markdown`。
|
||||
- `markdownPath` 只用于目录文档,不用于 marker 标注节点。
|
||||
- `data.nodes[]` 放页面 marker,必须有能在宿主页面解析到的 `locator`。
|
||||
- marker 只属于某些页面或状态时,写 `pageId`。
|
||||
- 长正文用 `hasMarkdown: true` + `markdownMap[node.id]`。
|
||||
- 状态标注写节点 `controls`;JSON 里只放可序列化字段。
|
||||
- `sourceReference` 不放在 JSON 数据源里,只描述发布包中的源码清单位置,不内联源码文件。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 启动宿主预览。
|
||||
2. 确认目标元素上出现 marker。
|
||||
3. 点击 marker,能看到短标注或 Markdown 正文。
|
||||
4. 打开目录,验证 `route`、`markdown`、`link`。
|
||||
5. 修改状态控件,确认 React 状态或普通 DOM 同步变化。
|
||||
6. 检查控制台是否有 import、peer dependency 或 locator 错误。
|
||||
|
||||
## 常见错误
|
||||
|
||||
- 不要把函数写进 JSON controls。
|
||||
- 不要依赖脆弱的生成 CSS 选择器;能加 `data-annotation-id` 就加。
|
||||
- 不要期待 `route` 自动跳转;宿主必须在 `onDirectoryRoute` 里处理。
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"documentVersion": 1,
|
||||
"format": "axhub-annotation-source",
|
||||
"data": {
|
||||
"version": 2,
|
||||
"prototypeName": "standalone-annotation-demo",
|
||||
"pageId": "overview",
|
||||
"updatedAt": 1779667200000,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "overview-hero",
|
||||
"index": 1,
|
||||
"title": "运行时总览",
|
||||
"pageId": "overview",
|
||||
"locator": {
|
||||
"selectors": ["[data-annotation-id=\"overview-hero\"]"],
|
||||
"fingerprint": "section|overview-hero",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "说明独立页面如何接入标注运行时。",
|
||||
"annotationText": "",
|
||||
"hasMarkdown": true,
|
||||
"color": "#D97706",
|
||||
"images": [],
|
||||
"createdAt": 1779667200000,
|
||||
"updatedAt": 1779667200000
|
||||
},
|
||||
{
|
||||
"id": "state-card",
|
||||
"index": 2,
|
||||
"title": "结果状态",
|
||||
"pageId": "states",
|
||||
"locator": {
|
||||
"selectors": ["[data-annotation-id=\"state-card\"]"],
|
||||
"fingerprint": "article|state-card",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "演示标注 controls 如何驱动页面状态。",
|
||||
"annotationText": "在标注面板里切换结果状态,页面应该同步展示成功或失败。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#059669",
|
||||
"images": [],
|
||||
"controls": [
|
||||
{
|
||||
"type": "segmented",
|
||||
"attributeId": "result_state",
|
||||
"displayName": "结果状态",
|
||||
"initialValue": "success",
|
||||
"options": [
|
||||
{ "label": "成功", "value": "success" },
|
||||
{ "label": "失败", "value": "failure" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1779667200000,
|
||||
"updatedAt": 1779667200000
|
||||
}
|
||||
]
|
||||
},
|
||||
"markdownMap": {
|
||||
"overview-hero": "# 独立接入说明\n\n`@axhub/annotation` 只负责运行时展示。宿主页面负责提供数据源、稳定选择器和目录 route 行为。"
|
||||
},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
"nodes": [
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "demo-root",
|
||||
"title": "示例目录",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "route",
|
||||
"id": "route-overview",
|
||||
"title": "运行时总览",
|
||||
"route": "overview"
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"id": "route-states",
|
||||
"title": "状态标注",
|
||||
"route": "states"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "doc-usage",
|
||||
"title": "接入说明",
|
||||
"markdown": "# 接入说明\n\nReact 使用 `AnnotationViewer`;普通 HTML 使用 `createAnnotationViewer`。"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"id": "docs-link",
|
||||
"title": "包文档",
|
||||
"href": "https://www.npmjs.com/package/@axhub/annotation",
|
||||
"target": "blank"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Axhub Annotation HTML Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<button type="button" data-route="overview">运行时总览</button>
|
||||
<button type="button" data-route="states">状态标注</button>
|
||||
</nav>
|
||||
|
||||
<section data-page="overview">
|
||||
<section data-annotation-id="overview-hero">
|
||||
<h1>@axhub/annotation</h1>
|
||||
<p>这是一个普通 HTML 宿主接入示例。</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section data-page="states" hidden>
|
||||
<article data-annotation-id="state-card">
|
||||
<strong data-result-label>成功</strong>
|
||||
<h2 data-result-title>发布完成</h2>
|
||||
<p>在标注面板里切换结果状态,页面会同步变化。</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<script type="module" src="./html-example.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
createAnnotationViewer,
|
||||
type AnnotationDirectoryRouteNode,
|
||||
type AnnotationSourceDocument,
|
||||
type ProtoDevState,
|
||||
} from '@axhub/annotation';
|
||||
import annotationSource from './annotation-source.json';
|
||||
|
||||
type PageId = 'overview' | 'states';
|
||||
|
||||
let currentPageId: PageId = 'overview';
|
||||
|
||||
function normalizePageId(value: unknown): PageId {
|
||||
return value === 'states' ? 'states' : 'overview';
|
||||
}
|
||||
|
||||
function renderPage(pageId: PageId): void {
|
||||
currentPageId = pageId;
|
||||
document.querySelectorAll<HTMLElement>('[data-page]').forEach((page) => {
|
||||
page.hidden = page.dataset.page !== pageId;
|
||||
});
|
||||
}
|
||||
|
||||
function renderState(state: ProtoDevState): void {
|
||||
const isFailure = state.result_state === 'failure';
|
||||
const label = document.querySelector('[data-result-label]');
|
||||
const title = document.querySelector('[data-result-title]');
|
||||
|
||||
if (label) label.textContent = isFailure ? '失败' : '成功';
|
||||
if (title) title.textContent = isFailure ? '发布失败' : '发布完成';
|
||||
}
|
||||
|
||||
const viewer = createAnnotationViewer({
|
||||
source: annotationSource as AnnotationSourceDocument,
|
||||
options: {
|
||||
getCurrentPageId: () => currentPageId,
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
onDirectoryRoute: (node: AnnotationDirectoryRouteNode) => {
|
||||
renderPage(normalizePageId(node.route));
|
||||
viewer.refresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-route]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
renderPage(normalizePageId(button.dataset.route));
|
||||
viewer.refresh();
|
||||
});
|
||||
});
|
||||
|
||||
void viewer.start().then(() => {
|
||||
const attach = () => {
|
||||
const protoDev = window.__AXHUB_PROTO_DEV__;
|
||||
if (!protoDev) {
|
||||
window.setTimeout(attach, 80);
|
||||
return;
|
||||
}
|
||||
|
||||
renderState(protoDev.getState());
|
||||
protoDev.subscribe(() => renderState(protoDev.getState()));
|
||||
};
|
||||
|
||||
attach();
|
||||
});
|
||||
|
||||
renderPage(currentPageId);
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AnnotationViewer,
|
||||
useProtoDevState,
|
||||
type AnnotationDirectoryRouteNode,
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions,
|
||||
} from '@axhub/annotation';
|
||||
import annotationSource from './annotation-source.json';
|
||||
|
||||
type PageId = 'overview' | 'states';
|
||||
type ResultState = 'success' | 'failure';
|
||||
|
||||
function normalizePageId(value: unknown): PageId {
|
||||
return value === 'states' ? 'states' : 'overview';
|
||||
}
|
||||
|
||||
function normalizeResultState(value: unknown): ResultState {
|
||||
return value === 'failure' ? 'failure' : 'success';
|
||||
}
|
||||
|
||||
function StateCard() {
|
||||
const protoState = useProtoDevState<{ result_state?: ResultState }>();
|
||||
const resultState = normalizeResultState(protoState.result_state);
|
||||
const isSuccess = resultState === 'success';
|
||||
|
||||
return (
|
||||
<article data-annotation-id="state-card">
|
||||
<strong>{isSuccess ? '成功' : '失败'}</strong>
|
||||
<h2>{isSuccess ? '发布完成' : '发布失败'}</h2>
|
||||
<p>{isSuccess ? '可以继续评审标注内容。' : '需要展示失败原因和重试入口。'}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnnotationStandaloneReactExample() {
|
||||
const [pageId, setPageId] = React.useState<PageId>('overview');
|
||||
|
||||
const options = React.useMemo<AnnotationViewerOptions>(() => ({
|
||||
currentPageId: pageId,
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
onDirectoryRoute: (node: AnnotationDirectoryRouteNode) => {
|
||||
setPageId(normalizePageId(node.route));
|
||||
},
|
||||
}), [pageId]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<nav>
|
||||
<button type="button" onClick={() => setPageId('overview')}>运行时总览</button>
|
||||
<button type="button" onClick={() => setPageId('states')}>状态标注</button>
|
||||
</nav>
|
||||
|
||||
{pageId === 'overview' ? (
|
||||
<section data-annotation-id="overview-hero">
|
||||
<h1>@axhub/annotation</h1>
|
||||
<p>这是一个脱离平台的 React 接入示例。</p>
|
||||
</section>
|
||||
) : (
|
||||
<StateCard />
|
||||
)}
|
||||
|
||||
<AnnotationViewer
|
||||
source={annotationSource as AnnotationSourceDocument}
|
||||
options={options}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user