扩展氢能站点、加氢记录与台账原型链路,新增工作台、车辆资产 H5、自营物流等原型,并同步导航注册、PRD 资源与交付技能。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
297
.agents/skills/user-story-mapping/SKILL.md
Normal file
297
.agents/skills/user-story-mapping/SKILL.md
Normal file
@@ -0,0 +1,297 @@
|
||||
---
|
||||
name: user-story-mapping
|
||||
argument-hint: "[product or workflow]"
|
||||
description: Create a user story map that lays out activities, steps, tasks, and release slices. Use when planning a workflow, backlog, or MVP around the user journey.
|
||||
intent: >-
|
||||
Visualize the user journey by creating a hierarchical map that breaks down high-level activities into steps and tasks, organized left-to-right as a narrative flow. Use this to build shared understanding across product, design, and engineering, prioritize features based on user workflows, and identify gaps or opportunities in the user experience.
|
||||
type: component
|
||||
---
|
||||
|
||||
|
||||
## Purpose
|
||||
Visualize the user journey by creating a hierarchical map that breaks down high-level activities into steps and tasks, organized left-to-right as a narrative flow. Use this to build shared understanding across product, design, and engineering, prioritize features based on user workflows, and identify gaps or opportunities in the user experience.
|
||||
|
||||
This is not a backlog—it's a strategic artifact that shows *how* users accomplish their goals, which then informs *what* to build.
|
||||
|
||||
## Input
|
||||
|
||||
**Works best with:** The product or user workflow being mapped.
|
||||
**Also useful:** The primary user, the end-to-end narrative as you understand it, existing backlog items to place, and release goals.
|
||||
|
||||
Anything supplied with the invocation itself — text after the skill name, a pasted context dump, or an appended `ARGUMENTS:` line — counts as answers already given. Use it and skip whatever it covers; don't re-ask.
|
||||
|
||||
**Arriving empty-handed? That works too.** The skill asks whose journey you're mapping and what they're trying to get done, then builds backbone → tasks → slices.
|
||||
|
||||
**Example invocation:** `Story map for our expense-reporting flow, from receipt capture to reimbursement, with an MVP slice for the pilot.`
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### The Jeff Patton Story Mapping Framework
|
||||
Invented by Jeff Patton, story mapping organizes work into a 2D structure:
|
||||
|
||||
**Horizontal axis (left-to-right):** User journey over time
|
||||
- **Backbone:** High-level activities the user performs
|
||||
- **Steps:** Specific actions within each activity
|
||||
- **Tasks:** Detailed work required to complete each step
|
||||
|
||||
**Vertical axis (top-to-bottom):** Priority and releases
|
||||
- **Top rows:** Essential tasks (MVP / Release 1)
|
||||
- **Lower rows:** Nice-to-have tasks (Future releases)
|
||||
|
||||
### Story Map Structure
|
||||
|
||||
```
|
||||
Segment → Persona → Narrative (User's goal)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
[Activity 1] → [Activity 2] → [Activity 3] → [Activity 4] → [Activity 5]
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
[Step 1.1] [Step 2.1] [Step 3.1] [Step 4.1] [Step 5.1]
|
||||
[Step 1.2] [Step 2.2] [Step 3.2] [Step 4.2] [Step 5.2]
|
||||
[Step 1.3] [Step 2.3] [Step 3.3] [Step 4.3] [Step 5.3]
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
[Task 1.1.1] [Task 2.1.1] [Task 3.1.1] [Task 4.1.1] [Task 5.1.1]
|
||||
[Task 1.1.2] [Task 2.1.2] [Task 3.1.2] [Task 4.1.2] [Task 5.1.2]
|
||||
[Task 1.1.3] [Task 2.1.3] [Task 3.1.3] [Task 4.1.3] [Task 5.1.3]
|
||||
... ... ... ... ...
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
- **User-centric:** Organizes work around user goals, not engineering modules
|
||||
- **Shared understanding:** Product, design, engineering all see the same journey
|
||||
- **Prioritization clarity:** Top tasks = MVP, lower tasks = future iterations
|
||||
- **Gap identification:** Missing steps or tasks become obvious
|
||||
- **Release planning:** Draw horizontal "release lines" to define scope
|
||||
|
||||
### Anti-Patterns (What This Is NOT)
|
||||
- **Not a Gantt chart:** This isn't project management—it's user journey visualization
|
||||
- **Not a feature list:** Activities aren't features—they're user behaviors
|
||||
- **Not static:** Story maps evolve as you learn more about users
|
||||
|
||||
### When to Use This
|
||||
- Kicking off a new product or major feature
|
||||
- Aligning stakeholders on user workflow
|
||||
- Prioritizing backlog based on user needs
|
||||
- Identifying MVP vs. future releases
|
||||
- Onboarding new team members to the product vision
|
||||
|
||||
### When NOT to Use This
|
||||
- For trivial features (don't map what you already understand)
|
||||
- When user workflows are constantly changing (map stabilizes workflows)
|
||||
- As a replacement for user stories (the map informs stories, doesn't replace them)
|
||||
|
||||
---
|
||||
|
||||
## Application
|
||||
|
||||
### Step 1: Define the Context
|
||||
|
||||
Use `template.md` for the full fill-in structure.
|
||||
|
||||
#### Segment
|
||||
Who are you building for?
|
||||
|
||||
```markdown
|
||||
### Segment:
|
||||
- [Specify the target segment, e.g., "Small business owners using DIY accounting software"]
|
||||
```
|
||||
|
||||
**Quality checks:**
|
||||
- **Specific:** Not "users" but "enterprise IT admins" or "freelance designers"
|
||||
|
||||
---
|
||||
|
||||
#### Persona
|
||||
Provide details about the persona within this segment (reference `skills/proto-persona/SKILL.md`).
|
||||
|
||||
```markdown
|
||||
### Persona:
|
||||
- [Describe the persona: demographics, behaviors, pains, goals]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
- "Sarah, 35-year-old freelance graphic designer, manages 5-10 client projects at once, struggles with invoicing and payment tracking, wants to spend less time on admin and more time designing"
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Define the Narrative
|
||||
What is the user trying to accomplish? Frame this as a Jobs-to-be-Done statement (reference `skills/jobs-to-be-done/SKILL.md`).
|
||||
|
||||
```markdown
|
||||
### Narrative:
|
||||
- [Concise narrative of the persona's objective, e.g., "Complete a client project from kickoff to final payment"]
|
||||
```
|
||||
|
||||
**Quality checks:**
|
||||
- **Outcome-focused:** Not "use the product" but "deliver a client project on time and get paid"
|
||||
- **One sentence:** If it takes more than one sentence, the scope may be too broad
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Identify Activities (Backbone)
|
||||
List 3-5 high-level activities the persona engages in to fulfill the narrative. These form the backbone of your map.
|
||||
|
||||
```markdown
|
||||
### Activities:
|
||||
1. [Activity 1, e.g., "Negotiate project scope and pricing"]
|
||||
2. [Activity 2, e.g., "Execute design work"]
|
||||
3. [Activity 3, e.g., "Deliver final assets to client"]
|
||||
4. [Activity 4, e.g., "Send invoice and receive payment"]
|
||||
5. [Activity 5, optional]
|
||||
```
|
||||
|
||||
**Quality checks:**
|
||||
- **Sequential:** Activities happen in order (left-to-right)
|
||||
- **User actions:** Describe what the user *does*, not what the product *provides*
|
||||
- **3-5 activities:** Too few = oversimplified, too many = overwhelming
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Break Activities into Steps
|
||||
For each activity, list 3-5 steps that detail how the activity is carried out.
|
||||
|
||||
```markdown
|
||||
### Steps:
|
||||
|
||||
**For Activity 1: [Activity Name]**
|
||||
- Step 1: [Detail step 1, e.g., "Review client brief"]
|
||||
- Step 2: [Detail step 2, e.g., "Draft project proposal"]
|
||||
- Step 3: [Detail step 3, e.g., "Negotiate timeline and budget"]
|
||||
- Step 4: [Optional step 4]
|
||||
- Step 5: [Optional step 5]
|
||||
|
||||
**For Activity 2: [Activity Name]**
|
||||
- Step 1: [Detail step 1]
|
||||
- Step 2: [Detail step 2]
|
||||
...
|
||||
```
|
||||
|
||||
**Quality checks:**
|
||||
- **Actionable:** Each step is something the user does
|
||||
- **Observable:** You could watch someone perform this step
|
||||
- **Logical sequence:** Steps follow a natural order
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Break Steps into Tasks
|
||||
For each step, list 5-7 tasks that must be completed.
|
||||
|
||||
```markdown
|
||||
### Tasks:
|
||||
|
||||
**For Activity 1, Step 1: [Step Name]**
|
||||
- Task 1: [Detail task 1, e.g., "Read client brief document"]
|
||||
- Task 2: [Detail task 2, e.g., "Identify key deliverables"]
|
||||
- Task 3: [Detail task 3, e.g., "Note budget constraints"]
|
||||
- Task 4: [Detail task 4, e.g., "Clarify timeline expectations"]
|
||||
- Task 5: [Detail task 5, e.g., "List open questions for client"]
|
||||
- Task 6: [Optional task 6]
|
||||
- Task 7: [Optional task 7]
|
||||
|
||||
**For Activity 1, Step 2: [Step Name]**
|
||||
- Task 1: [Detail task 1]
|
||||
...
|
||||
```
|
||||
|
||||
**Quality checks:**
|
||||
- **Granular:** Tasks are small, specific actions
|
||||
- **User-facing or behind-the-scenes:** Include both (e.g., "Send email" and "Receive confirmation")
|
||||
- **Prioritizable:** You'll prioritize tasks vertically (top = essential, bottom = nice-to-have)
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Prioritize Vertically
|
||||
Arrange tasks top-to-bottom by priority:
|
||||
- **Top rows:** MVP / Release 1 (must-have)
|
||||
- **Middle rows:** Release 2 (important but not critical)
|
||||
- **Bottom rows:** Future / Nice-to-have
|
||||
|
||||
Draw horizontal "release lines" to demarcate scope.
|
||||
|
||||
---
|
||||
|
||||
### Step 7: Identify Gaps and Opportunities
|
||||
Review the map and ask:
|
||||
- Are there missing steps or tasks?
|
||||
- Are there pain points we're not addressing?
|
||||
- Are there opportunities to delight users?
|
||||
- Do all activities flow logically?
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
See `examples/sample.md` for a full story map example.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Activities Are Features, Not User Behaviors
|
||||
**Symptom:** "Activity 1: Use the dashboard. Activity 2: Generate reports."
|
||||
|
||||
**Consequence:** You've mapped the product, not the user journey.
|
||||
|
||||
**Fix:** Reframe as user actions: "Activity 1: Monitor project progress. Activity 2: Summarize work for stakeholders."
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Too Many Activities
|
||||
**Symptom:** 10+ activities across the backbone
|
||||
|
||||
**Consequence:** Map becomes overwhelming and loses focus.
|
||||
|
||||
**Fix:** Consolidate. If you have 10 activities, you're likely mixing activities with steps. Aim for 3-5 high-level activities.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Tasks Are Too Vague
|
||||
**Symptom:** "Task 1: Do the thing"
|
||||
|
||||
**Consequence:** Can't prioritize or estimate vague tasks.
|
||||
|
||||
**Fix:** Be specific: "Task 1: Enter client email address in the 'Bill To' field."
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Ignoring Vertical Prioritization
|
||||
**Symptom:** All tasks at the same level—no MVP vs. future releases defined
|
||||
|
||||
**Consequence:** No clarity on what to build first.
|
||||
|
||||
**Fix:** Explicitly prioritize. Draw release lines. Force hard choices about what's MVP.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: Mapping in Isolation
|
||||
**Symptom:** PM creates the map alone, then presents it to the team
|
||||
|
||||
**Consequence:** No shared ownership or understanding.
|
||||
|
||||
**Fix:** Map collaboratively. Run a story mapping workshop with product, design, and engineering.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Related Skills
|
||||
- `skills/proto-persona/SKILL.md` — Defines the persona for the story map
|
||||
- `skills/jobs-to-be-done/SKILL.md` — Informs the narrative and activities
|
||||
- `skills/user-story/SKILL.md` — Tasks from the map become user stories
|
||||
- `skills/problem-statement/SKILL.md` — Problem statement frames the narrative
|
||||
|
||||
### External Frameworks
|
||||
- Jeff Patton, *User Story Mapping* (2014) — Origin of the story mapping technique
|
||||
- Teresa Torres, *Continuous Discovery Habits* (2021) — Opportunity solution trees (complementary to story maps)
|
||||
|
||||
### Dean's Work
|
||||
- User Story Mapping Prompt (adapted from Jeff Patton's methodology)
|
||||
|
||||
### Provenance
|
||||
- Adapted from `prompts/user-story-mapping.md` in the `https://github.com/deanpeters/product-manager-prompts` repo.
|
||||
|
||||
---
|
||||
|
||||
**Skill type:** Component
|
||||
**Suggested filename:** `user-story-mapping.md`
|
||||
**Suggested placement:** `/skills/components/`
|
||||
**Dependencies:** References `skills/proto-persona/SKILL.md`, `skills/jobs-to-be-done/SKILL.md`, `skills/user-story/SKILL.md`, `skills/problem-statement/SKILL.md`
|
||||
77
.agents/skills/user-story-mapping/examples/sample.md
Normal file
77
.agents/skills/user-story-mapping/examples/sample.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# User Story Mapping Example
|
||||
|
||||
## Example: Story Map for Freelance Invoicing Product
|
||||
|
||||
```markdown
|
||||
## User Story Map: Freelance Invoicing
|
||||
|
||||
### Who
|
||||
|
||||
#### Segment:
|
||||
- Freelance creative professionals (designers, writers, photographers)
|
||||
|
||||
#### Persona:
|
||||
- Sarah, 35, freelance graphic designer
|
||||
- Manages 5-10 clients at once
|
||||
- Struggles with invoicing, payment tracking, and follow-ups
|
||||
- Wants to spend less time on admin, more time designing
|
||||
- Currently uses Excel + email, which is error-prone and time-consuming
|
||||
|
||||
### Backbone
|
||||
|
||||
#### Narrative:
|
||||
- Complete a client project from kickoff to final payment without admin hassle
|
||||
|
||||
#### Activities:
|
||||
1. Negotiate project scope and pricing
|
||||
2. Execute design work
|
||||
3. Deliver final assets
|
||||
4. Send invoice and receive payment
|
||||
5. Follow up on late payments
|
||||
|
||||
### Steps:
|
||||
|
||||
**For Activity 1: Negotiate project scope and pricing**
|
||||
- Step 1: Review client brief
|
||||
- Step 2: Draft project proposal
|
||||
- Step 3: Negotiate timeline and budget
|
||||
|
||||
**For Activity 2: Execute design work**
|
||||
- Step 1: Create initial concepts
|
||||
- Step 2: Share concepts for feedback
|
||||
- Step 3: Iterate based on feedback
|
||||
- Step 4: Finalize design
|
||||
|
||||
**For Activity 3: Deliver final assets**
|
||||
- Step 1: Export final files in client-requested formats
|
||||
- Step 2: Upload files to shared folder or email
|
||||
- Step 3: Confirm client receipt
|
||||
|
||||
**For Activity 4: Send invoice and receive payment**
|
||||
- Step 1: Create invoice with project details
|
||||
- Step 2: Send invoice to client
|
||||
- Step 3: Track payment status
|
||||
- Step 4: Confirm payment received
|
||||
|
||||
**For Activity 5: Follow up on late payments**
|
||||
- Step 1: Identify overdue invoices
|
||||
- Step 2: Send payment reminder
|
||||
- Step 3: Escalate if still unpaid
|
||||
|
||||
### Tasks (Sample for Activity 4, Step 1: Create invoice):
|
||||
|
||||
**MVP (Release 1):**
|
||||
- Task 1: Enter client name and contact info
|
||||
- Task 2: Add line items (description, hours, rate)
|
||||
- Task 3: Calculate total automatically
|
||||
- Task 4: Preview invoice before sending
|
||||
|
||||
**Release 2:**
|
||||
- Task 5: Add logo and custom branding
|
||||
- Task 6: Save invoice templates for repeat clients
|
||||
- Task 7: Auto-populate line items from project notes
|
||||
|
||||
**Future:**
|
||||
- Task 8: Generate invoices from time tracking data
|
||||
- Task 9: Multi-currency support
|
||||
```
|
||||
41
.agents/skills/user-story-mapping/template.md
Normal file
41
.agents/skills/user-story-mapping/template.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# User Story Map Template
|
||||
|
||||
Use this template to create a complete user story map (segment, persona, backbone, steps, and tasks).
|
||||
|
||||
## Provenance
|
||||
Adapted from `prompts/user-story-mapping.md` in the `https://github.com/deanpeters/product-manager-prompts` repo.
|
||||
|
||||
## Template
|
||||
```markdown
|
||||
## User Story Map Template
|
||||
|
||||
### Who
|
||||
|
||||
#### Segment:
|
||||
- [Specify the target segment]
|
||||
|
||||
#### Persona:
|
||||
- [Describe the persona and their key characteristics]
|
||||
|
||||
### Backbone
|
||||
|
||||
#### Narrative:
|
||||
- [Insert the concise narrative of the persona's objective]
|
||||
|
||||
#### Activities:
|
||||
1. [Describe Activity 1]
|
||||
2. [Describe Activity 2]
|
||||
3. [Continue as necessary for up to 5 activities]
|
||||
|
||||
#### Steps:
|
||||
For [Activity 1]:
|
||||
- Step 1: [Detail Step 1 for Activity 1]
|
||||
- Step 2: [Detail Step 2 for Activity 1]
|
||||
- Step 3: [Detail Step 3 for Activity 1]
|
||||
|
||||
#### Tasks:
|
||||
For [Activity 1, Step 1]:
|
||||
- Task 1: [Detail Task 1 for Step 1 of Activity 1]
|
||||
- Task 2: [Detail Task 2 for Step 1 of Activity 1]
|
||||
- Task 3: [Detail Task 3 for Step 1 of Activity 1]
|
||||
```
|
||||
@@ -1,79 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-07-20T07:33:31.296Z",
|
||||
"updatedAt": "2026-07-21T08:13:15.367Z",
|
||||
"prototypes": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-prototype-demo",
|
||||
"kind": "item",
|
||||
"title": "原型演示",
|
||||
"itemKey": "prototypes/oneos-prototype-demo"
|
||||
},
|
||||
{
|
||||
"id": "folder-1783875936186-27raza",
|
||||
"kind": "folder",
|
||||
"title": "旧ONEOS",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-business",
|
||||
"kind": "item",
|
||||
"title": "业务管理",
|
||||
"itemKey": "prototypes/oneos-web-business"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-data-analysis",
|
||||
"kind": "item",
|
||||
"title": "数据分析",
|
||||
"itemKey": "prototypes/oneos-web-data-analysis"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-finance",
|
||||
"kind": "item",
|
||||
"title": "财务管理(合包)",
|
||||
"itemKey": "prototypes/oneos-web-finance"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-help-center",
|
||||
"kind": "item",
|
||||
"title": "帮助中心",
|
||||
"itemKey": "prototypes/oneos-web-help-center"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-lease-contract",
|
||||
"kind": "item",
|
||||
"title": "车辆租赁合同",
|
||||
"itemKey": "prototypes/oneos-web-lease-contract"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-ledger-data",
|
||||
"kind": "item",
|
||||
"title": "台账数据",
|
||||
"itemKey": "prototypes/oneos-web-ledger-data"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-ops",
|
||||
"kind": "item",
|
||||
"title": "运维管理",
|
||||
"itemKey": "prototypes/oneos-web-ops"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-procurement",
|
||||
"kind": "item",
|
||||
"title": "采购管理",
|
||||
"itemKey": "prototypes/oneos-web-procurement"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-prototype-nav",
|
||||
"kind": "item",
|
||||
"title": "原型导航",
|
||||
"itemKey": "prototypes/oneos-prototype-nav"
|
||||
},
|
||||
{
|
||||
"id": "folder-1782874576229-r8atbu",
|
||||
"kind": "folder",
|
||||
"title": "OneOS",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-prototype-demo",
|
||||
"kind": "item",
|
||||
"title": "原型演示",
|
||||
"itemKey": "prototypes/oneos-prototype-demo"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-workbench-new",
|
||||
"kind": "item",
|
||||
@@ -185,12 +124,6 @@
|
||||
"title": "站点信息",
|
||||
"itemKey": "prototypes/oneos-web-h2-station-site"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-h5-h2-order",
|
||||
"kind": "item",
|
||||
"title": "加氢记录(H5)",
|
||||
"itemKey": "prototypes/oneos-h5-h2-order"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-h2-station",
|
||||
"kind": "item",
|
||||
@@ -204,9 +137,9 @@
|
||||
"itemKey": "prototypes/oneos-web-h2-station-weekly"
|
||||
},
|
||||
{
|
||||
"id": "item-prototypes-oneos-web-h2-station-analysis",
|
||||
"id": "item:prototypes:oneos-web-h2-station-analysis",
|
||||
"kind": "item",
|
||||
"title": "oneos web h2 station analysis",
|
||||
"title": "加氢站分析",
|
||||
"itemKey": "prototypes/oneos-web-h2-station-analysis"
|
||||
}
|
||||
]
|
||||
@@ -328,8 +261,94 @@
|
||||
"kind": "item",
|
||||
"title": "业务条线说明",
|
||||
"itemKey": "prototypes/lease-business-line-overview"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:yunxiao-pipeline-handbook",
|
||||
"kind": "item",
|
||||
"title": "云效生产线手册",
|
||||
"itemKey": "prototypes/yunxiao-pipeline-handbook"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "folder-1784563868850-xi0guc",
|
||||
"kind": "folder",
|
||||
"title": "小羚羚",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-h5-vehicle-assets",
|
||||
"kind": "item",
|
||||
"title": "车辆资产(H5)",
|
||||
"itemKey": "prototypes/oneos-h5-vehicle-assets"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-h5-h2-order",
|
||||
"kind": "item",
|
||||
"title": "加氢记录(H5)",
|
||||
"itemKey": "prototypes/oneos-h5-h2-order"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "folder-1783875936186-27raza",
|
||||
"kind": "folder",
|
||||
"title": "旧ONEOS",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-business",
|
||||
"kind": "item",
|
||||
"title": "业务管理",
|
||||
"itemKey": "prototypes/oneos-web-business"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-data-analysis",
|
||||
"kind": "item",
|
||||
"title": "数据分析",
|
||||
"itemKey": "prototypes/oneos-web-data-analysis"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-finance",
|
||||
"kind": "item",
|
||||
"title": "财务管理(合包)",
|
||||
"itemKey": "prototypes/oneos-web-finance"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-help-center",
|
||||
"kind": "item",
|
||||
"title": "帮助中心",
|
||||
"itemKey": "prototypes/oneos-web-help-center"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-lease-contract",
|
||||
"kind": "item",
|
||||
"title": "车辆租赁合同",
|
||||
"itemKey": "prototypes/oneos-web-lease-contract"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-ledger-data",
|
||||
"kind": "item",
|
||||
"title": "台账数据",
|
||||
"itemKey": "prototypes/oneos-web-ledger-data"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-ops",
|
||||
"kind": "item",
|
||||
"title": "运维管理",
|
||||
"itemKey": "prototypes/oneos-web-ops"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-procurement",
|
||||
"kind": "item",
|
||||
"title": "采购管理",
|
||||
"itemKey": "prototypes/oneos-web-procurement"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-prototype-nav",
|
||||
"kind": "item",
|
||||
"title": "原型导航",
|
||||
"itemKey": "prototypes/oneos-prototype-nav"
|
||||
}
|
||||
],
|
||||
"docs": [],
|
||||
@@ -1116,4 +1135,4 @@
|
||||
"templates": [],
|
||||
"components": [],
|
||||
"canvas": []
|
||||
}
|
||||
}
|
||||
139
.claude/skills/AutoVUL/SKILL.md
Normal file
139
.claude/skills/AutoVUL/SKILL.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: AutoVUL
|
||||
description: >-
|
||||
Generates OneOS PC version update logs from a Yunxiao (云效) iteration name by
|
||||
loading all linked requirements, or from a pasted changelog list. Use when testers
|
||||
or PMs say AutoVUL, 版本更新日志, 按迭代生成更新日志, 发版公告, or give a 迭代名称.
|
||||
---
|
||||
|
||||
# AutoVUL(OneOS PC 版本更新日志)
|
||||
|
||||
面向**测试人员发版**:输入云效**迭代名称** → 拉取该迭代关联需求 → 自动生成对外版本更新日志。
|
||||
|
||||
也可手动粘贴变更清单(兜底)。**预计维护时长**由人工单独通知,不写进日志。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 测试说:`按 $AutoVUL` / `生成版本更新日志` / `按迭代 xxx 出更新日志`
|
||||
- 输入含云效「迭代名称」
|
||||
- 发版前需要统一文案口径
|
||||
|
||||
## 主流程(测试 · 按迭代,默认)
|
||||
|
||||
完整取数步骤见 [references/yunxiao-sprint.md](references/yunxiao-sprint.md)。
|
||||
|
||||
```text
|
||||
1. 确认云效项目(缺省:统一运营管理平台PC端;可改)
|
||||
2. 向用户索取「迭代名称」(若口令里已带则直接用)
|
||||
3. 在云效查找该迭代 → 立即反馈成功或失败(见下)
|
||||
4. 成功:列出迭代内关联「需求」清单(编号 + 标题 + 状态)供用户快速核对
|
||||
5. 从每条需求提取变更素材(优先级见「素材优先级」)
|
||||
6. 按本文格式分类、合并、润色 → 输出成稿正文
|
||||
```
|
||||
|
||||
### 迭代读取 · 成功 / 失败反馈(强制)
|
||||
|
||||
查找迭代后**必须先反馈**,再继续或停止:
|
||||
|
||||
| 结果 | 反馈文案(可微调,语义不变) | 下一步 |
|
||||
|------|------------------------------|--------|
|
||||
| 精确命中 1 个迭代 | `✅ 已找到迭代「{名称}」(状态:{状态}),关联需求 {N} 条。` | 继续拉需求并生成 |
|
||||
| 0 个匹配 | `❌ 未找到迭代「{输入}」。请核对后重新输入迭代名称(可复制云效迭代标题)。` | **停止生成**;等待用户重输 |
|
||||
| 多个模糊匹配 | `⚠️ 找到多个相似迭代:1)… 2)… 请回复序号或完整精确名称。` | 等用户选定后再继续 |
|
||||
| 登录/权限失败 | `❌ 无法访问云效(未登录或无权限)。请在浏览器登录云效后重试,或改用手动清单。` | 停止;提示登录或兜底 |
|
||||
| 迭代存在但 0 条需求 | `⚠️ 迭代「{名称}」已找到,但暂无关联需求。请确认规划后再试,或改用手动清单。` | 停止生成对外正文 |
|
||||
|
||||
**失败后**:用户只需再发一次迭代名称(或纠正项目名),从步骤 2 重跑;不要要求用户重装 Skill。
|
||||
|
||||
### 素材优先级(每条需求)
|
||||
|
||||
| 顺序 | 来源 | 用法 |
|
||||
|------|------|------|
|
||||
| 1 | 需求描述中的「更新内容」段(非「更新内容·历史」) | 首选,直接改写成日志条目 |
|
||||
| 2 | AutoPRD「功能变更记录」中可对外部分 | 次选 |
|
||||
| 3 | 需求标题 + 简短描述摘要 | 仅当前两者皆空时;价值句可合理补全 |
|
||||
| — | 纯技术债、内部监控、未对用户开放 | **不写入**对外日志 |
|
||||
|
||||
Bug 类需求(类型/标签含缺陷修复、或更新内容写「修复」)归入【Bug修复】;其余归【新功能】。边界不清优先 Bug修复。
|
||||
|
||||
### 版本号与更新时间
|
||||
|
||||
| 字段 | 规则 |
|
||||
|------|------|
|
||||
| 版本号 | 优先从迭代名解析(如含 `V1.1.5` / `1.1.5`);解析不到再问用户 |
|
||||
| 更新时间 | 用户提供则用;未提供可写迭代计划开始日的 `MM月DD日HH:MM`,或先问 |
|
||||
| 产品线 | 默认 `OneOS PC版` |
|
||||
|
||||
## 兜底流程(手动清单)
|
||||
|
||||
无云效权限、或迭代为空时:用 [input-template.md](input-template.md) 粘贴清单,跳过迭代步骤,直接分类润色。
|
||||
|
||||
## 固定输出格式
|
||||
|
||||
```text
|
||||
OneOS PC版V{主.次.修订}更新日志:
|
||||
更新时间:{MM}月{DD}日{HH}:{MM}
|
||||
----------------------------------------------------
|
||||
【新功能】
|
||||
1「{模块名}」{做了什么},{责任部门}可{用户价值 / 解决的问题}
|
||||
2「{模块名}」{…}
|
||||
|
||||
【Bug修复】
|
||||
3「{模块名}」{修复了什么},{责任部门}可{恢复的能力 / 避免的影响}
|
||||
```
|
||||
|
||||
### 版式硬约束
|
||||
|
||||
| 项 | 规则 |
|
||||
|----|------|
|
||||
| 标题 | `OneOS PC版V` + 版本号 + `更新日志:`(如 `V1.1.5`,无空格) |
|
||||
| 更新时间 | 仅开始时刻;`MM月DD日HH:MM`;**禁止**「预计x分钟内完成」 |
|
||||
| 分隔线 | `----------------------------------------------------` |
|
||||
| 分区 | 仅 `【新功能】` / `【Bug修复】`;空类整段省略 |
|
||||
| 序号 | 全篇连续,从 1 起 |
|
||||
| 模块名 | `「」`;与产品菜单正式名一致 |
|
||||
| 对外正文 | **不写**需求号、缺陷号、迭代 ID(核对清单可另附) |
|
||||
| 结尾 | 无签名、无「感谢使用」、无技术附录 |
|
||||
|
||||
## 分类 / 合并 / 文案(成稿规则)
|
||||
|
||||
| 归入【新功能】 | 归入【Bug修复】 | 不写进对外日志 |
|
||||
|----------------|-----------------|----------------|
|
||||
| 新增能力、新入口、新字段/流程、可感知体验优化 | 原有能力异常、报错、数据错、交互失效 | 技术债、无感性能、未开放灰度、仅内部监控 |
|
||||
|
||||
- **同分区同模块**合并为一条,分点用 `;`
|
||||
- **跨分区**同模块分栏各写一条
|
||||
- 每条含:做什么 + 用户价值 + 责任部门自然嵌入(`{部门}人员可…`)
|
||||
- **禁止**「供…条线…使用」;部门对照 [module-role-mapping.md](module-role-mapping.md)
|
||||
- 禁止编造迭代内不存在的能力;存疑跳过或标待确认(不进正式成稿)
|
||||
- 单条建议 ≤100 字
|
||||
|
||||
排序:先新功能后修复;同分区全员影响优先;资金/合规修复置修复区最前。
|
||||
|
||||
## 输出纪律
|
||||
|
||||
1. **先**输出迭代读取反馈(✅/❌/⚠️)
|
||||
2. 成功时:可先给「本迭代需求清单」短表(含编号,便于测试核对)
|
||||
3. 再输出**纯文本成稿正文**(默认不要 Markdown 大标题包裹成稿)
|
||||
4. 用户明确要求时,可在正文后附「分类依据 / 跳过项」
|
||||
|
||||
## 发布前自检
|
||||
|
||||
- [ ] 已反馈迭代查找结果;失败时未强行编造成稿
|
||||
- [ ] 成稿条目均可追溯到本迭代关联需求素材
|
||||
- [ ] 标题、时间行、分隔线正确;无预计时长
|
||||
- [ ] 同模块合并正确;每条含价值 + 部门自然嵌入
|
||||
- [ ] 对外正文无需求号/缺陷号/技术黑话;序号连续
|
||||
|
||||
## 给测试同事的口令
|
||||
|
||||
```text
|
||||
按 $AutoVUL 生成版本更新日志。
|
||||
项目:统一运营管理平台PC端
|
||||
迭代名称:V1.1.5发版迭代
|
||||
更新时间:07月16日16:00
|
||||
```
|
||||
|
||||
迭代名写错时,按提示改完再发一次即可。
|
||||
|
||||
手动兜底见 [input-template.md](input-template.md);成稿范例见 [examples.md](examples.md)。
|
||||
77
.claude/skills/AutoVUL/examples.md
Normal file
77
.claude/skills/AutoVUL/examples.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# AutoVUL 成稿与交互范例
|
||||
|
||||
## 1. 迭代读取失败 → 重输
|
||||
|
||||
**用户:**
|
||||
|
||||
```text
|
||||
按 $AutoVUL,迭代名称:V1.1.5
|
||||
```
|
||||
|
||||
**Agent:**
|
||||
|
||||
```text
|
||||
❌ 未找到迭代「V1.1.5」。请核对后重新输入迭代名称(可复制云效迭代标题)。
|
||||
```
|
||||
|
||||
**用户:**
|
||||
|
||||
```text
|
||||
V1.1.5发版迭代
|
||||
```
|
||||
|
||||
**Agent:**
|
||||
|
||||
```text
|
||||
✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 3 条:
|
||||
1. ONEOSP-201 车辆管理运营城市展示优化
|
||||
2. ONEOSP-202 还车应结款租金计算修复
|
||||
3. ONEOSP-203 业务管理新增租赁物流统计
|
||||
|
||||
OneOS PC版V1.1.5更新日志:
|
||||
更新时间:07月16日16:00
|
||||
----------------------------------------------------
|
||||
【新功能】
|
||||
1「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源
|
||||
2「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况
|
||||
|
||||
【Bug修复】
|
||||
3「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确
|
||||
```
|
||||
|
||||
## 2. 完整成稿风格基准(手动/迭代素材均可)
|
||||
|
||||
> 责任部门对照:`module-role-mapping.md`(OneOS 业务条线说明)
|
||||
|
||||
```text
|
||||
OneOS PC版V1.1.5更新日志:
|
||||
更新时间:07月16日16:00
|
||||
----------------------------------------------------
|
||||
【新功能】
|
||||
1「加氢站管理」升级新版加氢站模块,采购部可统一维护签约站与非签约站信息,签约加氢站可便捷管理站点日常运营
|
||||
2「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源
|
||||
3「审批中心」支持按客户名称搜索关联合同,业务员、法务部审批时可快速定位合同;还车应结款添加照片附件后,业务管理组、安全部、运维部、能源部会签人员可在审批流中查看完整附件
|
||||
4「保险采购」支持保比价单按车辆单独勾选并分开提交审批,运维部、安全部人员可按车辆分批推进投保审批
|
||||
5「故障管理」增加「故障来源」字段展示,运维部人员可区分上报渠道并跟进处理
|
||||
6「证照管理」特种设备使用登记证/使用标识照片支持OCR识别,运维部、安全部人员可减少手工录入、加快证照建档
|
||||
7「全局显示」统一各模块列表分页样式与交互,各模块操作人员跨页面浏览时体验一致、上手更快
|
||||
8「交还车管理」支持交车/还车签章文件在线预览,运维部、业务管理组、司机可现场核对签章文件;提交时增加胎纹数据必填校验,减少漏填导致的后续结算争议
|
||||
9「还车应结款」调整安全部待办区域权限,安全部人员仅处理职责范围内的还车应结款审批
|
||||
10「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况
|
||||
|
||||
【Bug修复】
|
||||
11「还车管理」修复签字单与还车检查单/还车应结款轮胎数据不一致,运维部、业务管理组人员还车验收与结算依据保持一致
|
||||
12「租赁合同」修复编辑合同提交时异常提示内容冗余,业务员、法务部人员可快速定位真正错误原因
|
||||
13「交还车管理」修复交车/还车签章客户名称关联错误,避免运维部、业务管理组、司机现场核对时客户信息张冠李戴
|
||||
14「车辆氢费明细」修复同客户不同生效时段、以及不同客户成本单价显示异常,能源部人员可准确核对氢费核算单价
|
||||
15「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确
|
||||
```
|
||||
|
||||
## 对照要点
|
||||
|
||||
| 规则 | 体现 |
|
||||
|------|------|
|
||||
| 先反馈迭代结果 | 失败可重输;成功列出需求再成稿 |
|
||||
| 无预计时长 | 更新时间仅 `07月16日16:00` |
|
||||
| 对外无需求号 | 成稿正文不出现 ONEOSP-xxx |
|
||||
| 部门自然嵌入 | `运维部人员可…`,无「供…条线使用」 |
|
||||
69
.claude/skills/AutoVUL/input-template.md
Normal file
69
.claude/skills/AutoVUL/input-template.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# AutoVUL · 输入模板
|
||||
|
||||
## A. 测试人员 · 按云效迭代(推荐)
|
||||
|
||||
复制下面口令发给 AI(改迭代名即可):
|
||||
|
||||
```text
|
||||
按 $AutoVUL 生成版本更新日志。
|
||||
项目:统一运营管理平台PC端
|
||||
迭代名称:
|
||||
更新时间:
|
||||
```
|
||||
|
||||
### 期望反馈
|
||||
|
||||
- 成功:`✅ 已找到迭代「…」,关联需求 N 条` → 随后输出成稿
|
||||
- 失败:`❌ 未找到迭代「…」。请重新输入迭代名称` → 改名后再发一次即可
|
||||
|
||||
**说明**:预计维护时长由人工单独通知,不写入版本更新日志。
|
||||
|
||||
---
|
||||
|
||||
## B. 手动清单(云效不可用时兜底)
|
||||
|
||||
### 版本元信息
|
||||
|
||||
| 字段 | 填写 | 示例 |
|
||||
|------|------|------|
|
||||
| 产品线 | | OneOS PC版 |
|
||||
| 版本号 | | V1.1.5 |
|
||||
| 更新开始时间 | | 07月16日16:00 |
|
||||
| 发布范围 | 全量 / 灰度(说明) | 全量 |
|
||||
| 是否对外发布 | 是 / 否 | 是 |
|
||||
|
||||
### 新功能(原始材料,可粗糙)
|
||||
|
||||
| 模块 | 做了什么 | 解决什么问题 / 用户故事 | 业务条线 | 责任部门 | 是否写入日志 |
|
||||
|------|----------|-------------------------|----------|----------|--------------|
|
||||
| 例:加氢站管理 | 升级新版模块 | 统一维护站点 | 能源业务条线 | 采购部、加氢站 | 是 |
|
||||
| | | | | | |
|
||||
|
||||
### Bug修复(原始材料,可粗糙)
|
||||
|
||||
| 模块 | 原问题现象 | 修复后表现 | 对用户的影响 | 业务条线 | 责任部门 | 是否写入日志 |
|
||||
|------|------------|------------|--------------|----------|----------|--------------|
|
||||
| 例:还车应结款 | 租金费用项重复 | 金额正确 | 避免结算偏差 | 租赁业务条线 | 业务管理组、安全部、运维部、业务管理组-能源部 | 是 |
|
||||
| | | | | | | |
|
||||
|
||||
### 明确不要写入对外日志
|
||||
|
||||
- (如:重构、依赖升级、仅内部监控、未上线配置)
|
||||
|
||||
### 粘贴给 AI
|
||||
|
||||
```text
|
||||
请按 $AutoVUL 用下方手动清单生成成稿(跳过云效迭代)。
|
||||
要求:同模块合并;每条含「做什么 + 用户价值」;责任部门自然融入;只输出正文。
|
||||
|
||||
【版本元信息】
|
||||
- 产品线:OneOS PC版
|
||||
- 版本号:V
|
||||
- 更新开始时间:
|
||||
|
||||
【新功能】
|
||||
(粘贴)
|
||||
|
||||
【Bug修复】
|
||||
(粘贴)
|
||||
```
|
||||
60
.claude/skills/AutoVUL/module-role-mapping.md
Normal file
60
.claude/skills/AutoVUL/module-role-mapping.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 模块 → 业务条线 / 责任部门对照
|
||||
|
||||
> 数据源:OneOS「业务条线说明」页责任部门定义(租赁 / 能源 / 运维 / 安全 / 自营)
|
||||
> 生成更新日志时,**适用对象**须从此表取 `roles`,不得自造部门名。
|
||||
|
||||
## 五条业务条线
|
||||
|
||||
| ID | 条线名称 |
|
||||
|----|----------|
|
||||
| lease | 租赁业务条线 |
|
||||
| energy | 能源业务条线 |
|
||||
| ops | 运维管理条线 |
|
||||
| safety | 安全管理条线 |
|
||||
| self-operated | 自营业务条线 |
|
||||
|
||||
## 本版变更模块对照
|
||||
|
||||
| 更新日志模块名 | 业务条线 | lines.ts 模块 | 责任部门(roles,原文) |
|
||||
|----------------|----------|---------------|-------------------------|
|
||||
| 加氢站管理 | 能源业务条线 | 加氢站管理 | 采购部、加氢站 |
|
||||
| 车辆管理 | 运维管理条线 | 验车入库 / 车辆管理(资产底座) | 运维部、采购部 |
|
||||
| 审批中心 | 跨条线 | 租赁合同 + 还车应结款(审批场景) | 业务员、法务部、业务管理组、安全部、运维部、业务管理组-能源部 |
|
||||
| 保险采购 | 运维管理条线 | 证照 / 保险(车辆资产关联) | 运维部、安全部 |
|
||||
| 故障管理 | 运维管理条线 | 故障管理 | 运维部、司机 |
|
||||
| 租赁合同 | 租赁业务条线 | 租赁合同 | 业务员、法务部 |
|
||||
| 证照管理 | 运维管理条线 | 证照管理 | 运维部、安全部 |
|
||||
| 全局显示 | 全系统 | 系统介绍(五条条线) | 业务管理组、运维部、采购部、安全部、法务部、财务部、业务管理组-能源部、业务服务组 |
|
||||
| 交还车管理 | 运维管理条线 | 交车管理 + 还车管理 | 运维部、业务管理组、司机 |
|
||||
| 还车应结款 | 租赁业务条线 | 还车应结款 | 业务管理组、安全部、运维部、业务管理组-能源部 |
|
||||
| 还车管理 | 运维管理条线 | 还车管理 | 运维部、业务管理组 |
|
||||
| 车辆氢费明细 | 能源业务条线 | 车辆氢费明细 | 业务管理组-能源部 |
|
||||
| 业务管理 · 租赁统计 | 租赁业务条线 | 租赁业务台账 / 客户管理 | 业务管理组、业务员 |
|
||||
| 业务管理 · 物流统计 | 自营业务条线 | 自营台账 | 业务服务组、财务部 |
|
||||
|
||||
## 写法规则
|
||||
|
||||
- 部门名与 `roles` **完全一致**(`业务管理组-能源部` 对外可简写「能源部人员」)
|
||||
- **禁止**「供…业务条线…使用」「供…人员使用」
|
||||
- **推荐**:`{部门}人员可…` / `{部门}可…` / `{部门}审批时可…`
|
||||
- 同模块合并后,多部门用顿号或分号自然串联
|
||||
- 找不到模块时:查 `lines.ts`;仍无则标「待产品确认」不进成稿
|
||||
|
||||
## 自然表述示例
|
||||
|
||||
| 模块 | 生硬(禁止) | 自然(推荐) |
|
||||
|------|--------------|--------------|
|
||||
| 车辆管理 | 供运维管理条线运维部使用 | 运维部人员可快速辨认车辆所属运营区域 |
|
||||
| 加氢站管理 | 供能源业务条线采购部、加氢站使用 | 采购部可统一维护站点信息,加氢站可便捷管理日常运营 |
|
||||
| 还车应结款 | 供租赁业务条线业务管理组…使用 | 业务管理组、安全部、运维部、能源部还车结算金额更准确 |
|
||||
|
||||
## 常见错误(禁止)
|
||||
|
||||
| 错误写法 | 正确写法 |
|
||||
|----------|----------|
|
||||
| 氢能运营业务人员 | 采购部、加氢站(能源 · 加氢站管理) |
|
||||
| 车辆运营及资产管理业务人员 | 运维部、采购部(运维 · 车辆管理) |
|
||||
| 各业务条线审批人员 | 业务员、法务部、…(按审批场景枚举) |
|
||||
| 保险采购业务人员 | 运维部、安全部 |
|
||||
| 还车结算业务人员 | 业务管理组、安全部、运维部、业务管理组-能源部 |
|
||||
| 经营分析及管理部门 | 业务管理组、业务服务组(分租赁 / 自营) |
|
||||
81
.claude/skills/AutoVUL/references/yunxiao-sprint.md
Normal file
81
.claude/skills/AutoVUL/references/yunxiao-sprint.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 云效迭代 → 需求清单(AutoVUL 取数)
|
||||
|
||||
测试人员只提供**迭代名称**;Agent 负责在云效定位迭代、拉取关联需求、抽出更新素材。
|
||||
|
||||
## 安全边界(强制)
|
||||
|
||||
- **禁止**在对话、Skill、日志、成稿中写入 Token、密码、Cookie、私钥。
|
||||
- 优先复用**已登录浏览器会话**操作云效 Projex(与 `yunxiao-requirement-lifecycle` 一致)。
|
||||
- 若团队已配置本机环境变量访问 OpenAPI,可走 API;Token 只读自环境,永不回显。
|
||||
|
||||
建议环境变量(可选,名称可按团队调整):
|
||||
|
||||
| 变量 | 用途 |
|
||||
|------|------|
|
||||
| `YUNXIAO_TOKEN` | 个人访问令牌(`x-yunxiao-token`) |
|
||||
| `YUNXIAO_DOMAIN` | 服务接入点域名 |
|
||||
| `YUNXIAO_ORG_ID` | 组织 ID(中心版) |
|
||||
| `YUNXIAO_PROJECT_ID` | 默认项目 ID(或运行时按项目名解析) |
|
||||
|
||||
## 默认项目
|
||||
|
||||
口令未指定时:`统一运营管理平台PC端`。项目名不精确时先问清再查迭代。
|
||||
|
||||
## 判定顺序:解析迭代
|
||||
|
||||
| 顺序 | 条件 | 结果 |
|
||||
|------|------|------|
|
||||
| 1 | 用户未给迭代名称 | 询问:「请输入云效迭代名称(可复制标题)」;不继续 |
|
||||
| 2 | 项目未确定 | 询问精确项目名;不继续 |
|
||||
| 3 | 云效不可访问 | `❌ 无法访问云效…`;不继续 |
|
||||
| 4 | 名称精确匹配 1 条迭代 | `✅ 已找到迭代…`;进入拉需求 |
|
||||
| 5 | 0 条 | `❌ 未找到迭代…请重新输入`;不继续 |
|
||||
| 6 | >1 条相似 | `⚠️ 多个相似…请回复序号或精确名称`;等用户选定 |
|
||||
| 7 | 命中但关联需求 0 | `⚠️ …暂无关联需求`;不生成对外正文 |
|
||||
|
||||
匹配规则:先**全等**(去首尾空格);全等失败再做包含/去空格模糊;模糊多条必须让用户确认,禁止擅自挑一个。
|
||||
|
||||
## 拉取关联需求
|
||||
|
||||
只收集工作项类型为**需求(Req)**且正式关联到该迭代的项。
|
||||
|
||||
- **浏览器路径**:打开项目 → 迭代 → 该迭代规划/工作项列表 → 筛选类型=需求 → 导出或逐条打开描述。
|
||||
- **OpenAPI 路径(示意)**:
|
||||
1. `ListSprints`:按项目列迭代,用 `name` 匹配用户输入,得到 `sprintId`
|
||||
2. `SearchWorkitems`:`category=Req`,`spaceId=项目Id`,`conditions` 中按字段 `sprint` CONTAINS `[sprintId]`(以租户实际 fieldIdentifier 为准;可先在需求列表页过滤后复制 `workitem/list` 的 conditions)
|
||||
3. 分页直到收齐(关注响应头 `x-total`)
|
||||
4. 需要正文时再 `GetWorkitem` 取 description
|
||||
|
||||
每条需求最少记录:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| serialNumber | 如 ONEOSP-123(仅用于核对清单,**不进对外成稿**) |
|
||||
| subject | 标题 |
|
||||
| status | 状态展示名 |
|
||||
| description | 用于抽取「更新内容」 |
|
||||
| category / type / labels | 辅助判断新功能 vs 修复 |
|
||||
|
||||
## 从描述抽「更新内容」
|
||||
|
||||
1. 定位标题「更新内容」或「## 更新内容」段落
|
||||
2. **不要**把「更新内容·历史」并入本版素材(历史仅备查)
|
||||
3. 若无「更新内容」:用标题 + 需求说明摘要;仍无有效产品语义则跳过并在内部备注
|
||||
|
||||
## 成功反馈后的核对清单(推荐短表)
|
||||
|
||||
在生成成稿前输出,例如:
|
||||
|
||||
```text
|
||||
✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 5 条:
|
||||
1. ONEOSP-101 保险采购比价分车提交
|
||||
2. ONEOSP-102 交还车签章预览
|
||||
…
|
||||
正在按 AutoVUL 格式生成更新日志…
|
||||
```
|
||||
|
||||
用户若立刻纠正「某条不要进日志」,从素材中剔除后再出成稿。
|
||||
|
||||
## 与发版任务关系
|
||||
|
||||
本 Skill **只读**迭代与需求,**不创建**【发版】任务、不改云效状态。发版挂链仍用 `yunxiao-requirement-lifecycle`(如「准备发布」口令 / A08)。
|
||||
183
.claude/skills/oneos-autoprd/SKILL.md
Normal file
183
.claude/skills/oneos-autoprd/SKILL.md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: oneos-autoprd
|
||||
description: >-
|
||||
Generates and keeps in sync OneOS AutoPRD (PM-facing requirements) plus Axhub
|
||||
Make annotation directory Markdown/PRD; on 需求定稿/定稿/确认定稿/本次定稿 appends
|
||||
functional release changelog below the PRD since last baseline. When a Yunxiao
|
||||
requirement advances to 分析中/设计中/待开发, creates a same-titled linked task
|
||||
(tag+create-time follow the requirement; 分析中/设计中 assignee=creator, 待开发
|
||||
assignee=何斐). Use eagerly for prototypes, AutoPRD, or Yunxiao requirement
|
||||
description/更新内容/stage advance—do not skip annotation sync, 定稿 changelog,
|
||||
or stage-task creation.
|
||||
---
|
||||
|
||||
# OneOS AutoPRD
|
||||
|
||||
为 OneOS 业务模块生成**产品经理可读、可评审、可排期**的需求说明,并**自动挂到 Axhub Make 标注工具 → 原型目录**。
|
||||
|
||||
另支持:**需求定稿**时汇总功能/逻辑变更记录;与云效组合时提供「需求说明 + 更新内容」;需求进入**分析中 / 设计中 / 待开发**时**自动创建与需求同名的关联任务**(规则见下,写在本 Skill,不改云效 Skill)。
|
||||
|
||||
颗粒度对齐「保险采购」全模块 PRD:讲清做什么、谁用、故事点、正逆向、流程图与关键业务逻辑;**不写**表结构、接口、字段代码名、文件路径、实现清单。
|
||||
|
||||
## 何时使用(含自动触发)
|
||||
|
||||
**主动调用时**
|
||||
|
||||
- AutoPRD、OneOS 需求说明、整模块 PRD、故事点 + 流程图
|
||||
|
||||
**改原型时必须自动跟进(全局规则)**
|
||||
|
||||
- 正在修改 `src/prototypes/<id>/` 下页面、交互、文案、判定、验收相关内容
|
||||
- 同一轮交付内同步更新 PRD Markdown + 标注目录,不得只改代码
|
||||
|
||||
**需求定稿时(强制)**
|
||||
|
||||
- 用户回复含:`需求定稿` / `定稿` / `确认定稿` / `本次定稿`
|
||||
- 执行 [references/release-changelog.md](references/release-changelog.md):写第 10 章 + 更新基线
|
||||
|
||||
**云效建需求 / 完善需求时(强制组合)**
|
||||
|
||||
- 与 `$yunxiao-requirement-lifecycle` 一起使用时:先跑本 Skill,再写云效描述
|
||||
- **需求说明** ← PRD 正文(或浓缩交付口径 + 关键章节)
|
||||
- **更新内容** ← 第 10 章中**自上次定稿以来**的条目(无增量则「首版定稿 / 本轮无功能增量」)
|
||||
|
||||
**云效需求推进至分析中 / 设计中 / 待开发时(强制)**
|
||||
|
||||
- 无论口令来自本 Skill 还是云效 Skill,只要本轮把需求推到上述状态,就执行 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)
|
||||
- 自动建**与需求同名**的任务并正式关联;标签与创建时间口径沿用需求;分析中/设计中负责人=创建人,待开发负责人=**何斐**
|
||||
|
||||
不要用本 Skill 替代:需求探索访谈、设计比稿、纯样式微调(无产品语义变化时可跳过全量重写)。
|
||||
|
||||
## 工作流
|
||||
|
||||
### 主流程(写/同步 PRD)
|
||||
|
||||
1. **定模块**:确认 OneOS 模块名与 `src/prototypes/<prototype-id>/`。
|
||||
2. **读上下文(只取产品语义)**
|
||||
- 用户说明、已确认口径、原型标注、`.spec/`、业务条线说明(`lines.ts`)
|
||||
- 忽略实现细节;字段名/接口改写成业务语言。
|
||||
3. **收敛边界**:做什么 / 不做什么、外部依赖、与其它模块关系。
|
||||
4. **按模板成文**:下方「输出结构」;缺关键信息最多问 1~2 个问题,其余写「假设」。
|
||||
5. **落盘 + 标注同步(强制)** — 见 [references/annotation-sync.md](references/annotation-sync.md)。
|
||||
|
||||
| 顺序 | 动作 |
|
||||
|------|------|
|
||||
| A | 写/更新 `src/prototypes/<id>/.spec/requirements-prd.md` |
|
||||
| B | 写/更新 `src/resources/prd/<id>-autoprd.md` |
|
||||
| C | 更新 `annotation-source.json` **顶层** `directory.nodes`(PRD 全文 + 推荐分章);禁止只写 `data.directory` |
|
||||
| D | 若存在 `scripts/sync-annotation-directory.mjs`,执行之 |
|
||||
|
||||
6. **交付说明**:路径、标注目录入口、故事点合计、开放问题/假设。
|
||||
|
||||
### 定稿流程(关键字触发)
|
||||
|
||||
见 [references/release-changelog.md](references/release-changelog.md)。摘要:
|
||||
|
||||
1. 读 PRD + `.spec/autoprd-baseline.json`
|
||||
2. 汇总自上次定稿以来的**功能/逻辑**变更(排除样式/UI/表结构)
|
||||
3. 追加到 `## 10. 功能变更记录`(最新在上)
|
||||
4. 更新基线 JSON + 标注目录
|
||||
5. 若同时发云效:本次定稿块 →「更新内容」;旧段 →「更新内容·历史」
|
||||
|
||||
### 云效交接格式(给 yunxiao skill 直接粘贴)
|
||||
|
||||
```markdown
|
||||
## 原型链接
|
||||
<对象存储或预览 URL>
|
||||
|
||||
## 需求说明
|
||||
<AutoPRD 正文或交付口径 + 必要章节>
|
||||
|
||||
## 更新内容
|
||||
<第 10 章本次定稿块;无则「首版定稿」或「本轮无功能/逻辑增量」>
|
||||
|
||||
## 更新内容·历史
|
||||
<以往更新内容倒序,勿删除>
|
||||
```
|
||||
|
||||
### 云效阶段任务(状态推进时强制)
|
||||
|
||||
见 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)。摘要:
|
||||
|
||||
| 需求状态 | 任务标签 | 负责人 | 标题 |
|
||||
|----------|----------|--------|------|
|
||||
| 分析中 | 分析 | 需求创建人 | 与需求同名 |
|
||||
| 设计中 | 设计 | 需求创建人 | 与需求同名 |
|
||||
| 待开发 | 交付(或开发) | 何斐 | 与需求同名 |
|
||||
|
||||
正式关联需求;创建时间口径沿用需求;同阶段未取消任务不重复建。
|
||||
|
||||
## 写作硬约束
|
||||
|
||||
**必须写**
|
||||
|
||||
- 一句话定位 + 目标 / 非目标
|
||||
- 模块边界(含 mermaid 总览更好)
|
||||
- 角色与目标(角色名优先对齐业务条线说明)
|
||||
- **用户故事**(业务条线说明口径)+ Epic 级**故事点(SP)**粗估
|
||||
- 分功能**正向**与**逆向/边界**
|
||||
- 至少 1~2 个 **mermaid** 流程图
|
||||
- **关键业务逻辑**(业务话)
|
||||
- 验收清单 + 「交付口径」一段话
|
||||
- 定稿后:**功能变更记录**(第 10 章)
|
||||
|
||||
**禁止写**
|
||||
|
||||
- 数据库表、字段名、接口路径、代码路径、组件名、存储 key
|
||||
- 研发实现指令(可写「正式环境由审批中心回写」这类业务依赖)
|
||||
- 在变更记录里写样式/UI/表结构优化
|
||||
|
||||
## 用户故事口径(强制 · 对齐业务条线说明)
|
||||
|
||||
真相源:原型 **业务条线说明**(`lease-business-line-overview` / `lines.ts`)。
|
||||
每条能力用「责任部门 → **起点** → **怎么运作** → **闭环**」叙述;**不要**用「作为…我希望…」宽表作主叙述。
|
||||
|
||||
| 块 | 写什么 |
|
||||
|----|--------|
|
||||
| 角色 | 谁负责、谁协同 |
|
||||
| 起点 | 谁在什么前提下启动 |
|
||||
| 怎么运作 | 有序步骤,含跨角色协作 |
|
||||
| 关键结果 | 可选标签 |
|
||||
| 闭环 | 业务终点与可追溯性 |
|
||||
|
||||
可选:`US-xx`、压缩句「作为…我想…以便…」、规模 S/M/L 或 SP(仅排期,不替代主叙述)。
|
||||
|
||||
## 输出结构
|
||||
|
||||
完整模板:[references/template.md](references/template.md)。
|
||||
|
||||
```markdown
|
||||
# <模块名> · 产品需求说明(全模块)
|
||||
|
||||
## 1. 一句话与目标
|
||||
## 2. 模块边界(最重要)
|
||||
## 3. 用户与角色
|
||||
## 4. 用户故事与故事点(业务条线说明口径)
|
||||
## 5. 功能模块说明(正向 / 逆向)
|
||||
## 6. 关键业务逻辑(必须对齐)
|
||||
## 7. 总览流程图
|
||||
## 8. 验收清单
|
||||
## 9. 交付口径
|
||||
## 10. 功能变更记录 ← 定稿后维护;日常改原型不强制每改必写
|
||||
```
|
||||
|
||||
## 质量自检
|
||||
|
||||
- [ ] 产品经理不看代码也能评审
|
||||
- [ ] 用户故事为起点 / 怎么运作 / 闭环
|
||||
- [ ] `.spec/requirements-prd.md` 已更新
|
||||
- [ ] 标注目录「产品需求说明(PRD)」已同步且正文一致
|
||||
- [ ] 定稿时:第 10 章 + `autoprd-baseline.json` 已更新
|
||||
- [ ] 推进至分析中/设计中/待开发时:同名任务已创建或复用,正式关联,负责人正确
|
||||
- [ ] 无表结构 / 接口 / 代码路径;变更记录无样式/UI 废话
|
||||
|
||||
## 参考
|
||||
|
||||
- 定稿变更日志:[references/release-changelog.md](references/release-changelog.md)
|
||||
- 云效阶段任务:[references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)
|
||||
- 标注同步细则:[references/annotation-sync.md](references/annotation-sync.md)
|
||||
- 章节模板:[references/template.md](references/template.md)
|
||||
- 故事示例:[references/granularity-example.md](references/granularity-example.md)
|
||||
- 业务条线:`src/prototypes/lease-business-line-overview/lines.ts`
|
||||
- 复杂判定规格:配合项目规则 `business-logic-documentation`
|
||||
- 云效组合:`$yunxiao-requirement-lifecycle`(建需求时先本 Skill)
|
||||
98
.claude/skills/oneos-autoprd/references/annotation-sync.md
Normal file
98
.claude/skills/oneos-autoprd/references/annotation-sync.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Axhub 标注目录同步(强制)
|
||||
|
||||
使用 OneOS AutoPRD 时,除资源库归档外,**必须**把 PRD 挂到 Axhub Make 标注工具的「原型目录」里,用户才能在右侧面板直接打开。
|
||||
|
||||
## 落点(三份必须一致)
|
||||
|
||||
| # | 路径 | 作用 |
|
||||
|---|------|------|
|
||||
| 1 | `src/prototypes/<prototype-id>/.spec/requirements-prd.md` | **主真相**:产品可读 PRD 全文 |
|
||||
| 2 | `src/prototypes/<prototype-id>/annotation-source.json` → **顶层** `directory.nodes` | **标注目录入口**:面板里可见的 Markdown/PRD |
|
||||
| 3 | `src/resources/prd/<prototype-id>-autoprd.md` | 资源库归档(可选但默认写) |
|
||||
|
||||
可选:复杂判定另写 `.spec/<topic>.md`,目录里再挂一条,并在 PRD 里链过去(与 `business-logic-documentation` 一致)。
|
||||
|
||||
## ⚠️ 目录必须写在文档顶层(常见踩坑)
|
||||
|
||||
Wire format(与 `prototype-annotation` 一致):
|
||||
|
||||
```json
|
||||
{
|
||||
"documentVersion": 1,
|
||||
"format": "axhub-annotation-source",
|
||||
"data": { "version": 2, "prototypeName": "...", "nodes": [] },
|
||||
"markdownMap": {},
|
||||
"assetMap": {},
|
||||
"directory": { "nodes": [] }
|
||||
}
|
||||
```
|
||||
|
||||
| 正确 | 错误 |
|
||||
|------|------|
|
||||
| 顶层 `directory.nodes` | 只写在 `data.directory` |
|
||||
| `PrototypeAnnotationHost` / Make 读的是 `source.directory` | 写在 `data` 下时右侧「原型目录」为空 |
|
||||
|
||||
若误写在 `data.directory`:迁移到顶层 `directory`,并删除 `data.directory`,避免双源。
|
||||
|
||||
## 目录节点写法(推荐)
|
||||
|
||||
在说明根 `folder` 的 `children` **靠前**插入或更新:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "<prefix>-doc-prd",
|
||||
"title": "产品需求说明(PRD)",
|
||||
"markdownPath": ".spec/requirements-prd.md",
|
||||
"markdown": "<与 .spec/requirements-prd.md 相同的全文>"
|
||||
}
|
||||
```
|
||||
|
||||
### 分章节 PRD(推荐,便于右侧目录浏览)
|
||||
|
||||
在「产品需求说明(PRD)」旁增加文件夹 **「PRD 分章」**,按 PRD 的 `##` 一级标题拆成多个 `markdown` 子节点(标题用章节名,正文为该章全文)。专题规格(`.spec/<topic>.md`)放在 **「专题规格」** 文件夹。
|
||||
|
||||
参考结构:
|
||||
|
||||
```text
|
||||
folder <模块>说明
|
||||
markdown 产品需求说明(PRD) ← 全文
|
||||
folder PRD 分章
|
||||
markdown 1. 一句话与目标
|
||||
markdown 2. 模块边界
|
||||
…
|
||||
folder 专题规格
|
||||
markdown <topic 标题>
|
||||
```
|
||||
|
||||
有 `scripts/sync-annotation-directory.mjs` 的原型:改 PRD / `.spec` 后**必须执行**该脚本再生目录。
|
||||
|
||||
定稿后第 10 章「功能变更记录」必须进入 PRD 全文节点;若有「PRD 分章」,同步该章节点。详见 [release-changelog.md](release-changelog.md)。
|
||||
|
||||
约定:
|
||||
|
||||
- `id`:`<短前缀>-doc-prd`(如 `ipc-doc-prd`、`wb-doc-prd`、`h5-va-doc-prd`)
|
||||
- `title`:优先「产品需求说明(PRD)」;已有「PRD」可保留不改名
|
||||
- **同时写** `markdownPath` + `markdown`:构建可内联路径,运行时目录也能直接读到正文
|
||||
- 若已有同 id / 同标题节点:只更新正文与 path,不新建重复入口
|
||||
- 遵守 `prototype-annotation-layout`:目录以 `markdown` 为主,不要用 `link`/`route` 顶替 PRD
|
||||
|
||||
## 增量改原型时怎么更新
|
||||
|
||||
1. 根据改动的文件路径确定 `<prototype-id>`。
|
||||
2. 读现有 `.spec/requirements-prd.md`(没有则按 AutoPRD 模板新建)。
|
||||
3. 只改受影响章节(故事、正逆向、关键逻辑、验收);其余保留。
|
||||
4. 回写 `.spec` + `resources/prd` + **顶层** `annotation-source.json` → `directory`(正文与文件一致;含分章节点)。
|
||||
5. 若原型有 `scripts/sync-annotation-directory.mjs`,改完后执行它。
|
||||
|
||||
## 可跳过全量重写的情况
|
||||
|
||||
纯样式、无文案/无流程/无判定变化的微调:不必重写整份 PRD,但若改了用户可见文案或交互结果,仍须同步对应段落与目录节点。
|
||||
|
||||
## 验收
|
||||
|
||||
- [ ] `.spec/requirements-prd.md` 存在且为最新
|
||||
- [ ] `annotation-source.json` **顶层**有 `directory.nodes`(不是只在 `data` 下)
|
||||
- [ ] 标注目录能打开「产品需求说明(PRD)」且内容与文件一致
|
||||
- [ ] 若有「PRD 分章」:各章可单独打开
|
||||
- [ ] 用户故事仍为业务条线说明口径(起点 / 怎么运作 / 闭环)
|
||||
@@ -0,0 +1,58 @@
|
||||
# 颗粒度标杆与用户故事口径
|
||||
|
||||
## 用户故事:对齐「业务条线说明」页面
|
||||
|
||||
页面:`/prototypes/lease-business-line-overview`
|
||||
数据:`src/prototypes/lease-business-line-overview/lines.ts`
|
||||
|
||||
每个能力卡片的产品叙述结构:
|
||||
|
||||
```text
|
||||
责任部门(roles)
|
||||
→ 起点(start)
|
||||
→ 怎么运作(process[],有序)
|
||||
→ 关键结果(outcomes,可选)
|
||||
→ 闭环(closure)
|
||||
```
|
||||
|
||||
AutoPRD 第 4 章必须用同一套结构写用户故事,而不是「作为…我希望…」宽表。
|
||||
|
||||
### 示例(保险采购 · 交车合规,示意)
|
||||
|
||||
#### US · 保险有效才允许交车
|
||||
- **角色**:采购部、运维部
|
||||
- **起点**:采购或运营维护车辆交强险与商业险台账,作为交车前置合规数据。
|
||||
- **怎么运作**:
|
||||
1. 在保险采购维护一车一档险种台账(录入 / 识别 / 导入)。
|
||||
2. 交车时系统校验交强 + 商业是否均为正常或临期。
|
||||
3. 不合规则拦截交车并给出可理解原因。
|
||||
- **关键结果**:`允许交车` / `禁止交车 · 保险异常`
|
||||
- **闭环**:交车合规与台账一致、可追溯;异常车辆不能进入履约运营。
|
||||
- **排期(可选)**:US-17 · 规模 M · 作为采购/运维,我想维护交强+商业险并在交车时校验,以便不合规车辆无法交车。
|
||||
|
||||
### 反例(不要作为主叙述)
|
||||
|
||||
| 编号 | 用户故事 | SP |
|
||||
|------|----------|----|
|
||||
| B1 | 作为运营,我能看到失败原因 | 2 |
|
||||
|
||||
↑ 信息太扁,缺少起点、协作步骤与闭环结果。
|
||||
|
||||
---
|
||||
|
||||
## AutoPRD 其余层次(仍要对齐)
|
||||
|
||||
1. 一句话 + 目标/非目标
|
||||
2. 模块边界(独立业务线写清「不自动同步」)
|
||||
3. 角色表(命名对齐业务条线)
|
||||
4. **用户故事:起点 / 怎么运作 / 闭环** + SP/规模
|
||||
5. 分模块正逆向
|
||||
6. 关键业务逻辑
|
||||
7. mermaid 流程图
|
||||
8. 验收清单
|
||||
9. 交付口径
|
||||
|
||||
## 刻意不写
|
||||
|
||||
- 表结构、接口、代码路径
|
||||
- 用实现指令代替业务闭环描述
|
||||
78
.claude/skills/oneos-autoprd/references/release-changelog.md
Normal file
78
.claude/skills/oneos-autoprd/references/release-changelog.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 功能变更记录(定稿触发)
|
||||
|
||||
用户回复**需求定稿关键字**后,必须生成「自上次定稿以来」的功能变更日志,追加到 PRD **最下方**,并更新定稿基线。
|
||||
|
||||
## 触发关键字(命中任一)
|
||||
|
||||
`需求定稿` · `定稿` · `确认定稿` · `本次定稿`
|
||||
|
||||
(可带模块名,如「保险采购需求定稿」。)
|
||||
|
||||
## 写什么 / 不写什么
|
||||
|
||||
| 要写(产品发版视角) | 不写 |
|
||||
|----------------------|------|
|
||||
| 哪个功能做了什么修改 | 布局、页面样式、UI 设计优化 |
|
||||
| 变更了什么业务逻辑 / 流程 / 验收口径 | 「优化了哪些表」「改了哪些字段/接口」 |
|
||||
| 用户可感知的交互结果变化 | 纯样式、动效、无语义文案微调 |
|
||||
|
||||
条目口吻示例:
|
||||
|
||||
- 「识别失败」:失败数可点击查看失败文件与原因;仅失败时也可打开明细
|
||||
- 「比价单」:审批通过后仍须在保单管理另行录入正式保单(强调不自动同步)
|
||||
|
||||
## 落点
|
||||
|
||||
1. **PRD 文末章节**(强制)
|
||||
`src/prototypes/<id>/.spec/requirements-prd.md` → `## 10. 功能变更记录`
|
||||
新定稿块插在该章**最上方**(倒序:最新在上);历史块保留。
|
||||
|
||||
2. **定稿基线**(强制)
|
||||
`src/prototypes/<id>/.spec/autoprd-baseline.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"prototypeId": "insurance-procurement",
|
||||
"lastConfirmedAt": "2026-07-21T12:00:00+08:00",
|
||||
"lastConfirmedLabel": "定稿 · 2026-07-21",
|
||||
"summaryBullets": ["…", "…"]
|
||||
}
|
||||
```
|
||||
|
||||
3. **标注目录**:同步更新「产品需求说明(PRD)」全文节点;若有「PRD 分章」,增加或更新「功能变更记录」分章。
|
||||
|
||||
4. **云效交接**(若本轮同时走云效):把**本次定稿块**正文作为需求描述「更新内容」;旧「更新内容」挪到「更新内容·历史」。
|
||||
|
||||
## 定稿工作流
|
||||
|
||||
1. 确认 `<prototype-id>`(从对话 / 打开文件 / 用户指名推断)。
|
||||
2. 读现有 PRD 与 `autoprd-baseline.json`(无基线则本轮视为**首版定稿**)。
|
||||
3. 收集自 `lastConfirmedAt` 以来的产品语义变更:
|
||||
对话确认口径 → 批注/操作说明 → PRD 新旧差异 →(可选)相关提交说明。
|
||||
过滤掉样式/表结构类改动。
|
||||
4. 若无功能增量:仍写定稿块,正文为「本轮无功能/逻辑增量(仅样式或未达产品语义变更)」或「首版定稿」。
|
||||
5. 追加 `### 定稿 · YYYY-MM-DD` + 条目列表到第 10 章顶部。
|
||||
6. 更新 `autoprd-baseline.json`;执行 annotation-sync。
|
||||
7. 向用户回报:基线时间、条目数、PRD 路径;若需发云效,提示可用口令。
|
||||
|
||||
## PRD 章节格式
|
||||
|
||||
```markdown
|
||||
## 10. 功能变更记录
|
||||
|
||||
> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。
|
||||
|
||||
### 定稿 · 2026-07-21
|
||||
|
||||
- 「能力名」:做了什么 / 逻辑如何变
|
||||
- …
|
||||
|
||||
### 定稿 · 2026-07-10
|
||||
|
||||
- …
|
||||
```
|
||||
|
||||
## 与日常改原型的关系
|
||||
|
||||
- **改原型过程中**:按 AutoPRD 主流程增量更新第 1–9 章;**不必**每改一次就写第 10 章。
|
||||
- **用户说定稿时**:才汇总写入第 10 章并刷新基线。
|
||||
163
.claude/skills/oneos-autoprd/references/template.md
Normal file
163
.claude/skills/oneos-autoprd/references/template.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# OneOS AutoPRD 输出模板
|
||||
|
||||
复制下列结构填写。方括号为占位。**第 4 章用户故事必须用业务条线说明口径。**
|
||||
|
||||
```markdown
|
||||
# <模块名> · 产品需求说明(全模块)
|
||||
|
||||
## 1. 一句话与目标
|
||||
|
||||
**一句话**
|
||||
<用一句话说明模块解决什么>
|
||||
|
||||
**要解决的问题**
|
||||
- …
|
||||
|
||||
**本期目标**
|
||||
1. …
|
||||
2. …
|
||||
|
||||
**非目标(本期不做)**
|
||||
- …
|
||||
|
||||
## 2. 模块边界(最重要)
|
||||
|
||||
(可选 mermaid:模块与外部系统关系)
|
||||
|
||||
| 业务线 / 子域 | 做什么 | 不做什么 |
|
||||
|---------------|--------|----------|
|
||||
| … | … | … |
|
||||
|
||||
**外部依赖(产品口径)**
|
||||
| 依赖方 | 交互方式 | 产品要求 |
|
||||
|--------|----------|----------|
|
||||
| … | … | … |
|
||||
|
||||
**关键约束**
|
||||
- …
|
||||
|
||||
## 3. 用户与角色
|
||||
|
||||
| 角色 | 主要目标 |
|
||||
|------|----------|
|
||||
| … | … |
|
||||
|
||||
> 角色名优先与「业务条线说明」一致(如业务管理组、采购部、运维部)。
|
||||
|
||||
## 4. 用户故事与故事点(业务条线说明口径)
|
||||
|
||||
> 故事点 / 规模供排期参考,可按团队基准调整。合计约 **N SP**(或 S/M/L 汇总说明)。
|
||||
|
||||
按 Epic 或能力单元分组。**每一条**按下面四块写(对齐业务条线说明页:责任部门 → 起点 → 怎么运作 → 闭环):
|
||||
|
||||
### Epic A · <名称>(约 N SP)
|
||||
|
||||
#### A1 · <能力标题>
|
||||
- **角色**:…
|
||||
- **起点**:…
|
||||
- **怎么运作**:
|
||||
1. …
|
||||
2. …
|
||||
3. …
|
||||
- **关键结果**(可选):`可…` / `禁止…`
|
||||
- **闭环**:…
|
||||
- **排期(可选)**:US-xx · 规模 S/M/L · 「作为…,我想…,以便…」
|
||||
|
||||
#### A2 · <能力标题>
|
||||
…
|
||||
|
||||
### Epic B · …
|
||||
|
||||
## 5. 功能模块说明(正向 / 逆向)
|
||||
|
||||
### 5.x <功能名>
|
||||
|
||||
**正向**
|
||||
1. …
|
||||
2. …
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| … | … |
|
||||
|
||||
## 6. 关键业务逻辑(必须对齐)
|
||||
|
||||
### 6.x <规则名>
|
||||
|
||||
用编号优先级、表格或短列表写清业务规则。
|
||||
|
||||
## 7. 总览流程图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
home[入口] --> a[能力A]
|
||||
home --> b[能力B]
|
||||
```
|
||||
|
||||
## 8. 验收清单(产品 / 测试)
|
||||
|
||||
**子域 A**
|
||||
- [ ] …
|
||||
|
||||
## 9. 交付口径
|
||||
|
||||
> <一段可贴需求单开头的浓缩描述>
|
||||
|
||||
## 10. 功能变更记录
|
||||
|
||||
> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。
|
||||
> 由「需求定稿」关键字触发追加;日常改原型不强制每改必写。详见 [release-changelog.md](release-changelog.md)。
|
||||
|
||||
### 定稿 · YYYY-MM-DD
|
||||
|
||||
- 「<能力名>」:<做了什么 / 逻辑如何变>
|
||||
- …
|
||||
```
|
||||
|
||||
成文后**必须**同步 Axhub 标注目录(见 [annotation-sync.md](annotation-sync.md)):
|
||||
|
||||
1. `src/prototypes/<id>/.spec/requirements-prd.md`
|
||||
2. `annotation-source.json` → 目录节点「产品需求说明(PRD)」(`markdownPath` + `markdown`)
|
||||
3. `src/resources/prd/<id>-autoprd.md`
|
||||
4. 定稿时另写:`src/prototypes/<id>/.spec/autoprd-baseline.json`
|
||||
|
||||
## 用户故事写法(对照业务条线说明)
|
||||
|
||||
业务条线说明页字段 → AutoPRD 字段:
|
||||
|
||||
| 页面 | AutoPRD |
|
||||
|------|---------|
|
||||
| 责任部门标签 | **角色** |
|
||||
| 起点 | **起点** |
|
||||
| 怎么运作(有序列表) | **怎么运作** |
|
||||
| 结果色签 | **关键结果**(可选) |
|
||||
| 闭环 | **闭环** |
|
||||
|
||||
示例口吻(摘自业务条线风格,非照抄某模块):
|
||||
|
||||
- **起点**:采购部维护交强与商业险台账,作为交车合规的前置数据。
|
||||
- **怎么运作**:1. 运营录入或识别保单… 2. 运维交车时校验…
|
||||
- **闭环**:核心险种有效则允许交车;异常则拦截并提示原因,过程可追溯。
|
||||
|
||||
**不要**用下面这种宽表作为第 4 章唯一形态:
|
||||
|
||||
| 编号 | 作为…我希望… | SP |
|
||||
|------|--------------|----|
|
||||
|
||||
「作为…我想…以便…」只允许作为**排期可选一行**,主叙述必须是起点/怎么运作/闭环。
|
||||
|
||||
## 正逆向写法约定
|
||||
|
||||
- **正向**:用户成功完成任务的最短路径
|
||||
- **逆向**:取消、关闭、禁用、冲突状态、不可操作项
|
||||
- 审批类:写清「本页只读 / 在外部系统办理」
|
||||
|
||||
## 规模与故事点
|
||||
|
||||
| 规模 | 建议含义 | 约 SP |
|
||||
|------|----------|-------|
|
||||
| S | 轻交互 / 列表展示 | 1~2 |
|
||||
| M | 标准流程 + 筛选详情 | 3~5 |
|
||||
| L | 状态机 / 跨模块 / 批量识别 | 6+ |
|
||||
@@ -0,0 +1,44 @@
|
||||
# 云效阶段任务自动创建(AutoPRD 负责)
|
||||
|
||||
本规则写在 **oneos-autoprd**,不依赖改写 `yunxiao-requirement-lifecycle`。
|
||||
凡本 Skill 参与的云效操作中,需求推进到下列状态时,**同一轮必须**建同名任务并正式关联需求。
|
||||
|
||||
## 触发状态
|
||||
|
||||
| 需求推进至 | 任务标签(右侧基础字段) | 负责人 | 任务标题 |
|
||||
|------------|--------------------------|--------|----------|
|
||||
| **分析中** | `分析` | **需求创建人**姓名 | 与需求标题**完全同名** |
|
||||
| **设计中** | `设计` | **需求创建人**姓名 | 与需求标题**完全同名** |
|
||||
| **待开发** | `交付`(若项目无「交付」标签则用 `开发`) | **何斐** | 与需求标题**完全同名** |
|
||||
|
||||
## 强制字段与关系
|
||||
|
||||
1. **正式关联需求**:父子或关联项;只挂标题不算关联。创建后必须在界面上能看到需求 ↔ 任务关系。
|
||||
2. **标签**:写入任务「标签」基础字段(见上表);标题前缀不能代替标签。
|
||||
3. **创建时间沿用需求**:任务的创建时间(及标签侧可见的创建时间口径)尽量与**需求创建时间**一致。
|
||||
- 平台允许改创建时间 / 用 API 指定时:写成需求的创建时间。
|
||||
- 平台不允许:在任务描述首行注明 `创建时间口径=需求创建时间(YYYY-MM-DD HH:mm)`,并在回报里说明「平台限制未改系统创建时间」。
|
||||
4. **幂等**:同一需求、同一阶段标签下,已存在**未取消**且已正式关联的同名任务 → **不新建**,只校验负责人/标签/关联是否正确,缺则补齐。
|
||||
5. **顺序**:先确认需求已进入目标状态(或本轮动作将写入该状态),再创建/补齐任务;回报时给出任务编号与链接。
|
||||
|
||||
## 执行步骤(apply 时)
|
||||
|
||||
```text
|
||||
1. 读取需求:标题、创建人、创建时间、当前/目标状态、已关联任务列表
|
||||
2. 按上表确定本轮阶段标签与负责人
|
||||
3. 查重(需求ID + 阶段标签 + 未取消)
|
||||
4. 无则创建:标题=需求标题;标签=阶段标签;负责人=上表;创建时间口径=需求创建时间;正式关联需求
|
||||
5. 有则校验并修补标签/负责人/关联
|
||||
6. 回报:需求状态、任务编号、负责人、是否新建/复用
|
||||
```
|
||||
|
||||
## 不在本规则内
|
||||
|
||||
- 不自动建「测试 / 发版」任务(除非用户另行要求)。
|
||||
- 不因样式类 PRD 变更触发建任务;仅**需求状态**进入分析中/设计中/待开发时触发。
|
||||
- 云效生命周期细则、流水线、缺陷闭环仍可由 `$yunxiao-requirement-lifecycle` 配合执行;**本建任务逻辑以 AutoPRD 本文为准**。
|
||||
|
||||
## 与定稿 / 需求说明的关系
|
||||
|
||||
- 写「需求说明 / 更新内容」仍按 AutoPRD 主流程与定稿流程。
|
||||
- 推进到待开发且需要描述时:先 AutoPRD 文档,再改状态,再按本文件建任务(负责人何斐)。
|
||||
42
.cursor/rules/oneos-autoprd-sync.mdc
Normal file
42
.cursor/rules/oneos-autoprd-sync.mdc
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
description: 改原型跟进 AutoPRD;定稿写功能变更;云效建需求先跑 AutoPRD;状态推进建同名任务
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# OneOS AutoPRD 全局同步
|
||||
|
||||
## 改原型
|
||||
|
||||
修改 `src/prototypes/**` 且涉及**行为、文案、流程、判定或验收**时,同一轮必须跟进 **oneos-autoprd**:
|
||||
|
||||
1. 更新 `.spec/requirements-prd.md`(用户故事:起点 → 怎么运作 → 闭环)。
|
||||
2. 更新 `annotation-source.json` **顶层** `directory.nodes` 中「产品需求说明(PRD)」。
|
||||
3. 同步 `src/resources/prd/<id>-autoprd.md`(若有)。
|
||||
|
||||
纯样式且无产品语义变化:可跳过全量重写。
|
||||
|
||||
## 需求定稿
|
||||
|
||||
用户回复含 `需求定稿` / `定稿` / `确认定稿` / `本次定稿` 时:
|
||||
|
||||
1. 按 skill `references/release-changelog.md` 汇总**自上次定稿**的功能/逻辑变更。
|
||||
2. 追加到 PRD `## 10. 功能变更记录`(最新在上);更新 `.spec/autoprd-baseline.json`。
|
||||
3. **不要**写布局/UI/表结构类条目。
|
||||
|
||||
## 云效建需求
|
||||
|
||||
使用 `yunxiao-requirement-lifecycle` **创建或完善**需求描述时,**先**跑 `oneos-autoprd`:
|
||||
|
||||
- 需求说明 ← AutoPRD 正文
|
||||
- 更新内容 ← 第 10 章自上次定稿以来的条目(无则首版/无增量)
|
||||
- 旧更新内容 →「更新内容·历史」
|
||||
|
||||
## 云效状态 → 同名任务(AutoPRD 规则,不改云效 Skill)
|
||||
|
||||
需求推进至 **分析中 / 设计中 / 待开发** 时,按 `references/yunxiao-stage-tasks.md`:
|
||||
|
||||
- 建与需求**同名**任务并正式关联
|
||||
- 标签:分析 / 设计 / 交付(或开发)
|
||||
- 创建时间口径沿用需求
|
||||
- 分析中、设计中负责人=创建人;待开发负责人=**何斐**
|
||||
- 同阶段已有未取消关联任务则复用,不重复建
|
||||
17
.cursor/rules/yunxiao-record-requirement-fast-path.mdc
Normal file
17
.cursor/rules/yunxiao-record-requirement-fast-path.mdc
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: 云效「记录需求」须先点选优先级/推进至;统一运营管理平台走快路径,禁止默认参数与盲目探测
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 云效记录需求 · 参数门禁与快路径
|
||||
|
||||
当用户使用 `$yunxiao-requirement-lifecycle` / 「记录需求」,且涉及优先级、推进至时:
|
||||
|
||||
1. **必须先拿到明确选择**:优先级(紧急/高/中/低)、推进至(分析中/设计中/设计完成/开发中)、**标签(从云效标签 catalog 点选,可多选)**。优先 `AskQuestion`;不可用则用 Plan 确认并由用户写明 A+B+C。
|
||||
2. **禁止**在未选择时默认「中 + 分析中」并自动建单。「批准/Implement 计划」≠ 已完成点选。
|
||||
3. 项目为**统一运营管理平台**(含 PC 端别名)时:先读
|
||||
`.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-fast-path.md` 与
|
||||
`.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-runtime-ids.json`(标签清单见同目录 `oneos-pc-tag-catalog.md`),按已验证 API/ID 执行;禁止重复探测 create URL。
|
||||
4. **标签禁止**按 `lines.ts` / 业务条线说明自动推断;只能用用户点选的 catalog 名。API 打标失败时 UI 兜底;仍失败则停下请用户回复标签名,不得声称已成功。
|
||||
5. 优先 API(创建时写入描述;标签用 `PATCH … propertyKey=tag`);浏览器仅作标签失败或状态连跳兜底;少截图、同失败不循环重试。
|
||||
139
.cursor/skills/AutoVUL/SKILL.md
Normal file
139
.cursor/skills/AutoVUL/SKILL.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: AutoVUL
|
||||
description: >-
|
||||
Generates OneOS PC version update logs from a Yunxiao (云效) iteration name by
|
||||
loading all linked requirements, or from a pasted changelog list. Use when testers
|
||||
or PMs say AutoVUL, 版本更新日志, 按迭代生成更新日志, 发版公告, or give a 迭代名称.
|
||||
---
|
||||
|
||||
# AutoVUL(OneOS PC 版本更新日志)
|
||||
|
||||
面向**测试人员发版**:输入云效**迭代名称** → 拉取该迭代关联需求 → 自动生成对外版本更新日志。
|
||||
|
||||
也可手动粘贴变更清单(兜底)。**预计维护时长**由人工单独通知,不写进日志。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 测试说:`按 $AutoVUL` / `生成版本更新日志` / `按迭代 xxx 出更新日志`
|
||||
- 输入含云效「迭代名称」
|
||||
- 发版前需要统一文案口径
|
||||
|
||||
## 主流程(测试 · 按迭代,默认)
|
||||
|
||||
完整取数步骤见 [references/yunxiao-sprint.md](references/yunxiao-sprint.md)。
|
||||
|
||||
```text
|
||||
1. 确认云效项目(缺省:统一运营管理平台PC端;可改)
|
||||
2. 向用户索取「迭代名称」(若口令里已带则直接用)
|
||||
3. 在云效查找该迭代 → 立即反馈成功或失败(见下)
|
||||
4. 成功:列出迭代内关联「需求」清单(编号 + 标题 + 状态)供用户快速核对
|
||||
5. 从每条需求提取变更素材(优先级见「素材优先级」)
|
||||
6. 按本文格式分类、合并、润色 → 输出成稿正文
|
||||
```
|
||||
|
||||
### 迭代读取 · 成功 / 失败反馈(强制)
|
||||
|
||||
查找迭代后**必须先反馈**,再继续或停止:
|
||||
|
||||
| 结果 | 反馈文案(可微调,语义不变) | 下一步 |
|
||||
|------|------------------------------|--------|
|
||||
| 精确命中 1 个迭代 | `✅ 已找到迭代「{名称}」(状态:{状态}),关联需求 {N} 条。` | 继续拉需求并生成 |
|
||||
| 0 个匹配 | `❌ 未找到迭代「{输入}」。请核对后重新输入迭代名称(可复制云效迭代标题)。` | **停止生成**;等待用户重输 |
|
||||
| 多个模糊匹配 | `⚠️ 找到多个相似迭代:1)… 2)… 请回复序号或完整精确名称。` | 等用户选定后再继续 |
|
||||
| 登录/权限失败 | `❌ 无法访问云效(未登录或无权限)。请在浏览器登录云效后重试,或改用手动清单。` | 停止;提示登录或兜底 |
|
||||
| 迭代存在但 0 条需求 | `⚠️ 迭代「{名称}」已找到,但暂无关联需求。请确认规划后再试,或改用手动清单。` | 停止生成对外正文 |
|
||||
|
||||
**失败后**:用户只需再发一次迭代名称(或纠正项目名),从步骤 2 重跑;不要要求用户重装 Skill。
|
||||
|
||||
### 素材优先级(每条需求)
|
||||
|
||||
| 顺序 | 来源 | 用法 |
|
||||
|------|------|------|
|
||||
| 1 | 需求描述中的「更新内容」段(非「更新内容·历史」) | 首选,直接改写成日志条目 |
|
||||
| 2 | AutoPRD「功能变更记录」中可对外部分 | 次选 |
|
||||
| 3 | 需求标题 + 简短描述摘要 | 仅当前两者皆空时;价值句可合理补全 |
|
||||
| — | 纯技术债、内部监控、未对用户开放 | **不写入**对外日志 |
|
||||
|
||||
Bug 类需求(类型/标签含缺陷修复、或更新内容写「修复」)归入【Bug修复】;其余归【新功能】。边界不清优先 Bug修复。
|
||||
|
||||
### 版本号与更新时间
|
||||
|
||||
| 字段 | 规则 |
|
||||
|------|------|
|
||||
| 版本号 | 优先从迭代名解析(如含 `V1.1.5` / `1.1.5`);解析不到再问用户 |
|
||||
| 更新时间 | 用户提供则用;未提供可写迭代计划开始日的 `MM月DD日HH:MM`,或先问 |
|
||||
| 产品线 | 默认 `OneOS PC版` |
|
||||
|
||||
## 兜底流程(手动清单)
|
||||
|
||||
无云效权限、或迭代为空时:用 [input-template.md](input-template.md) 粘贴清单,跳过迭代步骤,直接分类润色。
|
||||
|
||||
## 固定输出格式
|
||||
|
||||
```text
|
||||
OneOS PC版V{主.次.修订}更新日志:
|
||||
更新时间:{MM}月{DD}日{HH}:{MM}
|
||||
----------------------------------------------------
|
||||
【新功能】
|
||||
1「{模块名}」{做了什么},{责任部门}可{用户价值 / 解决的问题}
|
||||
2「{模块名}」{…}
|
||||
|
||||
【Bug修复】
|
||||
3「{模块名}」{修复了什么},{责任部门}可{恢复的能力 / 避免的影响}
|
||||
```
|
||||
|
||||
### 版式硬约束
|
||||
|
||||
| 项 | 规则 |
|
||||
|----|------|
|
||||
| 标题 | `OneOS PC版V` + 版本号 + `更新日志:`(如 `V1.1.5`,无空格) |
|
||||
| 更新时间 | 仅开始时刻;`MM月DD日HH:MM`;**禁止**「预计x分钟内完成」 |
|
||||
| 分隔线 | `----------------------------------------------------` |
|
||||
| 分区 | 仅 `【新功能】` / `【Bug修复】`;空类整段省略 |
|
||||
| 序号 | 全篇连续,从 1 起 |
|
||||
| 模块名 | `「」`;与产品菜单正式名一致 |
|
||||
| 对外正文 | **不写**需求号、缺陷号、迭代 ID(核对清单可另附) |
|
||||
| 结尾 | 无签名、无「感谢使用」、无技术附录 |
|
||||
|
||||
## 分类 / 合并 / 文案(成稿规则)
|
||||
|
||||
| 归入【新功能】 | 归入【Bug修复】 | 不写进对外日志 |
|
||||
|----------------|-----------------|----------------|
|
||||
| 新增能力、新入口、新字段/流程、可感知体验优化 | 原有能力异常、报错、数据错、交互失效 | 技术债、无感性能、未开放灰度、仅内部监控 |
|
||||
|
||||
- **同分区同模块**合并为一条,分点用 `;`
|
||||
- **跨分区**同模块分栏各写一条
|
||||
- 每条含:做什么 + 用户价值 + 责任部门自然嵌入(`{部门}人员可…`)
|
||||
- **禁止**「供…条线…使用」;部门对照 [module-role-mapping.md](module-role-mapping.md)
|
||||
- 禁止编造迭代内不存在的能力;存疑跳过或标待确认(不进正式成稿)
|
||||
- 单条建议 ≤100 字
|
||||
|
||||
排序:先新功能后修复;同分区全员影响优先;资金/合规修复置修复区最前。
|
||||
|
||||
## 输出纪律
|
||||
|
||||
1. **先**输出迭代读取反馈(✅/❌/⚠️)
|
||||
2. 成功时:可先给「本迭代需求清单」短表(含编号,便于测试核对)
|
||||
3. 再输出**纯文本成稿正文**(默认不要 Markdown 大标题包裹成稿)
|
||||
4. 用户明确要求时,可在正文后附「分类依据 / 跳过项」
|
||||
|
||||
## 发布前自检
|
||||
|
||||
- [ ] 已反馈迭代查找结果;失败时未强行编造成稿
|
||||
- [ ] 成稿条目均可追溯到本迭代关联需求素材
|
||||
- [ ] 标题、时间行、分隔线正确;无预计时长
|
||||
- [ ] 同模块合并正确;每条含价值 + 部门自然嵌入
|
||||
- [ ] 对外正文无需求号/缺陷号/技术黑话;序号连续
|
||||
|
||||
## 给测试同事的口令
|
||||
|
||||
```text
|
||||
按 $AutoVUL 生成版本更新日志。
|
||||
项目:统一运营管理平台PC端
|
||||
迭代名称:V1.1.5发版迭代
|
||||
更新时间:07月16日16:00
|
||||
```
|
||||
|
||||
迭代名写错时,按提示改完再发一次即可。
|
||||
|
||||
手动兜底见 [input-template.md](input-template.md);成稿范例见 [examples.md](examples.md)。
|
||||
77
.cursor/skills/AutoVUL/examples.md
Normal file
77
.cursor/skills/AutoVUL/examples.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# AutoVUL 成稿与交互范例
|
||||
|
||||
## 1. 迭代读取失败 → 重输
|
||||
|
||||
**用户:**
|
||||
|
||||
```text
|
||||
按 $AutoVUL,迭代名称:V1.1.5
|
||||
```
|
||||
|
||||
**Agent:**
|
||||
|
||||
```text
|
||||
❌ 未找到迭代「V1.1.5」。请核对后重新输入迭代名称(可复制云效迭代标题)。
|
||||
```
|
||||
|
||||
**用户:**
|
||||
|
||||
```text
|
||||
V1.1.5发版迭代
|
||||
```
|
||||
|
||||
**Agent:**
|
||||
|
||||
```text
|
||||
✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 3 条:
|
||||
1. ONEOSP-201 车辆管理运营城市展示优化
|
||||
2. ONEOSP-202 还车应结款租金计算修复
|
||||
3. ONEOSP-203 业务管理新增租赁物流统计
|
||||
|
||||
OneOS PC版V1.1.5更新日志:
|
||||
更新时间:07月16日16:00
|
||||
----------------------------------------------------
|
||||
【新功能】
|
||||
1「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源
|
||||
2「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况
|
||||
|
||||
【Bug修复】
|
||||
3「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确
|
||||
```
|
||||
|
||||
## 2. 完整成稿风格基准(手动/迭代素材均可)
|
||||
|
||||
> 责任部门对照:`module-role-mapping.md`(OneOS 业务条线说明)
|
||||
|
||||
```text
|
||||
OneOS PC版V1.1.5更新日志:
|
||||
更新时间:07月16日16:00
|
||||
----------------------------------------------------
|
||||
【新功能】
|
||||
1「加氢站管理」升级新版加氢站模块,采购部可统一维护签约站与非签约站信息,签约加氢站可便捷管理站点日常运营
|
||||
2「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源
|
||||
3「审批中心」支持按客户名称搜索关联合同,业务员、法务部审批时可快速定位合同;还车应结款添加照片附件后,业务管理组、安全部、运维部、能源部会签人员可在审批流中查看完整附件
|
||||
4「保险采购」支持保比价单按车辆单独勾选并分开提交审批,运维部、安全部人员可按车辆分批推进投保审批
|
||||
5「故障管理」增加「故障来源」字段展示,运维部人员可区分上报渠道并跟进处理
|
||||
6「证照管理」特种设备使用登记证/使用标识照片支持OCR识别,运维部、安全部人员可减少手工录入、加快证照建档
|
||||
7「全局显示」统一各模块列表分页样式与交互,各模块操作人员跨页面浏览时体验一致、上手更快
|
||||
8「交还车管理」支持交车/还车签章文件在线预览,运维部、业务管理组、司机可现场核对签章文件;提交时增加胎纹数据必填校验,减少漏填导致的后续结算争议
|
||||
9「还车应结款」调整安全部待办区域权限,安全部人员仅处理职责范围内的还车应结款审批
|
||||
10「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况
|
||||
|
||||
【Bug修复】
|
||||
11「还车管理」修复签字单与还车检查单/还车应结款轮胎数据不一致,运维部、业务管理组人员还车验收与结算依据保持一致
|
||||
12「租赁合同」修复编辑合同提交时异常提示内容冗余,业务员、法务部人员可快速定位真正错误原因
|
||||
13「交还车管理」修复交车/还车签章客户名称关联错误,避免运维部、业务管理组、司机现场核对时客户信息张冠李戴
|
||||
14「车辆氢费明细」修复同客户不同生效时段、以及不同客户成本单价显示异常,能源部人员可准确核对氢费核算单价
|
||||
15「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确
|
||||
```
|
||||
|
||||
## 对照要点
|
||||
|
||||
| 规则 | 体现 |
|
||||
|------|------|
|
||||
| 先反馈迭代结果 | 失败可重输;成功列出需求再成稿 |
|
||||
| 无预计时长 | 更新时间仅 `07月16日16:00` |
|
||||
| 对外无需求号 | 成稿正文不出现 ONEOSP-xxx |
|
||||
| 部门自然嵌入 | `运维部人员可…`,无「供…条线使用」 |
|
||||
69
.cursor/skills/AutoVUL/input-template.md
Normal file
69
.cursor/skills/AutoVUL/input-template.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# AutoVUL · 输入模板
|
||||
|
||||
## A. 测试人员 · 按云效迭代(推荐)
|
||||
|
||||
复制下面口令发给 AI(改迭代名即可):
|
||||
|
||||
```text
|
||||
按 $AutoVUL 生成版本更新日志。
|
||||
项目:统一运营管理平台PC端
|
||||
迭代名称:
|
||||
更新时间:
|
||||
```
|
||||
|
||||
### 期望反馈
|
||||
|
||||
- 成功:`✅ 已找到迭代「…」,关联需求 N 条` → 随后输出成稿
|
||||
- 失败:`❌ 未找到迭代「…」。请重新输入迭代名称` → 改名后再发一次即可
|
||||
|
||||
**说明**:预计维护时长由人工单独通知,不写入版本更新日志。
|
||||
|
||||
---
|
||||
|
||||
## B. 手动清单(云效不可用时兜底)
|
||||
|
||||
### 版本元信息
|
||||
|
||||
| 字段 | 填写 | 示例 |
|
||||
|------|------|------|
|
||||
| 产品线 | | OneOS PC版 |
|
||||
| 版本号 | | V1.1.5 |
|
||||
| 更新开始时间 | | 07月16日16:00 |
|
||||
| 发布范围 | 全量 / 灰度(说明) | 全量 |
|
||||
| 是否对外发布 | 是 / 否 | 是 |
|
||||
|
||||
### 新功能(原始材料,可粗糙)
|
||||
|
||||
| 模块 | 做了什么 | 解决什么问题 / 用户故事 | 业务条线 | 责任部门 | 是否写入日志 |
|
||||
|------|----------|-------------------------|----------|----------|--------------|
|
||||
| 例:加氢站管理 | 升级新版模块 | 统一维护站点 | 能源业务条线 | 采购部、加氢站 | 是 |
|
||||
| | | | | | |
|
||||
|
||||
### Bug修复(原始材料,可粗糙)
|
||||
|
||||
| 模块 | 原问题现象 | 修复后表现 | 对用户的影响 | 业务条线 | 责任部门 | 是否写入日志 |
|
||||
|------|------------|------------|--------------|----------|----------|--------------|
|
||||
| 例:还车应结款 | 租金费用项重复 | 金额正确 | 避免结算偏差 | 租赁业务条线 | 业务管理组、安全部、运维部、业务管理组-能源部 | 是 |
|
||||
| | | | | | | |
|
||||
|
||||
### 明确不要写入对外日志
|
||||
|
||||
- (如:重构、依赖升级、仅内部监控、未上线配置)
|
||||
|
||||
### 粘贴给 AI
|
||||
|
||||
```text
|
||||
请按 $AutoVUL 用下方手动清单生成成稿(跳过云效迭代)。
|
||||
要求:同模块合并;每条含「做什么 + 用户价值」;责任部门自然融入;只输出正文。
|
||||
|
||||
【版本元信息】
|
||||
- 产品线:OneOS PC版
|
||||
- 版本号:V
|
||||
- 更新开始时间:
|
||||
|
||||
【新功能】
|
||||
(粘贴)
|
||||
|
||||
【Bug修复】
|
||||
(粘贴)
|
||||
```
|
||||
60
.cursor/skills/AutoVUL/module-role-mapping.md
Normal file
60
.cursor/skills/AutoVUL/module-role-mapping.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 模块 → 业务条线 / 责任部门对照
|
||||
|
||||
> 数据源:OneOS「业务条线说明」页责任部门定义(租赁 / 能源 / 运维 / 安全 / 自营)
|
||||
> 生成更新日志时,**适用对象**须从此表取 `roles`,不得自造部门名。
|
||||
|
||||
## 五条业务条线
|
||||
|
||||
| ID | 条线名称 |
|
||||
|----|----------|
|
||||
| lease | 租赁业务条线 |
|
||||
| energy | 能源业务条线 |
|
||||
| ops | 运维管理条线 |
|
||||
| safety | 安全管理条线 |
|
||||
| self-operated | 自营业务条线 |
|
||||
|
||||
## 本版变更模块对照
|
||||
|
||||
| 更新日志模块名 | 业务条线 | lines.ts 模块 | 责任部门(roles,原文) |
|
||||
|----------------|----------|---------------|-------------------------|
|
||||
| 加氢站管理 | 能源业务条线 | 加氢站管理 | 采购部、加氢站 |
|
||||
| 车辆管理 | 运维管理条线 | 验车入库 / 车辆管理(资产底座) | 运维部、采购部 |
|
||||
| 审批中心 | 跨条线 | 租赁合同 + 还车应结款(审批场景) | 业务员、法务部、业务管理组、安全部、运维部、业务管理组-能源部 |
|
||||
| 保险采购 | 运维管理条线 | 证照 / 保险(车辆资产关联) | 运维部、安全部 |
|
||||
| 故障管理 | 运维管理条线 | 故障管理 | 运维部、司机 |
|
||||
| 租赁合同 | 租赁业务条线 | 租赁合同 | 业务员、法务部 |
|
||||
| 证照管理 | 运维管理条线 | 证照管理 | 运维部、安全部 |
|
||||
| 全局显示 | 全系统 | 系统介绍(五条条线) | 业务管理组、运维部、采购部、安全部、法务部、财务部、业务管理组-能源部、业务服务组 |
|
||||
| 交还车管理 | 运维管理条线 | 交车管理 + 还车管理 | 运维部、业务管理组、司机 |
|
||||
| 还车应结款 | 租赁业务条线 | 还车应结款 | 业务管理组、安全部、运维部、业务管理组-能源部 |
|
||||
| 还车管理 | 运维管理条线 | 还车管理 | 运维部、业务管理组 |
|
||||
| 车辆氢费明细 | 能源业务条线 | 车辆氢费明细 | 业务管理组-能源部 |
|
||||
| 业务管理 · 租赁统计 | 租赁业务条线 | 租赁业务台账 / 客户管理 | 业务管理组、业务员 |
|
||||
| 业务管理 · 物流统计 | 自营业务条线 | 自营台账 | 业务服务组、财务部 |
|
||||
|
||||
## 写法规则
|
||||
|
||||
- 部门名与 `roles` **完全一致**(`业务管理组-能源部` 对外可简写「能源部人员」)
|
||||
- **禁止**「供…业务条线…使用」「供…人员使用」
|
||||
- **推荐**:`{部门}人员可…` / `{部门}可…` / `{部门}审批时可…`
|
||||
- 同模块合并后,多部门用顿号或分号自然串联
|
||||
- 找不到模块时:查 `lines.ts`;仍无则标「待产品确认」不进成稿
|
||||
|
||||
## 自然表述示例
|
||||
|
||||
| 模块 | 生硬(禁止) | 自然(推荐) |
|
||||
|------|--------------|--------------|
|
||||
| 车辆管理 | 供运维管理条线运维部使用 | 运维部人员可快速辨认车辆所属运营区域 |
|
||||
| 加氢站管理 | 供能源业务条线采购部、加氢站使用 | 采购部可统一维护站点信息,加氢站可便捷管理日常运营 |
|
||||
| 还车应结款 | 供租赁业务条线业务管理组…使用 | 业务管理组、安全部、运维部、能源部还车结算金额更准确 |
|
||||
|
||||
## 常见错误(禁止)
|
||||
|
||||
| 错误写法 | 正确写法 |
|
||||
|----------|----------|
|
||||
| 氢能运营业务人员 | 采购部、加氢站(能源 · 加氢站管理) |
|
||||
| 车辆运营及资产管理业务人员 | 运维部、采购部(运维 · 车辆管理) |
|
||||
| 各业务条线审批人员 | 业务员、法务部、…(按审批场景枚举) |
|
||||
| 保险采购业务人员 | 运维部、安全部 |
|
||||
| 还车结算业务人员 | 业务管理组、安全部、运维部、业务管理组-能源部 |
|
||||
| 经营分析及管理部门 | 业务管理组、业务服务组(分租赁 / 自营) |
|
||||
81
.cursor/skills/AutoVUL/references/yunxiao-sprint.md
Normal file
81
.cursor/skills/AutoVUL/references/yunxiao-sprint.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 云效迭代 → 需求清单(AutoVUL 取数)
|
||||
|
||||
测试人员只提供**迭代名称**;Agent 负责在云效定位迭代、拉取关联需求、抽出更新素材。
|
||||
|
||||
## 安全边界(强制)
|
||||
|
||||
- **禁止**在对话、Skill、日志、成稿中写入 Token、密码、Cookie、私钥。
|
||||
- 优先复用**已登录浏览器会话**操作云效 Projex(与 `yunxiao-requirement-lifecycle` 一致)。
|
||||
- 若团队已配置本机环境变量访问 OpenAPI,可走 API;Token 只读自环境,永不回显。
|
||||
|
||||
建议环境变量(可选,名称可按团队调整):
|
||||
|
||||
| 变量 | 用途 |
|
||||
|------|------|
|
||||
| `YUNXIAO_TOKEN` | 个人访问令牌(`x-yunxiao-token`) |
|
||||
| `YUNXIAO_DOMAIN` | 服务接入点域名 |
|
||||
| `YUNXIAO_ORG_ID` | 组织 ID(中心版) |
|
||||
| `YUNXIAO_PROJECT_ID` | 默认项目 ID(或运行时按项目名解析) |
|
||||
|
||||
## 默认项目
|
||||
|
||||
口令未指定时:`统一运营管理平台PC端`。项目名不精确时先问清再查迭代。
|
||||
|
||||
## 判定顺序:解析迭代
|
||||
|
||||
| 顺序 | 条件 | 结果 |
|
||||
|------|------|------|
|
||||
| 1 | 用户未给迭代名称 | 询问:「请输入云效迭代名称(可复制标题)」;不继续 |
|
||||
| 2 | 项目未确定 | 询问精确项目名;不继续 |
|
||||
| 3 | 云效不可访问 | `❌ 无法访问云效…`;不继续 |
|
||||
| 4 | 名称精确匹配 1 条迭代 | `✅ 已找到迭代…`;进入拉需求 |
|
||||
| 5 | 0 条 | `❌ 未找到迭代…请重新输入`;不继续 |
|
||||
| 6 | >1 条相似 | `⚠️ 多个相似…请回复序号或精确名称`;等用户选定 |
|
||||
| 7 | 命中但关联需求 0 | `⚠️ …暂无关联需求`;不生成对外正文 |
|
||||
|
||||
匹配规则:先**全等**(去首尾空格);全等失败再做包含/去空格模糊;模糊多条必须让用户确认,禁止擅自挑一个。
|
||||
|
||||
## 拉取关联需求
|
||||
|
||||
只收集工作项类型为**需求(Req)**且正式关联到该迭代的项。
|
||||
|
||||
- **浏览器路径**:打开项目 → 迭代 → 该迭代规划/工作项列表 → 筛选类型=需求 → 导出或逐条打开描述。
|
||||
- **OpenAPI 路径(示意)**:
|
||||
1. `ListSprints`:按项目列迭代,用 `name` 匹配用户输入,得到 `sprintId`
|
||||
2. `SearchWorkitems`:`category=Req`,`spaceId=项目Id`,`conditions` 中按字段 `sprint` CONTAINS `[sprintId]`(以租户实际 fieldIdentifier 为准;可先在需求列表页过滤后复制 `workitem/list` 的 conditions)
|
||||
3. 分页直到收齐(关注响应头 `x-total`)
|
||||
4. 需要正文时再 `GetWorkitem` 取 description
|
||||
|
||||
每条需求最少记录:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| serialNumber | 如 ONEOSP-123(仅用于核对清单,**不进对外成稿**) |
|
||||
| subject | 标题 |
|
||||
| status | 状态展示名 |
|
||||
| description | 用于抽取「更新内容」 |
|
||||
| category / type / labels | 辅助判断新功能 vs 修复 |
|
||||
|
||||
## 从描述抽「更新内容」
|
||||
|
||||
1. 定位标题「更新内容」或「## 更新内容」段落
|
||||
2. **不要**把「更新内容·历史」并入本版素材(历史仅备查)
|
||||
3. 若无「更新内容」:用标题 + 需求说明摘要;仍无有效产品语义则跳过并在内部备注
|
||||
|
||||
## 成功反馈后的核对清单(推荐短表)
|
||||
|
||||
在生成成稿前输出,例如:
|
||||
|
||||
```text
|
||||
✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 5 条:
|
||||
1. ONEOSP-101 保险采购比价分车提交
|
||||
2. ONEOSP-102 交还车签章预览
|
||||
…
|
||||
正在按 AutoVUL 格式生成更新日志…
|
||||
```
|
||||
|
||||
用户若立刻纠正「某条不要进日志」,从素材中剔除后再出成稿。
|
||||
|
||||
## 与发版任务关系
|
||||
|
||||
本 Skill **只读**迭代与需求,**不创建**【发版】任务、不改云效状态。发版挂链仍用 `yunxiao-requirement-lifecycle`(如「准备发布」口令 / A08)。
|
||||
183
.cursor/skills/oneos-autoprd/SKILL.md
Normal file
183
.cursor/skills/oneos-autoprd/SKILL.md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: oneos-autoprd
|
||||
description: >-
|
||||
Generates and keeps in sync OneOS AutoPRD (PM-facing requirements) plus Axhub
|
||||
Make annotation directory Markdown/PRD; on 需求定稿/定稿/确认定稿/本次定稿 appends
|
||||
functional release changelog below the PRD since last baseline. When a Yunxiao
|
||||
requirement advances to 分析中/设计中/待开发, creates a same-titled linked task
|
||||
(tag+create-time follow the requirement; 分析中/设计中 assignee=creator, 待开发
|
||||
assignee=何斐). Use eagerly for prototypes, AutoPRD, or Yunxiao requirement
|
||||
description/更新内容/stage advance—do not skip annotation sync, 定稿 changelog,
|
||||
or stage-task creation.
|
||||
---
|
||||
|
||||
# OneOS AutoPRD
|
||||
|
||||
为 OneOS 业务模块生成**产品经理可读、可评审、可排期**的需求说明,并**自动挂到 Axhub Make 标注工具 → 原型目录**。
|
||||
|
||||
另支持:**需求定稿**时汇总功能/逻辑变更记录;与云效组合时提供「需求说明 + 更新内容」;需求进入**分析中 / 设计中 / 待开发**时**自动创建与需求同名的关联任务**(规则见下,写在本 Skill,不改云效 Skill)。
|
||||
|
||||
颗粒度对齐「保险采购」全模块 PRD:讲清做什么、谁用、故事点、正逆向、流程图与关键业务逻辑;**不写**表结构、接口、字段代码名、文件路径、实现清单。
|
||||
|
||||
## 何时使用(含自动触发)
|
||||
|
||||
**主动调用时**
|
||||
|
||||
- AutoPRD、OneOS 需求说明、整模块 PRD、故事点 + 流程图
|
||||
|
||||
**改原型时必须自动跟进(全局规则)**
|
||||
|
||||
- 正在修改 `src/prototypes/<id>/` 下页面、交互、文案、判定、验收相关内容
|
||||
- 同一轮交付内同步更新 PRD Markdown + 标注目录,不得只改代码
|
||||
|
||||
**需求定稿时(强制)**
|
||||
|
||||
- 用户回复含:`需求定稿` / `定稿` / `确认定稿` / `本次定稿`
|
||||
- 执行 [references/release-changelog.md](references/release-changelog.md):写第 10 章 + 更新基线
|
||||
|
||||
**云效建需求 / 完善需求时(强制组合)**
|
||||
|
||||
- 与 `$yunxiao-requirement-lifecycle` 一起使用时:先跑本 Skill,再写云效描述
|
||||
- **需求说明** ← PRD 正文(或浓缩交付口径 + 关键章节)
|
||||
- **更新内容** ← 第 10 章中**自上次定稿以来**的条目(无增量则「首版定稿 / 本轮无功能增量」)
|
||||
|
||||
**云效需求推进至分析中 / 设计中 / 待开发时(强制)**
|
||||
|
||||
- 无论口令来自本 Skill 还是云效 Skill,只要本轮把需求推到上述状态,就执行 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)
|
||||
- 自动建**与需求同名**的任务并正式关联;标签与创建时间口径沿用需求;分析中/设计中负责人=创建人,待开发负责人=**何斐**
|
||||
|
||||
不要用本 Skill 替代:需求探索访谈、设计比稿、纯样式微调(无产品语义变化时可跳过全量重写)。
|
||||
|
||||
## 工作流
|
||||
|
||||
### 主流程(写/同步 PRD)
|
||||
|
||||
1. **定模块**:确认 OneOS 模块名与 `src/prototypes/<prototype-id>/`。
|
||||
2. **读上下文(只取产品语义)**
|
||||
- 用户说明、已确认口径、原型标注、`.spec/`、业务条线说明(`lines.ts`)
|
||||
- 忽略实现细节;字段名/接口改写成业务语言。
|
||||
3. **收敛边界**:做什么 / 不做什么、外部依赖、与其它模块关系。
|
||||
4. **按模板成文**:下方「输出结构」;缺关键信息最多问 1~2 个问题,其余写「假设」。
|
||||
5. **落盘 + 标注同步(强制)** — 见 [references/annotation-sync.md](references/annotation-sync.md)。
|
||||
|
||||
| 顺序 | 动作 |
|
||||
|------|------|
|
||||
| A | 写/更新 `src/prototypes/<id>/.spec/requirements-prd.md` |
|
||||
| B | 写/更新 `src/resources/prd/<id>-autoprd.md` |
|
||||
| C | 更新 `annotation-source.json` **顶层** `directory.nodes`(PRD 全文 + 推荐分章);禁止只写 `data.directory` |
|
||||
| D | 若存在 `scripts/sync-annotation-directory.mjs`,执行之 |
|
||||
|
||||
6. **交付说明**:路径、标注目录入口、故事点合计、开放问题/假设。
|
||||
|
||||
### 定稿流程(关键字触发)
|
||||
|
||||
见 [references/release-changelog.md](references/release-changelog.md)。摘要:
|
||||
|
||||
1. 读 PRD + `.spec/autoprd-baseline.json`
|
||||
2. 汇总自上次定稿以来的**功能/逻辑**变更(排除样式/UI/表结构)
|
||||
3. 追加到 `## 10. 功能变更记录`(最新在上)
|
||||
4. 更新基线 JSON + 标注目录
|
||||
5. 若同时发云效:本次定稿块 →「更新内容」;旧段 →「更新内容·历史」
|
||||
|
||||
### 云效交接格式(给 yunxiao skill 直接粘贴)
|
||||
|
||||
```markdown
|
||||
## 原型链接
|
||||
<对象存储或预览 URL>
|
||||
|
||||
## 需求说明
|
||||
<AutoPRD 正文或交付口径 + 必要章节>
|
||||
|
||||
## 更新内容
|
||||
<第 10 章本次定稿块;无则「首版定稿」或「本轮无功能/逻辑增量」>
|
||||
|
||||
## 更新内容·历史
|
||||
<以往更新内容倒序,勿删除>
|
||||
```
|
||||
|
||||
### 云效阶段任务(状态推进时强制)
|
||||
|
||||
见 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)。摘要:
|
||||
|
||||
| 需求状态 | 任务标签 | 负责人 | 标题 |
|
||||
|----------|----------|--------|------|
|
||||
| 分析中 | 分析 | 需求创建人 | 与需求同名 |
|
||||
| 设计中 | 设计 | 需求创建人 | 与需求同名 |
|
||||
| 待开发 | 交付(或开发) | 何斐 | 与需求同名 |
|
||||
|
||||
正式关联需求;创建时间口径沿用需求;同阶段未取消任务不重复建。
|
||||
|
||||
## 写作硬约束
|
||||
|
||||
**必须写**
|
||||
|
||||
- 一句话定位 + 目标 / 非目标
|
||||
- 模块边界(含 mermaid 总览更好)
|
||||
- 角色与目标(角色名优先对齐业务条线说明)
|
||||
- **用户故事**(业务条线说明口径)+ Epic 级**故事点(SP)**粗估
|
||||
- 分功能**正向**与**逆向/边界**
|
||||
- 至少 1~2 个 **mermaid** 流程图
|
||||
- **关键业务逻辑**(业务话)
|
||||
- 验收清单 + 「交付口径」一段话
|
||||
- 定稿后:**功能变更记录**(第 10 章)
|
||||
|
||||
**禁止写**
|
||||
|
||||
- 数据库表、字段名、接口路径、代码路径、组件名、存储 key
|
||||
- 研发实现指令(可写「正式环境由审批中心回写」这类业务依赖)
|
||||
- 在变更记录里写样式/UI/表结构优化
|
||||
|
||||
## 用户故事口径(强制 · 对齐业务条线说明)
|
||||
|
||||
真相源:原型 **业务条线说明**(`lease-business-line-overview` / `lines.ts`)。
|
||||
每条能力用「责任部门 → **起点** → **怎么运作** → **闭环**」叙述;**不要**用「作为…我希望…」宽表作主叙述。
|
||||
|
||||
| 块 | 写什么 |
|
||||
|----|--------|
|
||||
| 角色 | 谁负责、谁协同 |
|
||||
| 起点 | 谁在什么前提下启动 |
|
||||
| 怎么运作 | 有序步骤,含跨角色协作 |
|
||||
| 关键结果 | 可选标签 |
|
||||
| 闭环 | 业务终点与可追溯性 |
|
||||
|
||||
可选:`US-xx`、压缩句「作为…我想…以便…」、规模 S/M/L 或 SP(仅排期,不替代主叙述)。
|
||||
|
||||
## 输出结构
|
||||
|
||||
完整模板:[references/template.md](references/template.md)。
|
||||
|
||||
```markdown
|
||||
# <模块名> · 产品需求说明(全模块)
|
||||
|
||||
## 1. 一句话与目标
|
||||
## 2. 模块边界(最重要)
|
||||
## 3. 用户与角色
|
||||
## 4. 用户故事与故事点(业务条线说明口径)
|
||||
## 5. 功能模块说明(正向 / 逆向)
|
||||
## 6. 关键业务逻辑(必须对齐)
|
||||
## 7. 总览流程图
|
||||
## 8. 验收清单
|
||||
## 9. 交付口径
|
||||
## 10. 功能变更记录 ← 定稿后维护;日常改原型不强制每改必写
|
||||
```
|
||||
|
||||
## 质量自检
|
||||
|
||||
- [ ] 产品经理不看代码也能评审
|
||||
- [ ] 用户故事为起点 / 怎么运作 / 闭环
|
||||
- [ ] `.spec/requirements-prd.md` 已更新
|
||||
- [ ] 标注目录「产品需求说明(PRD)」已同步且正文一致
|
||||
- [ ] 定稿时:第 10 章 + `autoprd-baseline.json` 已更新
|
||||
- [ ] 推进至分析中/设计中/待开发时:同名任务已创建或复用,正式关联,负责人正确
|
||||
- [ ] 无表结构 / 接口 / 代码路径;变更记录无样式/UI 废话
|
||||
|
||||
## 参考
|
||||
|
||||
- 定稿变更日志:[references/release-changelog.md](references/release-changelog.md)
|
||||
- 云效阶段任务:[references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)
|
||||
- 标注同步细则:[references/annotation-sync.md](references/annotation-sync.md)
|
||||
- 章节模板:[references/template.md](references/template.md)
|
||||
- 故事示例:[references/granularity-example.md](references/granularity-example.md)
|
||||
- 业务条线:`src/prototypes/lease-business-line-overview/lines.ts`
|
||||
- 复杂判定规格:配合项目规则 `business-logic-documentation`
|
||||
- 云效组合:`$yunxiao-requirement-lifecycle`(建需求时先本 Skill)
|
||||
98
.cursor/skills/oneos-autoprd/references/annotation-sync.md
Normal file
98
.cursor/skills/oneos-autoprd/references/annotation-sync.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Axhub 标注目录同步(强制)
|
||||
|
||||
使用 OneOS AutoPRD 时,除资源库归档外,**必须**把 PRD 挂到 Axhub Make 标注工具的「原型目录」里,用户才能在右侧面板直接打开。
|
||||
|
||||
## 落点(三份必须一致)
|
||||
|
||||
| # | 路径 | 作用 |
|
||||
|---|------|------|
|
||||
| 1 | `src/prototypes/<prototype-id>/.spec/requirements-prd.md` | **主真相**:产品可读 PRD 全文 |
|
||||
| 2 | `src/prototypes/<prototype-id>/annotation-source.json` → **顶层** `directory.nodes` | **标注目录入口**:面板里可见的 Markdown/PRD |
|
||||
| 3 | `src/resources/prd/<prototype-id>-autoprd.md` | 资源库归档(可选但默认写) |
|
||||
|
||||
可选:复杂判定另写 `.spec/<topic>.md`,目录里再挂一条,并在 PRD 里链过去(与 `business-logic-documentation` 一致)。
|
||||
|
||||
## ⚠️ 目录必须写在文档顶层(常见踩坑)
|
||||
|
||||
Wire format(与 `prototype-annotation` 一致):
|
||||
|
||||
```json
|
||||
{
|
||||
"documentVersion": 1,
|
||||
"format": "axhub-annotation-source",
|
||||
"data": { "version": 2, "prototypeName": "...", "nodes": [] },
|
||||
"markdownMap": {},
|
||||
"assetMap": {},
|
||||
"directory": { "nodes": [] }
|
||||
}
|
||||
```
|
||||
|
||||
| 正确 | 错误 |
|
||||
|------|------|
|
||||
| 顶层 `directory.nodes` | 只写在 `data.directory` |
|
||||
| `PrototypeAnnotationHost` / Make 读的是 `source.directory` | 写在 `data` 下时右侧「原型目录」为空 |
|
||||
|
||||
若误写在 `data.directory`:迁移到顶层 `directory`,并删除 `data.directory`,避免双源。
|
||||
|
||||
## 目录节点写法(推荐)
|
||||
|
||||
在说明根 `folder` 的 `children` **靠前**插入或更新:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "<prefix>-doc-prd",
|
||||
"title": "产品需求说明(PRD)",
|
||||
"markdownPath": ".spec/requirements-prd.md",
|
||||
"markdown": "<与 .spec/requirements-prd.md 相同的全文>"
|
||||
}
|
||||
```
|
||||
|
||||
### 分章节 PRD(推荐,便于右侧目录浏览)
|
||||
|
||||
在「产品需求说明(PRD)」旁增加文件夹 **「PRD 分章」**,按 PRD 的 `##` 一级标题拆成多个 `markdown` 子节点(标题用章节名,正文为该章全文)。专题规格(`.spec/<topic>.md`)放在 **「专题规格」** 文件夹。
|
||||
|
||||
参考结构:
|
||||
|
||||
```text
|
||||
folder <模块>说明
|
||||
markdown 产品需求说明(PRD) ← 全文
|
||||
folder PRD 分章
|
||||
markdown 1. 一句话与目标
|
||||
markdown 2. 模块边界
|
||||
…
|
||||
folder 专题规格
|
||||
markdown <topic 标题>
|
||||
```
|
||||
|
||||
有 `scripts/sync-annotation-directory.mjs` 的原型:改 PRD / `.spec` 后**必须执行**该脚本再生目录。
|
||||
|
||||
定稿后第 10 章「功能变更记录」必须进入 PRD 全文节点;若有「PRD 分章」,同步该章节点。详见 [release-changelog.md](release-changelog.md)。
|
||||
|
||||
约定:
|
||||
|
||||
- `id`:`<短前缀>-doc-prd`(如 `ipc-doc-prd`、`wb-doc-prd`、`h5-va-doc-prd`)
|
||||
- `title`:优先「产品需求说明(PRD)」;已有「PRD」可保留不改名
|
||||
- **同时写** `markdownPath` + `markdown`:构建可内联路径,运行时目录也能直接读到正文
|
||||
- 若已有同 id / 同标题节点:只更新正文与 path,不新建重复入口
|
||||
- 遵守 `prototype-annotation-layout`:目录以 `markdown` 为主,不要用 `link`/`route` 顶替 PRD
|
||||
|
||||
## 增量改原型时怎么更新
|
||||
|
||||
1. 根据改动的文件路径确定 `<prototype-id>`。
|
||||
2. 读现有 `.spec/requirements-prd.md`(没有则按 AutoPRD 模板新建)。
|
||||
3. 只改受影响章节(故事、正逆向、关键逻辑、验收);其余保留。
|
||||
4. 回写 `.spec` + `resources/prd` + **顶层** `annotation-source.json` → `directory`(正文与文件一致;含分章节点)。
|
||||
5. 若原型有 `scripts/sync-annotation-directory.mjs`,改完后执行它。
|
||||
|
||||
## 可跳过全量重写的情况
|
||||
|
||||
纯样式、无文案/无流程/无判定变化的微调:不必重写整份 PRD,但若改了用户可见文案或交互结果,仍须同步对应段落与目录节点。
|
||||
|
||||
## 验收
|
||||
|
||||
- [ ] `.spec/requirements-prd.md` 存在且为最新
|
||||
- [ ] `annotation-source.json` **顶层**有 `directory.nodes`(不是只在 `data` 下)
|
||||
- [ ] 标注目录能打开「产品需求说明(PRD)」且内容与文件一致
|
||||
- [ ] 若有「PRD 分章」:各章可单独打开
|
||||
- [ ] 用户故事仍为业务条线说明口径(起点 / 怎么运作 / 闭环)
|
||||
@@ -0,0 +1,58 @@
|
||||
# 颗粒度标杆与用户故事口径
|
||||
|
||||
## 用户故事:对齐「业务条线说明」页面
|
||||
|
||||
页面:`/prototypes/lease-business-line-overview`
|
||||
数据:`src/prototypes/lease-business-line-overview/lines.ts`
|
||||
|
||||
每个能力卡片的产品叙述结构:
|
||||
|
||||
```text
|
||||
责任部门(roles)
|
||||
→ 起点(start)
|
||||
→ 怎么运作(process[],有序)
|
||||
→ 关键结果(outcomes,可选)
|
||||
→ 闭环(closure)
|
||||
```
|
||||
|
||||
AutoPRD 第 4 章必须用同一套结构写用户故事,而不是「作为…我希望…」宽表。
|
||||
|
||||
### 示例(保险采购 · 交车合规,示意)
|
||||
|
||||
#### US · 保险有效才允许交车
|
||||
- **角色**:采购部、运维部
|
||||
- **起点**:采购或运营维护车辆交强险与商业险台账,作为交车前置合规数据。
|
||||
- **怎么运作**:
|
||||
1. 在保险采购维护一车一档险种台账(录入 / 识别 / 导入)。
|
||||
2. 交车时系统校验交强 + 商业是否均为正常或临期。
|
||||
3. 不合规则拦截交车并给出可理解原因。
|
||||
- **关键结果**:`允许交车` / `禁止交车 · 保险异常`
|
||||
- **闭环**:交车合规与台账一致、可追溯;异常车辆不能进入履约运营。
|
||||
- **排期(可选)**:US-17 · 规模 M · 作为采购/运维,我想维护交强+商业险并在交车时校验,以便不合规车辆无法交车。
|
||||
|
||||
### 反例(不要作为主叙述)
|
||||
|
||||
| 编号 | 用户故事 | SP |
|
||||
|------|----------|----|
|
||||
| B1 | 作为运营,我能看到失败原因 | 2 |
|
||||
|
||||
↑ 信息太扁,缺少起点、协作步骤与闭环结果。
|
||||
|
||||
---
|
||||
|
||||
## AutoPRD 其余层次(仍要对齐)
|
||||
|
||||
1. 一句话 + 目标/非目标
|
||||
2. 模块边界(独立业务线写清「不自动同步」)
|
||||
3. 角色表(命名对齐业务条线)
|
||||
4. **用户故事:起点 / 怎么运作 / 闭环** + SP/规模
|
||||
5. 分模块正逆向
|
||||
6. 关键业务逻辑
|
||||
7. mermaid 流程图
|
||||
8. 验收清单
|
||||
9. 交付口径
|
||||
|
||||
## 刻意不写
|
||||
|
||||
- 表结构、接口、代码路径
|
||||
- 用实现指令代替业务闭环描述
|
||||
78
.cursor/skills/oneos-autoprd/references/release-changelog.md
Normal file
78
.cursor/skills/oneos-autoprd/references/release-changelog.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 功能变更记录(定稿触发)
|
||||
|
||||
用户回复**需求定稿关键字**后,必须生成「自上次定稿以来」的功能变更日志,追加到 PRD **最下方**,并更新定稿基线。
|
||||
|
||||
## 触发关键字(命中任一)
|
||||
|
||||
`需求定稿` · `定稿` · `确认定稿` · `本次定稿`
|
||||
|
||||
(可带模块名,如「保险采购需求定稿」。)
|
||||
|
||||
## 写什么 / 不写什么
|
||||
|
||||
| 要写(产品发版视角) | 不写 |
|
||||
|----------------------|------|
|
||||
| 哪个功能做了什么修改 | 布局、页面样式、UI 设计优化 |
|
||||
| 变更了什么业务逻辑 / 流程 / 验收口径 | 「优化了哪些表」「改了哪些字段/接口」 |
|
||||
| 用户可感知的交互结果变化 | 纯样式、动效、无语义文案微调 |
|
||||
|
||||
条目口吻示例:
|
||||
|
||||
- 「识别失败」:失败数可点击查看失败文件与原因;仅失败时也可打开明细
|
||||
- 「比价单」:审批通过后仍须在保单管理另行录入正式保单(强调不自动同步)
|
||||
|
||||
## 落点
|
||||
|
||||
1. **PRD 文末章节**(强制)
|
||||
`src/prototypes/<id>/.spec/requirements-prd.md` → `## 10. 功能变更记录`
|
||||
新定稿块插在该章**最上方**(倒序:最新在上);历史块保留。
|
||||
|
||||
2. **定稿基线**(强制)
|
||||
`src/prototypes/<id>/.spec/autoprd-baseline.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"prototypeId": "insurance-procurement",
|
||||
"lastConfirmedAt": "2026-07-21T12:00:00+08:00",
|
||||
"lastConfirmedLabel": "定稿 · 2026-07-21",
|
||||
"summaryBullets": ["…", "…"]
|
||||
}
|
||||
```
|
||||
|
||||
3. **标注目录**:同步更新「产品需求说明(PRD)」全文节点;若有「PRD 分章」,增加或更新「功能变更记录」分章。
|
||||
|
||||
4. **云效交接**(若本轮同时走云效):把**本次定稿块**正文作为需求描述「更新内容」;旧「更新内容」挪到「更新内容·历史」。
|
||||
|
||||
## 定稿工作流
|
||||
|
||||
1. 确认 `<prototype-id>`(从对话 / 打开文件 / 用户指名推断)。
|
||||
2. 读现有 PRD 与 `autoprd-baseline.json`(无基线则本轮视为**首版定稿**)。
|
||||
3. 收集自 `lastConfirmedAt` 以来的产品语义变更:
|
||||
对话确认口径 → 批注/操作说明 → PRD 新旧差异 →(可选)相关提交说明。
|
||||
过滤掉样式/表结构类改动。
|
||||
4. 若无功能增量:仍写定稿块,正文为「本轮无功能/逻辑增量(仅样式或未达产品语义变更)」或「首版定稿」。
|
||||
5. 追加 `### 定稿 · YYYY-MM-DD` + 条目列表到第 10 章顶部。
|
||||
6. 更新 `autoprd-baseline.json`;执行 annotation-sync。
|
||||
7. 向用户回报:基线时间、条目数、PRD 路径;若需发云效,提示可用口令。
|
||||
|
||||
## PRD 章节格式
|
||||
|
||||
```markdown
|
||||
## 10. 功能变更记录
|
||||
|
||||
> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。
|
||||
|
||||
### 定稿 · 2026-07-21
|
||||
|
||||
- 「能力名」:做了什么 / 逻辑如何变
|
||||
- …
|
||||
|
||||
### 定稿 · 2026-07-10
|
||||
|
||||
- …
|
||||
```
|
||||
|
||||
## 与日常改原型的关系
|
||||
|
||||
- **改原型过程中**:按 AutoPRD 主流程增量更新第 1–9 章;**不必**每改一次就写第 10 章。
|
||||
- **用户说定稿时**:才汇总写入第 10 章并刷新基线。
|
||||
163
.cursor/skills/oneos-autoprd/references/template.md
Normal file
163
.cursor/skills/oneos-autoprd/references/template.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# OneOS AutoPRD 输出模板
|
||||
|
||||
复制下列结构填写。方括号为占位。**第 4 章用户故事必须用业务条线说明口径。**
|
||||
|
||||
```markdown
|
||||
# <模块名> · 产品需求说明(全模块)
|
||||
|
||||
## 1. 一句话与目标
|
||||
|
||||
**一句话**
|
||||
<用一句话说明模块解决什么>
|
||||
|
||||
**要解决的问题**
|
||||
- …
|
||||
|
||||
**本期目标**
|
||||
1. …
|
||||
2. …
|
||||
|
||||
**非目标(本期不做)**
|
||||
- …
|
||||
|
||||
## 2. 模块边界(最重要)
|
||||
|
||||
(可选 mermaid:模块与外部系统关系)
|
||||
|
||||
| 业务线 / 子域 | 做什么 | 不做什么 |
|
||||
|---------------|--------|----------|
|
||||
| … | … | … |
|
||||
|
||||
**外部依赖(产品口径)**
|
||||
| 依赖方 | 交互方式 | 产品要求 |
|
||||
|--------|----------|----------|
|
||||
| … | … | … |
|
||||
|
||||
**关键约束**
|
||||
- …
|
||||
|
||||
## 3. 用户与角色
|
||||
|
||||
| 角色 | 主要目标 |
|
||||
|------|----------|
|
||||
| … | … |
|
||||
|
||||
> 角色名优先与「业务条线说明」一致(如业务管理组、采购部、运维部)。
|
||||
|
||||
## 4. 用户故事与故事点(业务条线说明口径)
|
||||
|
||||
> 故事点 / 规模供排期参考,可按团队基准调整。合计约 **N SP**(或 S/M/L 汇总说明)。
|
||||
|
||||
按 Epic 或能力单元分组。**每一条**按下面四块写(对齐业务条线说明页:责任部门 → 起点 → 怎么运作 → 闭环):
|
||||
|
||||
### Epic A · <名称>(约 N SP)
|
||||
|
||||
#### A1 · <能力标题>
|
||||
- **角色**:…
|
||||
- **起点**:…
|
||||
- **怎么运作**:
|
||||
1. …
|
||||
2. …
|
||||
3. …
|
||||
- **关键结果**(可选):`可…` / `禁止…`
|
||||
- **闭环**:…
|
||||
- **排期(可选)**:US-xx · 规模 S/M/L · 「作为…,我想…,以便…」
|
||||
|
||||
#### A2 · <能力标题>
|
||||
…
|
||||
|
||||
### Epic B · …
|
||||
|
||||
## 5. 功能模块说明(正向 / 逆向)
|
||||
|
||||
### 5.x <功能名>
|
||||
|
||||
**正向**
|
||||
1. …
|
||||
2. …
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| … | … |
|
||||
|
||||
## 6. 关键业务逻辑(必须对齐)
|
||||
|
||||
### 6.x <规则名>
|
||||
|
||||
用编号优先级、表格或短列表写清业务规则。
|
||||
|
||||
## 7. 总览流程图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
home[入口] --> a[能力A]
|
||||
home --> b[能力B]
|
||||
```
|
||||
|
||||
## 8. 验收清单(产品 / 测试)
|
||||
|
||||
**子域 A**
|
||||
- [ ] …
|
||||
|
||||
## 9. 交付口径
|
||||
|
||||
> <一段可贴需求单开头的浓缩描述>
|
||||
|
||||
## 10. 功能变更记录
|
||||
|
||||
> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。
|
||||
> 由「需求定稿」关键字触发追加;日常改原型不强制每改必写。详见 [release-changelog.md](release-changelog.md)。
|
||||
|
||||
### 定稿 · YYYY-MM-DD
|
||||
|
||||
- 「<能力名>」:<做了什么 / 逻辑如何变>
|
||||
- …
|
||||
```
|
||||
|
||||
成文后**必须**同步 Axhub 标注目录(见 [annotation-sync.md](annotation-sync.md)):
|
||||
|
||||
1. `src/prototypes/<id>/.spec/requirements-prd.md`
|
||||
2. `annotation-source.json` → 目录节点「产品需求说明(PRD)」(`markdownPath` + `markdown`)
|
||||
3. `src/resources/prd/<id>-autoprd.md`
|
||||
4. 定稿时另写:`src/prototypes/<id>/.spec/autoprd-baseline.json`
|
||||
|
||||
## 用户故事写法(对照业务条线说明)
|
||||
|
||||
业务条线说明页字段 → AutoPRD 字段:
|
||||
|
||||
| 页面 | AutoPRD |
|
||||
|------|---------|
|
||||
| 责任部门标签 | **角色** |
|
||||
| 起点 | **起点** |
|
||||
| 怎么运作(有序列表) | **怎么运作** |
|
||||
| 结果色签 | **关键结果**(可选) |
|
||||
| 闭环 | **闭环** |
|
||||
|
||||
示例口吻(摘自业务条线风格,非照抄某模块):
|
||||
|
||||
- **起点**:采购部维护交强与商业险台账,作为交车合规的前置数据。
|
||||
- **怎么运作**:1. 运营录入或识别保单… 2. 运维交车时校验…
|
||||
- **闭环**:核心险种有效则允许交车;异常则拦截并提示原因,过程可追溯。
|
||||
|
||||
**不要**用下面这种宽表作为第 4 章唯一形态:
|
||||
|
||||
| 编号 | 作为…我希望… | SP |
|
||||
|------|--------------|----|
|
||||
|
||||
「作为…我想…以便…」只允许作为**排期可选一行**,主叙述必须是起点/怎么运作/闭环。
|
||||
|
||||
## 正逆向写法约定
|
||||
|
||||
- **正向**:用户成功完成任务的最短路径
|
||||
- **逆向**:取消、关闭、禁用、冲突状态、不可操作项
|
||||
- 审批类:写清「本页只读 / 在外部系统办理」
|
||||
|
||||
## 规模与故事点
|
||||
|
||||
| 规模 | 建议含义 | 约 SP |
|
||||
|------|----------|-------|
|
||||
| S | 轻交互 / 列表展示 | 1~2 |
|
||||
| M | 标准流程 + 筛选详情 | 3~5 |
|
||||
| L | 状态机 / 跨模块 / 批量识别 | 6+ |
|
||||
@@ -0,0 +1,44 @@
|
||||
# 云效阶段任务自动创建(AutoPRD 负责)
|
||||
|
||||
本规则写在 **oneos-autoprd**,不依赖改写 `yunxiao-requirement-lifecycle`。
|
||||
凡本 Skill 参与的云效操作中,需求推进到下列状态时,**同一轮必须**建同名任务并正式关联需求。
|
||||
|
||||
## 触发状态
|
||||
|
||||
| 需求推进至 | 任务标签(右侧基础字段) | 负责人 | 任务标题 |
|
||||
|------------|--------------------------|--------|----------|
|
||||
| **分析中** | `分析` | **需求创建人**姓名 | 与需求标题**完全同名** |
|
||||
| **设计中** | `设计` | **需求创建人**姓名 | 与需求标题**完全同名** |
|
||||
| **待开发** | `交付`(若项目无「交付」标签则用 `开发`) | **何斐** | 与需求标题**完全同名** |
|
||||
|
||||
## 强制字段与关系
|
||||
|
||||
1. **正式关联需求**:父子或关联项;只挂标题不算关联。创建后必须在界面上能看到需求 ↔ 任务关系。
|
||||
2. **标签**:写入任务「标签」基础字段(见上表);标题前缀不能代替标签。
|
||||
3. **创建时间沿用需求**:任务的创建时间(及标签侧可见的创建时间口径)尽量与**需求创建时间**一致。
|
||||
- 平台允许改创建时间 / 用 API 指定时:写成需求的创建时间。
|
||||
- 平台不允许:在任务描述首行注明 `创建时间口径=需求创建时间(YYYY-MM-DD HH:mm)`,并在回报里说明「平台限制未改系统创建时间」。
|
||||
4. **幂等**:同一需求、同一阶段标签下,已存在**未取消**且已正式关联的同名任务 → **不新建**,只校验负责人/标签/关联是否正确,缺则补齐。
|
||||
5. **顺序**:先确认需求已进入目标状态(或本轮动作将写入该状态),再创建/补齐任务;回报时给出任务编号与链接。
|
||||
|
||||
## 执行步骤(apply 时)
|
||||
|
||||
```text
|
||||
1. 读取需求:标题、创建人、创建时间、当前/目标状态、已关联任务列表
|
||||
2. 按上表确定本轮阶段标签与负责人
|
||||
3. 查重(需求ID + 阶段标签 + 未取消)
|
||||
4. 无则创建:标题=需求标题;标签=阶段标签;负责人=上表;创建时间口径=需求创建时间;正式关联需求
|
||||
5. 有则校验并修补标签/负责人/关联
|
||||
6. 回报:需求状态、任务编号、负责人、是否新建/复用
|
||||
```
|
||||
|
||||
## 不在本规则内
|
||||
|
||||
- 不自动建「测试 / 发版」任务(除非用户另行要求)。
|
||||
- 不因样式类 PRD 变更触发建任务;仅**需求状态**进入分析中/设计中/待开发时触发。
|
||||
- 云效生命周期细则、流水线、缺陷闭环仍可由 `$yunxiao-requirement-lifecycle` 配合执行;**本建任务逻辑以 AutoPRD 本文为准**。
|
||||
|
||||
## 与定稿 / 需求说明的关系
|
||||
|
||||
- 写「需求说明 / 更新内容」仍按 AutoPRD 主流程与定稿流程。
|
||||
- 推进到待开发且需要描述时:先 AutoPRD 文档,再改状态,再按本文件建任务(负责人何斐)。
|
||||
157
.cursor/skills/yunxiao-requirement-lifecycle/SKILL.md
Normal file
157
.cursor/skills/yunxiao-requirement-lifecycle/SKILL.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: yunxiao-requirement-lifecycle
|
||||
description: >-
|
||||
Audit, design, configure, migrate, test, troubleshoot, and document Alibaba Cloud
|
||||
Yunxiao/Projex requirement lifecycle automation. Use for short conversational Chinese
|
||||
commands such as 记录需求、确认需求、需求定稿、开始分析、开始开发、提交测试、测试完成、
|
||||
发布成功 and 验收通过; OneOS delivery or legacy stage-task model. When creating or
|
||||
refreshing OneOS product requirements, first call oneos-autoprd for 需求说明 and
|
||||
更新内容 (functional changelog since last 定稿), then write Yunxiao description.
|
||||
Also covers role handoffs, Codeup, Testhub, pipelines, release acceptance, migration,
|
||||
and Chinese runbooks without exposing credentials or changing production pipelines unexpectedly.
|
||||
---
|
||||
|
||||
# Yunxiao Requirement Lifecycle
|
||||
|
||||
Use this as the single entry skill. Load only the internal modules needed for the request. Installing the skill does not change Yunxiao; live transitions start only after an authorized `apply` and verified `test` run.
|
||||
|
||||
## Route to modules
|
||||
|
||||
Read each selected file completely before acting:
|
||||
|
||||
| Request scope | Required module |
|
||||
|---|---|
|
||||
| OneOS终态模型、`【交付】`主任务、多开发子任务、迭代测试/发版、Y01–Y40、A01–A10、旧规则迁移 | [references/oneos-terminal-flow.md](references/oneos-terminal-flow.md) |
|
||||
| Lifecycle statuses, requirement labels, related tasks, R01–R12, stage entry/completion | [references/lifecycle-rules.md](references/lifecycle-rules.md) |
|
||||
| Design/development/test task creation, task states, rollups, TK01–TK08 | [references/task-lifecycle.md](references/task-lifecycle.md) |
|
||||
| Test plans, case results, failed cases, bugs, retest, TC01–TC03, DF01–DF03 | [references/test-defect-closure.md](references/test-defect-closure.md) |
|
||||
| Repositories, `dev`/`develop`, branches, commits, MRs, multi-repository gate, CI01 | [references/codeup-integration.md](references/codeup-integration.md) |
|
||||
| test/prod pipelines, Docker callback, release changes, acceptance, CI02–CI03, L01–L02 | [references/pipeline-release.md](references/pipeline-release.md) |
|
||||
| Live audit/apply/test, browser execution, evidence, diagnosis, rollback, governance | [references/live-operations.md](references/live-operations.md) |
|
||||
| 日常口语化操作,使用“记录需求、开始分析、安排开发、提交测试、复测通过、发布成功”等短口令 | [references/simple-role-commands.md](references/simple-role-commands.md) |
|
||||
| **统一运营管理平台**日常「记录需求」加速执行(已验证 ID / API / 状态三跳) | [references/oneos-pc-fast-path.md](references/oneos-pc-fast-path.md) + [assets/oneos-pc-runtime-ids.json](assets/oneos-pc-runtime-ids.json) |
|
||||
| **统一运营管理平台**「记录需求到云效」完整可复制提示词(A/B/C 门禁 + 30 标签) | [references/oneos-pc-record-requirement-prompt.md](references/oneos-pc-record-requirement-prompt.md) |
|
||||
| 需求进入分析中/设计中/待开发时自动建同名任务、关联需求、负责人与时间沿用 | [references/auto-stage-task.md](references/auto-stage-task.md) |
|
||||
| 产品、分析、设计、开发、评审、测试、缺陷、运维、验收等角色的节点沟通话术和交接回报 | [references/role-trigger-prompts.md](references/role-trigger-prompts.md) |
|
||||
| Final Chinese report | [references/report-template.md](references/report-template.md) |
|
||||
|
||||
## 记录需求 · 参数门禁与加速(强制)
|
||||
|
||||
当口令为「记录需求」且项目为统一运营管理平台(含 PC 端别名)时:
|
||||
|
||||
1. **先读** [references/oneos-pc-fast-path.md](references/oneos-pc-fast-path.md) 与 [assets/oneos-pc-runtime-ids.json](assets/oneos-pc-runtime-ids.json),按快路径执行,禁止重复探测 create URL / 优先级 ID。
|
||||
2. 若用户要求 Plan/单选优先级、推进至与标签:必须先拿到明确 **A+B+C**;**禁止**未选时默认「中 + 分析中」并建单。「批准计划」≠ 已选 A+B+C。完整用户口令见 [references/oneos-pc-record-requirement-prompt.md](references/oneos-pc-record-requirement-prompt.md)。
|
||||
3. 优先 API 建单(创建时写入 `document`)、同名任务与打标签(`PATCH` `propertyKey=tag`);浏览器仅用于标签 API 失败或状态连跳兜底。统一运营管理平台标签从 `assets/oneos-pc-tag-catalog.md` 选择器点选,禁止按 `lines.ts` 自动映射。
|
||||
|
||||
## AutoPRD 组合(强制 · OneOS 产品需求)
|
||||
|
||||
当请求涉及**创建产品类需求、写入/刷新需求描述、快轨待开发、完善需求说明/更新内容**,或用户口令含「记录需求(带模块/原型)」「需求已经确定」「需求定稿」并要进云效时:
|
||||
|
||||
1. **先**加载并执行项目/全局的 **`oneos-autoprd`** skill(勿跳过)。
|
||||
2. 用其产出填充云效需求描述:
|
||||
- **需求说明** ← AutoPRD 正文(产品语言;不写表结构/接口/代码)
|
||||
- **更新内容** ← AutoPRD 第 10 章「功能变更记录」中自上次定稿以来的条目;无则「首版定稿」或「本轮无功能/逻辑增量」
|
||||
- 已有「更新内容」时:旧段挪到 **更新内容·历史**(倒序追加,勿删)
|
||||
3. 原型发版增量以 AutoPRD 功能变更记录为准;PC 整包发版公告仍可另用 `AutoVUL`。
|
||||
4. 对象存储链接、主任务、状态推进等仍按 `oneos-terminal-flow.md` / `simple-role-commands.md`。
|
||||
|
||||
`A02` / `A02B` 在 OneOS 项目上的默认实现方式即为调用 AutoPRD,不得只写占位文案。
|
||||
|
||||
## 阶段同名任务(强制 · AI apply)
|
||||
|
||||
凡将需求推进到 **分析中 / 设计中 / 待开发**(含口令「开始分析」「开始设计」「快速开发」「需求已经确定…待开发」等),必须先读并执行 [references/auto-stage-task.md](references/auto-stage-task.md):
|
||||
|
||||
| 状态 | 任务 | 负责人 |
|
||||
|---|---|---|
|
||||
| 分析中 | 与需求**同名**;标签`分析`;正式关联需求 | 需求**创建人** |
|
||||
| 设计中 | 与需求**同名**;标签`设计`;正式关联需求 | 需求**创建人** |
|
||||
| 待开发 | 与需求**同名**;标签`交付`(或阶段模型下`开发`);正式关联需求 | 固定**何斐** |
|
||||
|
||||
时间:任务计划开始(及可写的创建时间)**沿用需求**的计划开始/创建时间。查重幂等,禁止同阶段重复建单。
|
||||
|
||||
First identify `flow_model`:
|
||||
|
||||
- `stage_tasks`: use R01–R12 and TK01–TK08; read the five domain modules plus `live-operations.md` for a full audit.
|
||||
- `oneos_delivery`: read `oneos-terminal-flow.md`, `codeup-integration.md`, `test-defect-closure.md`, `pipeline-release.md`, and `live-operations.md`.
|
||||
|
||||
For a narrow diagnosis, read the relevant domain module plus `live-operations.md`. Do not blend both models in one project or load unrelated modules merely because they exist.
|
||||
|
||||
## Classify authority
|
||||
|
||||
- `audit`: inspect and report only.
|
||||
- `plan`: prepare a project profile and change plan only.
|
||||
- `apply`: create or modify only explicitly named projects, labels, rules, and test assets.
|
||||
- `test`: exercise approved rules with isolated artifacts and collect evidence.
|
||||
- `document`: produce a Chinese runbook from verified facts.
|
||||
|
||||
Treat ambiguous requests as `audit` or `plan`. Documentation never authorizes live changes.
|
||||
|
||||
When the user uses a write verb defined in `simple-role-commands.md`—for example `记录需求`、`确认需求`、`开始分析`、`开始设计`、`开始开发`、`提交测试`、`复测通过`、`发布成功` or `验收通过`—treat it as `apply` authorization only for the current exact project and named work item. If the project is not exact, ask only for the project before writing. Query verbs such as `查状态`、`为什么没流转`、`下一步` and `给我方案` remain `audit` or `plan`.
|
||||
|
||||
Whenever apply changes a requirement status to `分析中`、`设计中` or `待开发`, also apply [references/auto-stage-task.md](references/auto-stage-task.md) in the same turn (same-name task, formal link, assignee and time rules) before reporting success.
|
||||
|
||||
## Build the project profile
|
||||
|
||||
1. Choose exactly one template and copy it outside the skill:
|
||||
- `stage_tasks`: `assets/project-profile.template.json`.
|
||||
- `oneos_delivery`: `assets/oneos-project-profile.template.json`.
|
||||
2. Keep one independent object in `projects` for every target project; duplicate the template object when needed.
|
||||
3. Replace observed statuses, rule instances, repositories, branches, pipelines, test plans, defect workflows, release evidence, and control dispositions.
|
||||
4. Never add usernames, passwords, cookies, tokens, OTPs, Webhook secrets, or private keys.
|
||||
5. Validate before applying:
|
||||
|
||||
```text
|
||||
python scripts/profile_tool.py validate <profile.json>
|
||||
python scripts/profile_tool.py render <profile.json> --output <execution-plan.md>
|
||||
|
||||
python scripts/oneos_profile_tool.py validate <oneos-profile.json>
|
||||
python scripts/oneos_profile_tool.py render <oneos-profile.json> --output <execution-plan.md>
|
||||
```
|
||||
|
||||
Do not apply while placeholders remain or validation fails.
|
||||
|
||||
## Completeness contract
|
||||
|
||||
For `stage_tasks`, account for every control for every project independently:
|
||||
|
||||
- `R01`–`R12`: requirement lifecycle and manual gates.
|
||||
- `TK01`–`TK08`: stage-task creation, task-state transitions, requirement rollups, and idempotency.
|
||||
- `TC01`–`TC03`: test-case execution, failed-case/defect association, and retest result update.
|
||||
- `DF01`–`DF03`: fixed handoff, tester-pass closure, and tester-fail reopen.
|
||||
- `CI01`–`CI03`: multi-repository coverage, callback safeguards, and environment/release separation.
|
||||
- `L01`–`L02`: legacy immediate-close rule disposition.
|
||||
|
||||
Record the actual R01–R12 `rule_instances`, TK01–TK08 `task_rule_instances`, and all 31 `control_coverage` entries. Valid modes are `manual`, `native_rule`, `integration_bridge`, `disabled_legacy`, and `not_supported`. `not_supported` needs an owner and fallback and is never equivalent to automated.
|
||||
|
||||
Completeness covers the requirement lifecycle, stage-task lifecycle and rollups, Testhub closure, Codeup assets, pipeline/release evidence, and legacy-close controls. Notifications, SLA reminders, generic automatic assignment, and unrelated custom-field synchronization remain outside scope unless explicitly added to `adjunct_automations`.
|
||||
|
||||
For `oneos_delivery`, account for all 48 controls independently:
|
||||
|
||||
- `Y01`–`Y16`, `Y20`–`Y22`, `Y30`–`Y35`, `Y40`: native/mirrored lifecycle controls.
|
||||
- `A01`, `A02`, `A02B`, `A03`–`A10`: AI/Webhook bridge controls.
|
||||
- `TC01`–`TC03`, `DF01`–`DF03`, `CI01`–`CI03`, `L01`–`L02`: test, defect, code/release separation, and legacy-close controls.
|
||||
|
||||
Do not disable the old stage rules until the OneOS migration readiness gate passes: required requirement statuses exist, the main-task identity is filterable or procedurally exclusive, task workflows are ready, bridge owners and idempotency exist, and rollback evidence is saved. `L01` and `L02` remain disabled because they bypass acceptance.
|
||||
|
||||
## Global safety rules
|
||||
|
||||
- Reuse the authenticated browser session or approved semantic connector.
|
||||
- Preserve unrelated rules and user edits.
|
||||
- Never modify an existing production pipeline unless the user authorizes that exact change.
|
||||
- Clone a test pipeline before callback experiments and modify only the clone.
|
||||
- Require the expected current state in every automatic transition.
|
||||
- Separate observed evidence from proposals and inference.
|
||||
- Wait 5–30 seconds for asynchronous events and inspect execution logs.
|
||||
- Stop at CAPTCHA, OTP, login approval, permission elevation, protected-branch ambiguity, or indistinguishable similarly named resources.
|
||||
|
||||
## Required outputs
|
||||
|
||||
Return only the applicable artifacts:
|
||||
|
||||
1. Observed inventory and current-versus-target matrix.
|
||||
2. Per-project R01–R12 and TK01–TK08 rule-instance inventory with a 31-control ledger.
|
||||
3. Live change log with reopen verification.
|
||||
4. Test result per transition: `通过`、`异步通过`、`未触发`、`误触发` or `阻塞`.
|
||||
5. Remaining risks, manual gates, and rollback steps.
|
||||
6. When requested, a role-and-trigger communication playbook with copyable prompts, required evidence, next owner, and stop conditions.
|
||||
7. For daily commands, answer briefly with the work item, actual state change, created/linked asset, blocker if any, and the exact next short command.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "云效需求生命周期自动化"
|
||||
short_description: "使用口语化短口令驱动云效需求开发测试发布闭环并自动核查"
|
||||
default_prompt: "使用 $yunxiao-requirement-lifecycle,把当前项目设为统一运营管理平台PC端,然后通过“记录需求、开始开发、提交测试、验收通过”等短口令协作。"
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"verified_at": "2026-07-21",
|
||||
"verified_by_example": {
|
||||
"requirement_serial": "ONEOS-88",
|
||||
"requirement_identifier": "a34e700286d456ee74ddc16e58",
|
||||
"note": "标签 API:PATCH /workitem/workitem/{id} propertyKey=tag operateType=COVER;全量标签来自 space/tag/search(spaceType=Space)"
|
||||
},
|
||||
"project": {
|
||||
"name_aliases": ["统一运营管理平台", "统一运营管理平台PC端", "统一运营管理平台 PC 端"],
|
||||
"spaceIdentifier": "1280be963a5a2cc126a4118dca",
|
||||
"customCode": "ONEOS",
|
||||
"spaceType": "Project"
|
||||
},
|
||||
"people": {
|
||||
"wangmian": {
|
||||
"displayName": "王冕",
|
||||
"identifier": "6811df000601d2fea60144a9"
|
||||
},
|
||||
"hefei": {
|
||||
"displayName": "何斐",
|
||||
"identifier": "695f0400562f09713f9c3a93"
|
||||
}
|
||||
},
|
||||
"workitem_types": {
|
||||
"product_req": {
|
||||
"name": "产品类需求",
|
||||
"category": "Req",
|
||||
"identifier": "9uy29901re573f561d69jn40"
|
||||
},
|
||||
"task": {
|
||||
"name": "任务",
|
||||
"category": "Task",
|
||||
"identifier": "ba102e46bc6a8483d9b7f25c"
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"紧急": "646004e97f54bb77fec7b455df",
|
||||
"高": "95b89e0a524d9693e1f335ffe5",
|
||||
"中": "fa155d1214f9f8db222d39db3b",
|
||||
"低": "92924feff9c1085891e7511872"
|
||||
},
|
||||
"fields": {
|
||||
"plan_start": {
|
||||
"fieldIdentifier": "79",
|
||||
"name": "计划开始时间",
|
||||
"value_shape": "epoch_ms_string_china_noon",
|
||||
"example": "Date.parse('YYYY-MM-DDT12:00:00+08:00') 转 String,避免 00:00 显示成前一天"
|
||||
},
|
||||
"plan_end": {
|
||||
"fieldIdentifier": "80",
|
||||
"name": "计划完成时间"
|
||||
},
|
||||
"tag": {
|
||||
"fieldIdentifier": "tag",
|
||||
"name": "标签",
|
||||
"propertyKey": "tag"
|
||||
}
|
||||
},
|
||||
"status_ui_paths": {
|
||||
"from_待处理": {
|
||||
"分析中": ["分析中"],
|
||||
"设计中": ["设计中"],
|
||||
"设计完成": ["设计完成"],
|
||||
"开发中": ["设计完成", "待开发", "开发中"],
|
||||
"note": "待处理下拉无「开发中」;须按路径连跳。状态 identifier 未在 UI 路径中固化,优先 UI 三跳;有可靠 API 后再补 statusIdentifier。"
|
||||
}
|
||||
},
|
||||
"assignee_rules": {
|
||||
"分析中": "creator",
|
||||
"设计中": "creator",
|
||||
"设计完成": "creator",
|
||||
"开发中": "hefei",
|
||||
"待开发": "hefei"
|
||||
},
|
||||
"api": {
|
||||
"create_workitem": {
|
||||
"url": "https://devops.aliyun.com/projex/api/workitem/workitem?_input_charset=utf-8",
|
||||
"methods": ["POST", "PUT"],
|
||||
"do_not_use": ["/projex/api/workitem/workitem/create"],
|
||||
"required_create_fields": [
|
||||
"subject",
|
||||
"spaceType",
|
||||
"spaceIdentifier",
|
||||
"category",
|
||||
"categoryIdentifier",
|
||||
"workitemTypeIdentifier",
|
||||
"workitemType",
|
||||
"assignedTo"
|
||||
],
|
||||
"document_on_create": {
|
||||
"supported": true,
|
||||
"shape": { "content": "<html>", "formatType": "RICHTEXT" },
|
||||
"note": "创建时一并写入描述,避免再开 Cangjie 编辑器"
|
||||
}
|
||||
},
|
||||
"get_workitem": {
|
||||
"url_template": "https://devops.aliyun.com/projex/api/workitem/workitem/{identifier}?_input_charset=utf-8"
|
||||
},
|
||||
"list_workitem": {
|
||||
"url": "https://devops.aliyun.com/projex/api/workitem/workitem/list?_input_charset=utf-8",
|
||||
"method": "POST"
|
||||
},
|
||||
"update_field_value": {
|
||||
"url": "https://devops.aliyun.com/projex/api/workitem/workitem/updateWorkitemFieldValue?_input_charset=utf-8",
|
||||
"method": "PATCH",
|
||||
"note": "部分会话返回 InvalidWorkitem.NotFound;失败则改用详情页 UI onChange / 下拉点选,勿盲目重试超过 1 次"
|
||||
},
|
||||
"update_document": {
|
||||
"preferred": "create 时写入 document;若需补写,用详情页 onWorkItemEditorSubmit(html, 'RICHTEXT')",
|
||||
"avoid": "对 Cangjie 编辑器直接改 DOM / 错误 onChange 形状(曾导致页面崩溃)"
|
||||
},
|
||||
"list_tags": {
|
||||
"url": "https://devops.aliyun.com/projex/api/workspace/space/tag/search?spaceType=Space&spaceIdentifier=1280be963a5a2cc126a4118dca&q=&_input_charset=utf-8",
|
||||
"method": "GET",
|
||||
"note": "注意 spaceType=Space(不是 Project)。刷新全量标签时用此接口覆盖 tags.catalog"
|
||||
},
|
||||
"apply_tags": {
|
||||
"url_template": "https://devops.aliyun.com/projex/api/workitem/workitem/{identifier}?_input_charset=utf-8",
|
||||
"method": "PATCH",
|
||||
"body_shape": {
|
||||
"workitemIdentifier": "{identifier}",
|
||||
"propertyKey": "tag",
|
||||
"propertyValue": "{comma_separated_tag_identifiers}",
|
||||
"operateType": "COVER"
|
||||
},
|
||||
"note": "propertyValue 为标签 identifier 逗号拼接;COVER=覆盖整组。已验证于 ONEOS-88。"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"selection_mode": "plan_or_askquestion_selector",
|
||||
"do_not_match_from": ["lease-business-line-overview/lines.ts", "业务条线说明自动推断"],
|
||||
"apply_strategy": "用户从 catalog 点选标签名 → 用 identifier 调 apply_tags API;失败 1 次再 UI 兜底;仍失败则停并请用户回标签名,禁止假装已打上",
|
||||
"refresh_hint": "标签库变更时重新 GET list_tags 并更新本 catalog / fetched_at",
|
||||
"fetched_at": "2026-07-21",
|
||||
"source": "GET /projex/api/workspace/space/tag/search?spaceType=Space&spaceIdentifier=1280be963a5a2cc126a4118dca&q=",
|
||||
"count": 30,
|
||||
"by_name": {
|
||||
"运维管理条线": "a611b2f2b7c3fd3623ba2d5f52",
|
||||
"报表中心": "8428d15f45a5b34173202db758",
|
||||
"能源管理": "3396ebb527358fe6f89fe42849",
|
||||
"维修站管理": "e7b4da7984801e75630645f437",
|
||||
"充电站管理": "6ea830485f28a0d8a6e14851a8",
|
||||
"还车应结款": "4204960ce658c94b17abb7c5f6",
|
||||
"交车应收款": "6e7a9127d0f70281ca65d335d9",
|
||||
"加氢站管理": "6cdacae673f9e1fba96296f2ad",
|
||||
"保险管理": "578ed5ba12bfcf274721e956f2",
|
||||
"合同管理": "23873be81931cb0dc1bf87a456",
|
||||
"租赁账单": "3e9433f571b20e80b02626d2de",
|
||||
"供应商管理": "7b86b7c1f3b3da21b814c5a39d",
|
||||
"客户管理": "dbf959a70bc979106dc78afa90",
|
||||
"还车任务": "763de323cefee45e5270e4661e",
|
||||
"安全培训": "76988b5f73ef515de5930d59b9",
|
||||
"备件管理": "5d09c864767d898a4f8c0ca804",
|
||||
"停车场管理": "7c522e2fb29213d7d390e78b97",
|
||||
"备车管理": "22c6a3ed0e4bf95fd99bf95695",
|
||||
"异动管理": "84b4333ec7c76d0fa2adcc47bf",
|
||||
"调拨管理": "fa88c16c152c6fb5978e3b4339",
|
||||
"上牌管理": "990af2fb716dd85568bc7019bf",
|
||||
"替换车管理": "8d1decf30259b941016cadc9d2",
|
||||
"还车管理": "1e0fce2d6929ff4ac13b310e97",
|
||||
"交车管理": "b6947b5aca82a8c759612ba039",
|
||||
"车辆管理": "cb71b6db9373d6d8c7aae452a5",
|
||||
"审批中心": "338add796f2221bbc36dd77d35",
|
||||
"故障管理": "ceb526a7343995577645317e9a",
|
||||
"车辆年审": "5193165dad1e91a161502b54c2",
|
||||
"维修管理": "f5202633870ff409e0ebc16d5f",
|
||||
"工作台": "b62d21beac55c0b43389915eab"
|
||||
},
|
||||
"catalog": [
|
||||
{ "name": "运维管理条线", "identifier": "a611b2f2b7c3fd3623ba2d5f52", "color": "#49AEAC" },
|
||||
{ "name": "报表中心", "identifier": "8428d15f45a5b34173202db758", "color": "#49AEAC" },
|
||||
{ "name": "能源管理", "identifier": "3396ebb527358fe6f89fe42849", "color": "#49AEAC" },
|
||||
{ "name": "维修站管理", "identifier": "e7b4da7984801e75630645f437", "color": "#49AEAC" },
|
||||
{ "name": "充电站管理", "identifier": "6ea830485f28a0d8a6e14851a8", "color": "#49AEAC" },
|
||||
{ "name": "还车应结款", "identifier": "4204960ce658c94b17abb7c5f6", "color": "#49AEAC" },
|
||||
{ "name": "交车应收款", "identifier": "6e7a9127d0f70281ca65d335d9", "color": "#49AEAC" },
|
||||
{ "name": "加氢站管理", "identifier": "6cdacae673f9e1fba96296f2ad", "color": "#49AEAC" },
|
||||
{ "name": "保险管理", "identifier": "578ed5ba12bfcf274721e956f2", "color": "#49AEAC" },
|
||||
{ "name": "合同管理", "identifier": "23873be81931cb0dc1bf87a456", "color": "#49AEAC" },
|
||||
{ "name": "租赁账单", "identifier": "3e9433f571b20e80b02626d2de", "color": "#49AEAC" },
|
||||
{ "name": "供应商管理", "identifier": "7b86b7c1f3b3da21b814c5a39d", "color": "#49AEAC" },
|
||||
{ "name": "客户管理", "identifier": "dbf959a70bc979106dc78afa90", "color": "#49AEAC" },
|
||||
{ "name": "还车任务", "identifier": "763de323cefee45e5270e4661e", "color": "#49AEAC" },
|
||||
{ "name": "安全培训", "identifier": "76988b5f73ef515de5930d59b9", "color": "#49AEAC" },
|
||||
{ "name": "备件管理", "identifier": "5d09c864767d898a4f8c0ca804", "color": "#49AEAC" },
|
||||
{ "name": "停车场管理", "identifier": "7c522e2fb29213d7d390e78b97", "color": "#49AEAC" },
|
||||
{ "name": "备车管理", "identifier": "22c6a3ed0e4bf95fd99bf95695", "color": "#49AEAC" },
|
||||
{ "name": "异动管理", "identifier": "84b4333ec7c76d0fa2adcc47bf", "color": "#49AEAC" },
|
||||
{ "name": "调拨管理", "identifier": "fa88c16c152c6fb5978e3b4339", "color": "#49AEAC" },
|
||||
{ "name": "上牌管理", "identifier": "990af2fb716dd85568bc7019bf", "color": "#49AEAC" },
|
||||
{ "name": "替换车管理", "identifier": "8d1decf30259b941016cadc9d2", "color": "#49AEAC" },
|
||||
{ "name": "还车管理", "identifier": "1e0fce2d6929ff4ac13b310e97", "color": "#49AEAC" },
|
||||
{ "name": "交车管理", "identifier": "b6947b5aca82a8c759612ba039", "color": "#49AEAC" },
|
||||
{ "name": "车辆管理", "identifier": "cb71b6db9373d6d8c7aae452a5", "color": "#49AEAC" },
|
||||
{ "name": "审批中心", "identifier": "338add796f2221bbc36dd77d35", "color": "#49AEAC" },
|
||||
{ "name": "故障管理", "identifier": "ceb526a7343995577645317e9a", "color": "#49AEAC" },
|
||||
{ "name": "车辆年审", "identifier": "5193165dad1e91a161502b54c2", "color": "#49AEAC" },
|
||||
{ "name": "维修管理", "identifier": "f5202633870ff409e0ebc16d5f", "color": "#49AEAC" },
|
||||
{ "name": "工作台", "identifier": "b62d21beac55c0b43389915eab", "color": "#49AEAC" }
|
||||
]
|
||||
},
|
||||
"performance_rules": {
|
||||
"prefer_api_over_browser": true,
|
||||
"max_endpoint_probes_per_step": 1,
|
||||
"screenshot_only_at": ["after_create", "after_final_status"],
|
||||
"browser_fallback_for": ["tag_apply_when_api_fails", "status_multi_hop_when_api_unknown"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# 统一运营管理平台 · 云效标签清单(选择器用)
|
||||
|
||||
来源:`GET /projex/api/workspace/space/tag/search?spaceType=Space&spaceIdentifier=1280be963a5a2cc126a4118dca&q=`
|
||||
抓取时间:2026-07-21
|
||||
机器可读:同目录 `oneos-pc-runtime-ids.json` → `tags`
|
||||
|
||||
## 使用约定
|
||||
|
||||
- **记录需求**时标签由用户用选择器点选(`AskQuestion` / Plan ○),**不要**再按 `lines.ts` 业务条线说明自动推断。
|
||||
- Agent 用点选到的 `name` 查 `tags.by_name` → `identifier`,再 `PATCH` 打标。
|
||||
- 完整建单提示词(含 A/B/C 与 30 标签枚举):[references/oneos-pc-record-requirement-prompt.md](../references/oneos-pc-record-requirement-prompt.md)
|
||||
|
||||
## 全量标签(30)
|
||||
|
||||
| # | 标签名 | identifier |
|
||||
|---|---|---|
|
||||
| 1 | 运维管理条线 | `a611b2f2b7c3fd3623ba2d5f52` |
|
||||
| 2 | 报表中心 | `8428d15f45a5b34173202db758` |
|
||||
| 3 | 能源管理 | `3396ebb527358fe6f89fe42849` |
|
||||
| 4 | 维修站管理 | `e7b4da7984801e75630645f437` |
|
||||
| 5 | 充电站管理 | `6ea830485f28a0d8a6e14851a8` |
|
||||
| 6 | 还车应结款 | `4204960ce658c94b17abb7c5f6` |
|
||||
| 7 | 交车应收款 | `6e7a9127d0f70281ca65d335d9` |
|
||||
| 8 | 加氢站管理 | `6cdacae673f9e1fba96296f2ad` |
|
||||
| 9 | 保险管理 | `578ed5ba12bfcf274721e956f2` |
|
||||
| 10 | 合同管理 | `23873be81931cb0dc1bf87a456` |
|
||||
| 11 | 租赁账单 | `3e9433f571b20e80b02626d2de` |
|
||||
| 12 | 供应商管理 | `7b86b7c1f3b3da21b814c5a39d` |
|
||||
| 13 | 客户管理 | `dbf959a70bc979106dc78afa90` |
|
||||
| 14 | 还车任务 | `763de323cefee45e5270e4661e` |
|
||||
| 15 | 安全培训 | `76988b5f73ef515de5930d59b9` |
|
||||
| 16 | 备件管理 | `5d09c864767d898a4f8c0ca804` |
|
||||
| 17 | 停车场管理 | `7c522e2fb29213d7d390e78b97` |
|
||||
| 18 | 备车管理 | `22c6a3ed0e4bf95fd99bf95695` |
|
||||
| 19 | 异动管理 | `84b4333ec7c76d0fa2adcc47bf` |
|
||||
| 20 | 调拨管理 | `fa88c16c152c6fb5978e3b4339` |
|
||||
| 21 | 上牌管理 | `990af2fb716dd85568bc7019bf` |
|
||||
| 22 | 替换车管理 | `8d1decf30259b941016cadc9d2` |
|
||||
| 23 | 还车管理 | `1e0fce2d6929ff4ac13b310e97` |
|
||||
| 24 | 交车管理 | `b6947b5aca82a8c759612ba039` |
|
||||
| 25 | 车辆管理 | `cb71b6db9373d6d8c7aae452a5` |
|
||||
| 26 | 审批中心 | `338add796f2221bbc36dd77d35` |
|
||||
| 27 | 故障管理 | `ceb526a7343995577645317e9a` |
|
||||
| 28 | 车辆年审 | `5193165dad1e91a161502b54c2` |
|
||||
| 29 | 维修管理 | `f5202633870ff409e0ebc16d5f` |
|
||||
| 30 | 工作台 | `b62d21beac55c0b43389915eab` |
|
||||
|
||||
## 选择器文案(C 段)
|
||||
|
||||
已并入 [oneos-pc-record-requirement-prompt.md](../references/oneos-pc-record-requirement-prompt.md)「用户口令」;此处仅作 identifier 对照表。
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"flow_model": "oneos_delivery",
|
||||
"organization": "请填写组织名称",
|
||||
"work_item_type": "产品类需求",
|
||||
"target_lifecycle": [
|
||||
"待处理", "已确认", "分析中", "分析完成", "设计中", "设计完成",
|
||||
"待开发", "开发中", "开发完成", "待测试", "测试中", "测试完成",
|
||||
"发布中", "发布完成", "发布失败", "已关闭"
|
||||
],
|
||||
"task_labels": ["交付", "开发", "测试", "发版"],
|
||||
"policies": {
|
||||
"require_formal_relations": true,
|
||||
"require_current_state_condition": true,
|
||||
"allow_automatic_final_close_without_acceptance": false,
|
||||
"require_nonzero_development_tasks": true,
|
||||
"test_success_is_production_release": false,
|
||||
"bridge_idempotency_key_components": ["项目ID", "需求ID", "动作类型"],
|
||||
"require_signed_timestamped_replay_protected_callbacks": true
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"name": "请填写项目名称",
|
||||
"migration_phase": "P0",
|
||||
"requirement_statuses_observed": ["请填写当前全部需求状态"],
|
||||
"task_workflow_mode": "B",
|
||||
"task_identity_mode": "task_label",
|
||||
"native_rule_can_filter_related_task_identity": false,
|
||||
"main_task_prefix": "【交付】",
|
||||
"development_task_prefix": "【开发】",
|
||||
"test_task_prefix": "【测试】",
|
||||
"release_task_prefix": "【发版】",
|
||||
"repositories": [
|
||||
{
|
||||
"name": "请填写仓库名称",
|
||||
"role": "frontend",
|
||||
"integration_branch": "develop",
|
||||
"codeup_integrated": true,
|
||||
"required_for_mr_gate": true,
|
||||
"evidence": "请填写观察证据"
|
||||
}
|
||||
],
|
||||
"test_plan": {
|
||||
"name": "请填写测试计划名称",
|
||||
"iteration_formally_related": true,
|
||||
"cases_partitioned_by_requirement": true,
|
||||
"fixed_defect_is_terminal": false,
|
||||
"tester_retest_required": true,
|
||||
"evidence": "请填写观察证据"
|
||||
},
|
||||
"release": {
|
||||
"release_task_formally_related_to_iteration": true,
|
||||
"production_evidence_separate_from_test": true,
|
||||
"acceptance_required_after_release": true,
|
||||
"production_pipeline_unchanged": true,
|
||||
"evidence": "请填写观察证据"
|
||||
},
|
||||
"legacy_rules": [
|
||||
{
|
||||
"name": "请填写旧规则显示名",
|
||||
"disposition": "keep_until_ready",
|
||||
"enabled": true,
|
||||
"reason": "请填写保留、停用或替换原因",
|
||||
"evidence": "请填写观察证据"
|
||||
}
|
||||
],
|
||||
"controls": [
|
||||
{"id":"Y01","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接创建并关联唯一主任务","evidence":"请填写观察证据"},
|
||||
{"id":"Y02","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步分析完成","evidence":"请填写观察证据"},
|
||||
{"id":"Y03","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步设计中","evidence":"请填写观察证据"},
|
||||
{"id":"Y04","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步设计完成","evidence":"请填写观察证据"},
|
||||
{"id":"Y05","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步待开发","evidence":"请填写观察证据"},
|
||||
{"id":"Y06","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步开发中","evidence":"请填写观察证据"},
|
||||
{"id":"Y07","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步开发完成","evidence":"请填写观察证据"},
|
||||
{"id":"Y08","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步待测试","evidence":"请填写观察证据"},
|
||||
{"id":"Y09","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步测试中","evidence":"请填写观察证据"},
|
||||
{"id":"Y10","mode":"not_supported","enabled":false,"owner":"请填写测试负责人","fallback":"测试负责人按用例和缺陷闭环同步测试完成","evidence":"请填写观察证据"},
|
||||
{"id":"Y11","mode":"not_supported","enabled":false,"owner":"请填写发布负责人","fallback":"发版任务提交后人工同步发布中","evidence":"请填写观察证据"},
|
||||
{"id":"Y12","mode":"not_supported","enabled":false,"owner":"请填写发布负责人","fallback":"核对生产发布证据后人工同步发布完成","evidence":"请填写观察证据"},
|
||||
{"id":"Y13","mode":"not_supported","enabled":false,"owner":"请填写发布负责人","fallback":"人工记录发布失败及原因","evidence":"请填写观察证据"},
|
||||
{"id":"Y14","mode":"manual","enabled":true,"owner":"请填写产品负责人","fallback":"产品验收后人工关闭","evidence":"请填写观察证据"},
|
||||
{"id":"Y15","mode":"not_supported","enabled":false,"owner":"请填写项目管理员","fallback":"只保留一个权威镜像方向","evidence":"请填写观察证据"},
|
||||
{"id":"Y16","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"快轨时桥接同步主任务","evidence":"请填写观察证据"},
|
||||
{"id":"Y20","mode":"native_rule","enabled":true,"owner":"请填写研发负责人","fallback":"开发负责人人工开始任务和需求","evidence":"请填写观察证据"},
|
||||
{"id":"Y21","mode":"integration_bridge","enabled":false,"owner":"请填写研发负责人","fallback":"人工核对全部开发任务和MR","evidence":"请填写观察证据"},
|
||||
{"id":"Y22","mode":"integration_bridge","enabled":false,"owner":"请填写项目管理员","fallback":"人工创建并关联唯一测试任务","evidence":"请填写观察证据"},
|
||||
{"id":"Y30","mode":"integration_bridge","enabled":false,"owner":"请填写测试负责人","fallback":"人工同步待测试","evidence":"请填写观察证据"},
|
||||
{"id":"Y31","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"测试负责人开始测试","evidence":"请填写观察证据"},
|
||||
{"id":"Y32","mode":"integration_bridge","enabled":false,"owner":"请填写测试负责人","fallback":"按需求用例包人工验收","evidence":"请填写观察证据"},
|
||||
{"id":"Y33","mode":"integration_bridge","enabled":false,"owner":"请填写发布负责人","fallback":"人工提交发版任务并同步范围","evidence":"请填写观察证据"},
|
||||
{"id":"Y34","mode":"integration_bridge","enabled":false,"owner":"请填写流水线负责人","fallback":"人工核对生产发布成功","evidence":"请填写观察证据"},
|
||||
{"id":"Y35","mode":"integration_bridge","enabled":false,"owner":"请填写流水线负责人","fallback":"人工记录生产发布失败","evidence":"请填写观察证据"},
|
||||
{"id":"Y40","mode":"manual","enabled":true,"owner":"请填写产品负责人","fallback":"人工关联迭代","evidence":"请填写观察证据"},
|
||||
{"id":"A01","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"人工上传并回写链接","evidence":"请填写观察证据"},
|
||||
{"id":"A02","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"人工维护更新内容历史","evidence":"请填写观察证据"},
|
||||
{"id":"A02B","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"人工维护需求说明","evidence":"请填写观察证据"},
|
||||
{"id":"A03","mode":"integration_bridge","enabled":false,"owner":"请填写项目管理员","fallback":"人工建需求和唯一主任务","evidence":"请填写观察证据"},
|
||||
{"id":"A04","mode":"integration_bridge","enabled":false,"owner":"请填写研发负责人","fallback":"人工拆分开发任务","evidence":"请填写观察证据"},
|
||||
{"id":"A05","mode":"integration_bridge","enabled":false,"owner":"请填写研发负责人","fallback":"人工核对MR并完成开发任务","evidence":"请填写观察证据"},
|
||||
{"id":"A06","mode":"integration_bridge","enabled":false,"owner":"请填写项目管理员","fallback":"人工建唯一测试任务","evidence":"请填写观察证据"},
|
||||
{"id":"A07","mode":"integration_bridge","enabled":false,"owner":"请填写测试负责人","fallback":"人工核对用例与缺陷闭环","evidence":"请填写观察证据"},
|
||||
{"id":"A08","mode":"integration_bridge","enabled":false,"owner":"请填写发布负责人","fallback":"人工建发版任务和更新说明","evidence":"请填写观察证据"},
|
||||
{"id":"A09","mode":"integration_bridge","enabled":false,"owner":"请填写流水线负责人","fallback":"人工回写发布成败","evidence":"请填写观察证据"},
|
||||
{"id":"A10","mode":"manual","enabled":true,"owner":"请填写产品负责人","fallback":"产品验收后人工关闭","evidence":"请填写观察证据"},
|
||||
{"id":"TC01","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"逐条执行并记录用例结果","evidence":"请填写观察证据"},
|
||||
{"id":"TC02","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"人工正式关联失败用例和缺陷","evidence":"请填写观察证据"},
|
||||
{"id":"TC03","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"复测后更新原用例状态","evidence":"请填写观察证据"},
|
||||
{"id":"DF01","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"已修复继续等待复测","evidence":"请填写观察证据"},
|
||||
{"id":"DF02","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"复测通过后关闭缺陷并通过用例","evidence":"请填写观察证据"},
|
||||
{"id":"DF03","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"复测失败后重开缺陷并保持用例失败","evidence":"请填写观察证据"},
|
||||
{"id":"CI01","mode":"manual","enabled":true,"owner":"请填写研发负责人","fallback":"人工核对全部前后端MR","evidence":"请填写观察证据"},
|
||||
{"id":"CI02","mode":"not_supported","enabled":false,"owner":"请填写流水线负责人","fallback":"使用test副本评审回调POC","evidence":"请填写观察证据"},
|
||||
{"id":"CI03","mode":"manual","enabled":true,"owner":"请填写发布负责人","fallback":"人工区分test、生产发布和验收","evidence":"请填写观察证据"},
|
||||
{"id":"L01","mode":"disabled_legacy","enabled":false,"owner":"请填写项目管理员","fallback":"保持发布完成等待验收","evidence":"请填写观察证据"},
|
||||
{"id":"L02","mode":"disabled_legacy","enabled":false,"owner":"请填写项目管理员","fallback":"保持发布完成等待验收","evidence":"请填写观察证据"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"schema_version": 3,
|
||||
"organization": "请填写组织名称",
|
||||
"work_item_type": "产品类需求",
|
||||
"lifecycle": [
|
||||
"待处理",
|
||||
"已确认",
|
||||
"分析中",
|
||||
"分析完成",
|
||||
"设计中",
|
||||
"设计完成",
|
||||
"待开发",
|
||||
"开发中",
|
||||
"开发完成",
|
||||
"测试中",
|
||||
"测试完成",
|
||||
"完成发布",
|
||||
"已完成"
|
||||
],
|
||||
"requirement_labels": ["分析", "设计", "开发", "测试"],
|
||||
"automation_policy": {
|
||||
"require_current_state_condition": true,
|
||||
"inspect_rule_order_and_cascades": true,
|
||||
"allow_automatic_final_close_without_acceptance": false,
|
||||
"zero_related_items_are_complete": false,
|
||||
"development_start_event_candidates": ["添加关联分支", "正式关联代码资产"],
|
||||
"require_code_assets_linked_to_requirement_and_development_task": true,
|
||||
"stage_task_creation_idempotency_key_components": ["项目ID", "需求ID", "阶段类型"]
|
||||
},
|
||||
"test_closure_policy": {
|
||||
"accepted_case_states": ["已通过"],
|
||||
"rejected_case_states": ["未执行", "待测试", "阻塞", "未通过"],
|
||||
"defect_fixed_is_terminal": false,
|
||||
"require_tester_retest": true
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"name": "请填写项目名称",
|
||||
"work_item_type": "产品类需求",
|
||||
"release_status": "完成发布",
|
||||
"final_status": "已完成",
|
||||
"labels_observed": ["分析", "设计", "开发", "测试"],
|
||||
"repositories": [
|
||||
{
|
||||
"name": "请填写仓库名称",
|
||||
"role": "frontend",
|
||||
"integration_branch": "dev",
|
||||
"codeup_integrated": true,
|
||||
"required_for_mr_gate": true,
|
||||
"evidence": "请填写仓库与需求正式关联的观察证据"
|
||||
}
|
||||
],
|
||||
"test_pipeline": {
|
||||
"name": "请填写test流水线名称",
|
||||
"environment": "test",
|
||||
"is_clone_for_callback_poc": true,
|
||||
"original_pipeline_unchanged": true,
|
||||
"docker_success_callback_enabled": false,
|
||||
"callback_security": {
|
||||
"signed": false,
|
||||
"timestamp_checked": false,
|
||||
"replay_protected": false,
|
||||
"idempotent_by_execution_id": false,
|
||||
"failure_retry_tested": false
|
||||
},
|
||||
"evidence": "请填写流水线观察证据"
|
||||
},
|
||||
"release_evidence": {
|
||||
"mode": "native_release_change",
|
||||
"target_environment": "请填写目标发布环境",
|
||||
"test_success_is_production_release": false,
|
||||
"evidence": "请填写发布证据"
|
||||
},
|
||||
"test_plan": {
|
||||
"name": "请填写测试计划名称",
|
||||
"requirement_formally_related": true,
|
||||
"case_states_observed": ["未执行", "已通过", "未通过", "阻塞"],
|
||||
"evidence": "请填写测试计划与用例状态证据"
|
||||
},
|
||||
"defect_workflow": {
|
||||
"fixed_status": "已修复",
|
||||
"fixed_is_terminal": false,
|
||||
"tester_retest_required": true,
|
||||
"closed_status": "请填写缺陷真实关闭状态",
|
||||
"reopen_status": "请填写缺陷复测失败后的状态",
|
||||
"evidence": "请填写缺陷工作流证据"
|
||||
},
|
||||
"rule_instances": [
|
||||
{"id": "R01", "mode": "manual", "actual_rule_name": "人工门禁:需求确认", "trigger": "负责人确认", "source_status": "待处理", "target_status": "已确认", "conditions": ["范围、负责人和验收口径明确"], "action": "人工变更状态为已确认", "order": 1, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R02", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "已确认", "target_status": "分析中", "conditions": ["当前状态=已确认", "需求自身标签包含分析"], "action": "变更状态为分析中", "order": 2, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R03", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联任务工作项状态变化", "source_status": "分析中", "target_status": "分析完成", "conditions": ["当前状态=分析中", "本阶段分析任务全部完成;若平台不能过滤阶段则记录替代策略"], "action": "变更状态为分析完成", "order": 3, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R04", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "分析完成", "target_status": "设计中", "conditions": ["当前状态=分析完成", "需求自身标签包含设计"], "action": "变更状态为设计中", "order": 4, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R05", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联任务工作项状态变化", "source_status": "设计中", "target_status": "设计完成", "conditions": ["当前状态=设计中", "本阶段设计任务全部完成;若平台不能过滤阶段则记录替代策略"], "action": "变更状态为设计完成", "order": 5, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R06", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "设计完成", "target_status": "待开发", "conditions": ["当前状态=设计完成", "需求自身标签包含开发"], "action": "变更状态为待开发", "order": 6, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R07", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联分支或正式关联代码资产", "source_status": "待开发", "target_status": "开发中", "conditions": ["当前状态=待开发", "仓库已集成", "代码资产正式关联需求"], "action": "变更状态为开发中", "order": 7, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R08", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联合并请求状态变化", "source_status": "开发中", "target_status": "开发完成", "conditions": ["当前状态=开发中", "全部相关前后端合并请求已合并"], "action": "变更状态为开发完成", "order": 8, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R09", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "开发完成", "target_status": "测试中", "conditions": ["当前状态=开发完成", "需求自身标签包含测试"], "action": "变更状态为测试中", "order": 9, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R10", "mode": "manual", "actual_rule_name": "人工门禁:测试验收", "trigger": "测试验收任务完成或获批回调", "source_status": "测试中", "target_status": "测试完成", "conditions": ["当前状态=测试中", "范围内用例全部通过", "失败用例已复测通过", "范围内缺陷已由测试复测并关闭"], "action": "变更状态为测试完成", "order": 10, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R11", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联发布变更完成或获批回调", "source_status": "测试完成", "target_status": "完成发布", "conditions": ["当前状态=测试完成", "目标环境发布证据满足组织口径"], "action": "变更状态为完成发布", "order": 11, "enabled": false, "has_current_state_condition": true, "evidence": "请填写观察证据"},
|
||||
{"id": "R12", "mode": "manual", "actual_rule_name": "人工门禁:业务验收", "trigger": "业务验收", "source_status": "完成发布", "target_status": "已完成", "conditions": ["当前状态=完成发布", "正式验收证据有效"], "action": "人工变更状态为已完成", "order": 12, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}
|
||||
],
|
||||
"task_rule_instances": [
|
||||
{"id": "TK01", "mode": "manual", "actual_rule_name": "人工门禁:设计任务真实开工", "trigger": "负责人开始处理设计任务", "scope": "设计任务", "source_state": "待处理", "conditions": ["任务标签=设计", "任务正式关联需求"], "actions": ["设计任务变为处理中"], "order": 101, "enabled": true, "evidence": "请填写观察证据"},
|
||||
{"id": "TK02", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "设计任务状态变为已完成", "scope": "产品需求", "source_state": "设计中", "conditions": ["本阶段设计任务非零且全部完成"], "actions": ["需求变为设计完成"], "order": 102, "enabled": true, "evidence": "请填写观察证据"},
|
||||
{"id": "TK03", "mode": "integration_bridge", "actual_rule_name": "桥接:设计完成后创建开发任务", "trigger": "需求状态变为设计完成", "scope": "产品需求", "source_state": "设计完成", "conditions": ["不存在同需求同阶段未取消开发任务", "幂等键=项目ID+需求ID+开发"], "actions": ["创建并关联开发任务", "开发任务标签=开发", "开发任务状态=待处理"], "order": 103, "enabled": false, "evidence": "请填写观察证据"},
|
||||
{"id": "TK04", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "开发任务创建并正式关联需求", "scope": "产品需求", "source_state": "设计完成", "conditions": ["需求标签包含开发", "开发任务关系可见"], "actions": ["需求变为待开发"], "order": 104, "enabled": true, "evidence": "请填写观察证据"},
|
||||
{"id": "TK05", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加正式关联分支或代码资产", "scope": "产品需求和开发任务", "source_state": "需求=待开发;开发任务=待处理", "conditions": ["代码资产同时关联需求和开发任务", "仓库已接入项目"], "actions": ["需求变为开发中", "开发任务变为处理中"], "order": 105, "enabled": true, "evidence": "请填写观察证据"},
|
||||
{"id": "TK06", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联合并请求状态变化", "scope": "产品需求和开发任务", "source_state": "需求=开发中;开发任务=处理中", "conditions": ["全部相关前后端MR已合并到真实dev/develop", "MR同时关联需求和开发任务"], "actions": ["需求变为开发完成", "开发任务变为已完成"], "order": 106, "enabled": true, "evidence": "请填写观察证据"},
|
||||
{"id": "TK07", "mode": "integration_bridge", "actual_rule_name": "桥接:test部署成功后创建测试任务", "trigger": "test流水线部署成功", "scope": "产品需求", "source_state": "开发完成", "conditions": ["流水线执行ID未处理", "不存在同需求同阶段未取消测试任务", "回调签名和防重放验证通过"], "actions": ["创建并关联测试任务", "测试任务变为处理中", "关联测试计划和用例", "需求变为测试中"], "order": 107, "enabled": false, "evidence": "请填写观察证据"},
|
||||
{"id": "TK08", "mode": "manual", "actual_rule_name": "人工门禁:测试任务验收闭环", "trigger": "测试验收完成", "scope": "产品需求和测试任务", "source_state": "需求=测试中;测试任务=处理中", "conditions": ["用例全部通过", "失败用例已复测", "范围内缺陷已由测试关闭"], "actions": ["测试任务变为已完成", "需求变为测试完成"], "order": 108, "enabled": true, "evidence": "请填写观察证据"}
|
||||
],
|
||||
"control_coverage": [
|
||||
{"id": "R01", "mode": "manual", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工确认需求范围与验收口径"},
|
||||
{"id": "R02", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入分析中"},
|
||||
{"id": "R03", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工核对本阶段分析任务"},
|
||||
{"id": "R04", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入设计中"},
|
||||
{"id": "R05", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工核对本阶段设计任务"},
|
||||
{"id": "R06", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入待开发"},
|
||||
{"id": "R07", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "开发人员真实开工时人工开始开发"},
|
||||
{"id": "R08", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工核对全部相关MR"},
|
||||
{"id": "R09", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入测试中"},
|
||||
{"id": "R10", "mode": "manual", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写测试负责人", "fallback": "测试负责人验收任务"},
|
||||
{"id": "R11", "mode": "native_rule", "enabled": false, "evidence": "请填写观察证据", "owner": "请填写发布负责人", "fallback": "人工核对发布变更"},
|
||||
{"id": "R12", "mode": "manual", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写产品负责人", "fallback": "产品或业务负责人验收"},
|
||||
{"id": "TK01", "mode": "manual", "enabled": true, "evidence": "请填写设计任务开工证据", "owner": "请填写设计负责人", "fallback": "负责人真实开工时人工改为处理中"},
|
||||
{"id": "TK02", "mode": "native_rule", "enabled": true, "evidence": "请填写设计任务完成汇总证据", "owner": "请填写产品负责人", "fallback": "人工核对非零设计任务全部完成"},
|
||||
{"id": "TK03", "mode": "integration_bridge", "enabled": false, "evidence": "请填写自动创建开发任务能力证据", "owner": "请填写项目管理员", "fallback": "从需求中人工新建并关联唯一开发任务"},
|
||||
{"id": "TK04", "mode": "native_rule", "enabled": true, "evidence": "请填写开发任务关联证据", "owner": "请填写项目管理员", "fallback": "人工核对关系后将需求改为待开发"},
|
||||
{"id": "TK05", "mode": "native_rule", "enabled": true, "evidence": "请填写需求和开发任务双对象开工证据", "owner": "请填写研发负责人", "fallback": "开发人员真实开工时人工更新任务和需求"},
|
||||
{"id": "TK06", "mode": "native_rule", "enabled": true, "evidence": "请填写需求和开发任务双对象完成证据", "owner": "请填写研发负责人", "fallback": "人工核对全部相关MR后完成任务和需求"},
|
||||
{"id": "TK07", "mode": "integration_bridge", "enabled": false, "evidence": "请填写test部署回调和自动创建测试任务证据", "owner": "请填写流水线负责人", "fallback": "test部署成功后人工创建并关联测试任务"},
|
||||
{"id": "TK08", "mode": "manual", "enabled": true, "evidence": "请填写测试任务与需求双闭环证据", "owner": "请填写测试负责人", "fallback": "测试负责人验收后同步完成测试任务和需求"},
|
||||
{"id": "TC01", "mode": "manual", "enabled": true, "evidence": "请填写用例执行状态证据", "owner": "请填写测试负责人", "fallback": "逐条执行并记录用例结果"},
|
||||
{"id": "TC02", "mode": "manual", "enabled": true, "evidence": "请填写失败用例关联缺陷证据", "owner": "请填写测试负责人", "fallback": "人工核对失败用例与缺陷正式关系"},
|
||||
{"id": "TC03", "mode": "manual", "enabled": true, "evidence": "请填写用例复测更新证据", "owner": "请填写测试负责人", "fallback": "复测后更新原用例状态"},
|
||||
{"id": "DF01", "mode": "manual", "enabled": true, "evidence": "请填写已修复交接证据", "owner": "请填写测试负责人", "fallback": "已修复继续等待测试复测"},
|
||||
{"id": "DF02", "mode": "manual", "enabled": true, "evidence": "请填写复测通过关闭证据", "owner": "请填写测试负责人", "fallback": "测试复测通过后关闭缺陷"},
|
||||
{"id": "DF03", "mode": "manual", "enabled": true, "evidence": "请填写复测失败重开证据", "owner": "请填写测试负责人", "fallback": "测试复测失败后重开缺陷"},
|
||||
{"id": "CI01", "mode": "native_rule", "enabled": true, "evidence": "请填写多仓库关联证据", "owner": "请填写研发负责人", "fallback": "人工核对全部前后端MR"},
|
||||
{"id": "CI02", "mode": "not_supported", "enabled": false, "evidence": "请填写流水线能力证据", "owner": "请填写流水线负责人", "fallback": "复制test流水线后评审回调POC"},
|
||||
{"id": "CI03", "mode": "manual", "enabled": true, "evidence": "请填写环境与发布证据", "owner": "请填写发布负责人", "fallback": "人工区分test部署、生产发布与业务验收"},
|
||||
{"id": "L01", "mode": "disabled_legacy", "enabled": false, "evidence": "请填写旧规则盘点证据", "owner": "请填写项目管理员", "fallback": "使用独立业务验收任务"},
|
||||
{"id": "L02", "mode": "disabled_legacy", "enabled": false, "evidence": "请填写旧规则盘点证据", "owner": "请填写项目管理员", "fallback": "保持发布完成状态等待业务验收"}
|
||||
],
|
||||
"adjunct_automations": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# 需求进阶段自动建同名任务(强制 · AI 桥接)
|
||||
|
||||
当**产品类需求**推进到下列状态时,AI(`apply`)必须查重后创建/复用**与需求同名**的任务,并**正式关联**该需求。不得只改需求状态却不建任务。
|
||||
|
||||
## 触发状态与字段规则
|
||||
|
||||
| 需求进入状态 | 任务标题 | 任务标签(右侧基础字段) | 负责人 | 时间字段 |
|
||||
|---|---|---|---|---|
|
||||
| **分析中** | = 需求标题(同名) | `分析` | **需求创建人**姓名 | 沿用需求(见下) |
|
||||
| **设计中** | = 需求标题(同名) | `设计` | **需求创建人**姓名 | 沿用需求(见下) |
|
||||
| **待开发** | = 需求标题(同名) | `交付`(OneOS 主任务);若项目仍用阶段开发标签则用 `开发` | 固定 **何斐** | 沿用需求(见下) |
|
||||
|
||||
### 时间沿用(「标签创建时间沿用需求的」)
|
||||
|
||||
按平台能力尽量对齐,优先级:
|
||||
|
||||
1. 任务 **计划开始时间** = 需求的计划开始时间;若需求无计划开始,则用需求 **创建时间** 的日期。
|
||||
2. 若平台允许写入任务创建时间:任务创建时间 = 需求创建时间。
|
||||
3. 若都不能改创建时间:仍写计划开始;并在任务描述首行注明:`创建时间沿用需求:YYYY-MM-DD HH:mm`。
|
||||
|
||||
### 正式关联(强制)
|
||||
|
||||
- 使用云效**父子项或关联项**把任务挂到**该产品需求**。
|
||||
- 仅标题写需求编号、或只关联其他任务而不关联需求 → **不合格**,必须补关联后再回报成功。
|
||||
- 标题前缀(如 `【分析】`)**不是**关联;本规则默认标题与需求**完全同名**;若项目强制标题前缀,允许 `【分析】{需求标题}`,但仍须正式关联 + 正确标签。
|
||||
|
||||
## 幂等与查重
|
||||
|
||||
幂等键:
|
||||
|
||||
```text
|
||||
项目ID + 需求ID + 阶段标签(分析|设计|交付/开发)
|
||||
```
|
||||
|
||||
执行顺序:
|
||||
|
||||
1. 查询该需求下是否已有**未取消**、同阶段标签、同名(或同名前缀)任务。
|
||||
2. **已存在一条** → 复用:核对正式关联、标签、负责人(待开发须为何斐)、时间字段;缺则补齐;**不新建第二条**。
|
||||
3. **存在多条冲突** → 停止创建,列出任务编号,请人合并后再继续。
|
||||
4. **不存在** → 创建并正式关联,再改需求状态(若口令是「推进到某状态」且任务尚未建好,先建任务再改状态,避免有状态无任务)。
|
||||
|
||||
快轨直接到 **待开发**(跳过分析/设计):只建/复用 **待开发** 那一条同名任务(负责人何斐),不要为跳过的阶段补建分析/设计任务,除非用户明确要求。
|
||||
|
||||
## 与 OneOS 终态模型对齐
|
||||
|
||||
- `oneos_delivery`:待开发阶段的同名任务即唯一 **【交付】主任务**(标题优先与需求同名;标签=`交付`)。分析中/设计中另建同名阶段任务(标签分析/设计),**不**再额外建第二条交付主任务。
|
||||
- `stage_tasks`:分析/设计/开发阶段任务按上表;待开发对应标签=`开发` 的同名任务,负责人何斐(若项目配置了其他默认开发负责人,以用户当次口令为准,口令未写则何斐)。
|
||||
|
||||
## AI 回报模板(短)
|
||||
|
||||
```text
|
||||
需求:ONEOSP-xxx「标题」→ 状态:分析中/设计中/待开发
|
||||
任务:TASK-xxx「同名」;标签:…;负责人:…;计划开始:…;已正式关联需求
|
||||
查重:新建 / 复用
|
||||
```
|
||||
|
||||
## 负向(不得误做)
|
||||
|
||||
- 需求仅到 `已确认` / `待处理`:不自动建分析/设计/待开发任务。
|
||||
- 不得把样式类口头变更当成建任务触发。
|
||||
- 不得在未查重时重复创建同阶段同名任务。
|
||||
- 不得把负责人设错:分析中/设计中≠何斐(除非创建人就是何斐);待开发必须何斐(除非用户当次明确改派)。
|
||||
@@ -0,0 +1,60 @@
|
||||
# Codeup研发资产集成
|
||||
|
||||
## 目录
|
||||
|
||||
1. 仓库覆盖
|
||||
2. 分支策略
|
||||
3. 正式关联
|
||||
4. 提交与合并请求
|
||||
5. CI01多仓库门禁
|
||||
6. 验证场景
|
||||
|
||||
## 1. 仓库覆盖
|
||||
|
||||
前端、后端均可触发研发自动化,前提是仓库接入正确项目,分支/提交/MR正式关联需求,规则没有错误限定到单一仓库。
|
||||
|
||||
逐仓库记录:仓库名、角色、项目集成状态、Webhook、实际集成分支、是否纳入全部MR门禁和观察证据。
|
||||
|
||||
## 2. 分支策略
|
||||
|
||||
- 检查真实集成分支:存在`dev`则优先`dev`,否则使用存在的`develop`。
|
||||
- 从集成分支创建`feature/<WORK-ITEM-ID>`或`fix/<WORK-ITEM-ID>`。
|
||||
- 一个需求涉及多个仓库时,每个仓库可有一个同名活动分支。
|
||||
- 快速迭代仍使用短生命周期分支;合并后是否删除按仓库策略和用户授权处理。
|
||||
- 不直接推送保护分支,不为自动化方便创建不存在的`dev`/`develop`。
|
||||
|
||||
## 3. 正式关联
|
||||
|
||||
关联优先级:
|
||||
|
||||
1. 从需求“代码”区域创建分支并回到需求确认资产数量。
|
||||
2. 在Codeup创建分支/MR时显式选择工作项;存在开发任务时同时关联需求和开发任务。
|
||||
3. 使用以工作项ID结尾的分支名,并在需求页确认自动关联。
|
||||
4. 提交说明关键字仅作兜底,必须回需求页验证。
|
||||
|
||||
需求编号、分支名或MR标题有关键字,不等同已正式关联。
|
||||
|
||||
## 4. 提交与合并请求
|
||||
|
||||
- 提交可带工作项ID增强追溯,但不判定开发完成。
|
||||
- MR目标必须是仓库真实集成分支。
|
||||
- MR显式关联需求和对应开发任务并完成评审。
|
||||
- 只有全部相关MR合并后才观察R08。
|
||||
- 首次提交时间不作为真实开发开始时间;人工“开始开发”为效能主口径。
|
||||
|
||||
## 5. CI01多仓库门禁
|
||||
|
||||
CI01要求所有相关前后端仓库的分支、提交和MR都正式关联需求与对应开发任务。只遗漏一个仓库或只关联需求,平台的“全部MR已合并”就可能只看到部分资产、提前完成需求,或完全无法完成开发任务。
|
||||
|
||||
如果无法可靠聚合所有仓库,保留人工核对门禁,不能声称R08已完全自动化。
|
||||
|
||||
## 6. 验证场景
|
||||
|
||||
| 场景 | 操作 | 期望 |
|
||||
|---|---|---|
|
||||
| 正式关联分支 | 从需求代码区创建真实仓库分支 | 异步进入开发中 |
|
||||
| 非关联分支 | 仅本地创建/推送无关系分支 | 不触发R07 |
|
||||
| 仅有提交 | 提交代码但不合并MR | 不进入开发完成 |
|
||||
| 单仓库MR | 所有相关仓库只有一个MR合并 | 仍保持开发中 |
|
||||
| 多仓库全部合并 | 所有相关MR正式关联并合并 | 进入开发完成 |
|
||||
| 错误集成分支 | MR指向非真实集成分支 | 阻塞并修正,不伪造通过 |
|
||||
@@ -0,0 +1,103 @@
|
||||
# 需求生命周期与项目规则
|
||||
|
||||
## 目录
|
||||
|
||||
1. 状态参数化
|
||||
2. R01–R12规则矩阵
|
||||
3. 阶段入口和阶段完成
|
||||
4. 开发开始与开发完成
|
||||
5. 标签、任务和关键字
|
||||
6. 规则实例与限制
|
||||
|
||||
## 1. 状态参数化
|
||||
|
||||
典型流程为:
|
||||
|
||||
```text
|
||||
待处理 → 已确认 → 分析中 → 分析完成 → 设计中 → 设计完成 → 待开发
|
||||
→ 开发中 → 开发完成 → 测试中 → 测试完成 → 完成发布/发布完成 → 已完成/已关闭
|
||||
```
|
||||
|
||||
逐项目读取真实状态。`完成发布`与`发布完成`、`已完成`与`已关闭`是候选映射,不能静默互换或创建近义重复状态。
|
||||
|
||||
## 2. R01–R12规则矩阵
|
||||
|
||||
| ID | 流转 | 触发事件 | 必要条件 | 动作 | 方式 |
|
||||
|---|---|---|---|---|---|
|
||||
| R01 | 待处理 → 已确认 | 负责人确认 | 范围、负责人、优先级、验收口径明确 | 改为已确认 | 人工 |
|
||||
| R02 | 已确认 → 分析中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`分析` | 改为分析中 | 原生规则 |
|
||||
| R03 | 分析中 → 分析完成 | 关联任务状态变化 | 当前状态;本阶段分析任务全部完成 | 改为分析完成 | 原生/条件化 |
|
||||
| R04 | 分析完成 → 设计中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`设计` | 改为设计中 | 原生规则 |
|
||||
| R05 | 设计中 → 设计完成 | 关联任务状态变化 | 当前状态;本阶段设计任务全部完成 | 改为设计完成 | 原生/条件化 |
|
||||
| R06 | 设计完成 → 待开发 | 添加关联任务工作项 | 当前状态;需求自身标签包含`开发` | 改为待开发 | 原生规则 |
|
||||
| R07 | 待开发 → 开发中 | 添加关联分支或正式关联代码资产 | 当前状态;仓库已集成;资产正式关联 | 改为开发中 | 自动兜底 |
|
||||
| R08 | 开发中 → 开发完成 | 关联合并请求状态变化 | 当前状态;全部相关MR已合并 | 改为开发完成 | 原生规则 |
|
||||
| R09 | 开发完成 → 测试中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`测试` | 改为测试中 | 原生规则 |
|
||||
| R10 | 测试中 → 测试完成 | 测试验收任务或获批回调 | 当前状态;用例、失败复测、缺陷均闭环 | 改为测试完成 | 人工/桥接 |
|
||||
| R11 | 测试完成 → 发布完成状态 | 发布变更完成或获批回调 | 当前状态;目标环境发布证据有效 | 改为真实发布状态 | 原生/桥接 |
|
||||
| R12 | 发布完成状态 → 项目终态 | 业务验收 | 当前状态;验收证据有效 | 改为真实终态 | 默认人工 |
|
||||
|
||||
R05–R10与阶段任务自身状态的双向联动见`task-lifecycle.md`;R10的用例和缺陷口径见`test-defect-closure.md`;R11、R12见`pipeline-release.md`;R07、R08的代码资产口径见`codeup-integration.md`。
|
||||
|
||||
## 3. 阶段入口和阶段完成
|
||||
|
||||
R02、R04、R06、R09同时要求:
|
||||
|
||||
1. 从需求中“新建并关联”或“关联已有”任务工作项。
|
||||
2. 当前状态等于前一阶段完成状态。
|
||||
3. 需求自身包含目标阶段标签。
|
||||
|
||||
如果规则编辑器不能读取需求自身标签,标签只能用于筛选和报表;保留人工入口或使用当前状态+显式关联事件,不能声称标签已成为技术门槛。
|
||||
|
||||
R03、R05首选“本阶段任务全部完成”。若平台只能判断“全部关联任务工作项”,选择一种明确策略:
|
||||
|
||||
- 阶段开始时才创建/关联本阶段任务。
|
||||
- 使用不同工作项类型,并确认规则能按类型过滤。
|
||||
- 使用父子需求拆分阶段。
|
||||
- 保留人工完成动作。
|
||||
|
||||
不得把零个关联任务当作全部完成。使用关联项状态变化触发,并执行零关联项负向测试。
|
||||
|
||||
## 4. 开发开始与开发完成
|
||||
|
||||
R07采用双轨口径:
|
||||
|
||||
- 效能主口径:开发人员真实开工时执行“开始开发”。
|
||||
- 自动兜底:创建或正式关联需求分支/代码资产后进入`开发中`。
|
||||
|
||||
首次提交可能晚于真实开工,不能作为权威开始时间。自动规则必须要求当前状态=`待开发`。
|
||||
|
||||
R08推荐定义:
|
||||
|
||||
```text
|
||||
事件 = 关联合并请求状态变化
|
||||
且当前状态 = 开发中
|
||||
且全部相关合并请求 = 已合并
|
||||
则状态 = 开发完成
|
||||
```
|
||||
|
||||
提交活动不能证明开发完成。全部相关前端、后端MR都必须正式关联并合并到实际集成分支。
|
||||
|
||||
## 5. 标签、任务和关键字
|
||||
|
||||
`分析`、`设计`、`开发`、`测试`标签加在产品类需求自身。
|
||||
|
||||
“添加关联任务工作项”依赖云效正式关系,不依赖标题关键字。有效方式:
|
||||
|
||||
- 在需求详情新建并关联任务。
|
||||
- 在需求详情关联已有任务。
|
||||
- 在任务详情选择该需求为父项/关联项,并在页面确认关系。
|
||||
|
||||
仅写需求编号、粘贴链接或添加同名标签都不能替代正式关联。编号仍建议出现在标题中,方便检索和审计。
|
||||
|
||||
## 6. 规则实例与限制
|
||||
|
||||
每个项目分别记录R01–R12的实际规则显示名/人工门禁、精确触发、源状态、目标状态、全部条件、动作、顺序、启用状态、执行账号和证据。
|
||||
|
||||
已知限制:
|
||||
|
||||
- 规则通常异步执行,5–30秒内不要过早判失败。
|
||||
- “全部关联项”只能看到已集成并正式关联的资产。
|
||||
- 提前创建未来阶段任务可能阻塞“全部任务完成”。
|
||||
- 需求状态规则不会自动更新阶段任务状态;必须同时配置TK01–TK08。
|
||||
- 平台能力因模板、版本和权限不同,以当前UI和执行日志为准。
|
||||
@@ -0,0 +1,133 @@
|
||||
# 线上实施、验证与治理
|
||||
|
||||
## 目录
|
||||
|
||||
1. 安全边界
|
||||
2. 项目盘点
|
||||
3. 规则应用
|
||||
4. 隔离验证
|
||||
5. 故障诊断
|
||||
6. 回滚
|
||||
7. 运行治理
|
||||
|
||||
## 1. 安全边界
|
||||
|
||||
- 只操作明确授权的组织、项目、工作项类型和测试资产。
|
||||
- 复用已登录会话,不记录账号、密码、Cookie、Token、OTP、私钥或签名材料。
|
||||
- 不修改现有prod流水线;回调实验只改test副本。
|
||||
- 不删除测试需求、分支、MR、流水线或规则,除非另有授权。
|
||||
- 保留无关规则和用户修改。
|
||||
|
||||
## 2. 项目盘点
|
||||
|
||||
先记录`flow_model=stage_tasks`或`flow_model=oneos_delivery`。发现两套规则同时启用时判定为迁移中,不按规则名称猜测主模型。
|
||||
|
||||
对每个项目独立记录:
|
||||
|
||||
| 类别 | 必查内容 |
|
||||
|---|---|
|
||||
| 项目 | 精确项目名、项目ID、工作项类型 |
|
||||
| 工作流 | 全部状态、顺序、发布状态、终态 |
|
||||
| 阶段任务 | 设计/开发/测试任务类型、状态、标签、父子/关联关系、自动创建能力 |
|
||||
| 标签 | `分析`、`设计`、`开发`、`测试` |
|
||||
| 规则 | 名称、事件、条件、动作、顺序、启用、执行账号、后继规则 |
|
||||
| Codeup | 前后端仓库、集成、Webhook、真实`dev/develop` |
|
||||
| 流水线 | test/prod名称、环境、部署阶段、副本能力 |
|
||||
| 发布 | 发布变更关系、目标环境、完成状态 |
|
||||
| 测试 | 计划范围、用例状态枚举、需求关系 |
|
||||
| 缺陷 | 已修复、复测、关闭态、重开态、权限 |
|
||||
|
||||
两个项目只复制逻辑,不复制内部ID、仓库、分支、流水线、规则ID或观察证据。
|
||||
|
||||
OneOS终态迁移还要盘点:`待测试/发布中/发布失败`状态缺口、主任务身份方式、任务工作流方案A/B、原生规则能否过滤关联任务身份、Y/A控制负责人和幂等键。门禁未齐时保留可用旧规则,只停用会绕过验收或造成已确认误触发的规则。
|
||||
|
||||
## 3. 规则应用
|
||||
|
||||
1. 导出或截图现有规则和启用状态。
|
||||
2. 识别后继规则和可能的连锁路径。
|
||||
3. 创建缺失阶段标签。
|
||||
4. 一次只创建或修改一条规则。
|
||||
5. 设置精确触发、当前状态、需求标签、其他条件和目标动作。
|
||||
6. 保存后重新打开,核对全部字段、顺序、启用状态和执行账号。
|
||||
7. 把真实字段写回该项目`rule_instances`。
|
||||
8. 触发最小事件,等待5–30秒。
|
||||
9. 查看状态、研发资产、执行日志和二次流转。
|
||||
10. 记录证据后再处理下一条。
|
||||
|
||||
手工推进只能标记为“测试准备动作”,不能伪造规则通过。
|
||||
|
||||
## 4. 隔离验证
|
||||
|
||||
测试需求命名示例:
|
||||
|
||||
```text
|
||||
[自动化测试] 产品类需求生命周期验证-YYYYMMDD
|
||||
```
|
||||
|
||||
| 用例 | 操作 | 期望 | 负向检查 |
|
||||
|---|---|---|---|
|
||||
| T01 | 已确认+分析标签+关联任务 | 分析中 | 无标签不流转 |
|
||||
| T02 | 完成分析任务 | 分析完成 | 零任务不误触发 |
|
||||
| T03 | 设计标签+关联任务 | 设计中 | 仅标题关键字不触发 |
|
||||
| T04 | 完成设计任务 | 设计完成 | 未完成任务阻塞 |
|
||||
| T04a | 设计完成后触发TK03 | 创建且只创建一个开发任务 | 重复事件不得重复创建 |
|
||||
| T04b | 开发任务正式关联需求 | 需求待开发、任务待处理 | 仅标题编号不触发 |
|
||||
| T05 | 开发标签+关联任务 | 待开发 | 错误当前状态不触发 |
|
||||
| T06 | 正式关联真实仓库分支 | 开发中 | 非关联分支不触发 |
|
||||
| T06a | 同一代码资产同时关联需求和开发任务 | 需求开发中、开发任务处理中 | 只关联需求不得声称任务已联动 |
|
||||
| T07 | 前后端各关联MR,只合并一个 | 保持开发中 | 不得提前完成 |
|
||||
| T08 | 全部相关MR合并 | 开发完成 | 仅提交不得完成 |
|
||||
| T08a | 全部相关MR同时关联开发任务并合并 | 开发任务已完成 | 部分仓库MR未合并时阻塞 |
|
||||
| T09 | 测试标签+关联测试任务 | 测试中 | 未来任务不应误阻塞 |
|
||||
| T09a | test流水线部署成功触发TK07 | 创建且只创建一个测试任务并进入处理中 | 流水线失败或重复回调不得创建 |
|
||||
| T10a | 执行全部用例并记录结果 | 每条有明确状态 | 未执行/阻塞/失败阻塞 |
|
||||
| T10b | 失败用例关联缺陷,改为已修复 | 保持测试中 | 未复测不得完成 |
|
||||
| T10c | 复测通过/失败 | 关闭并通过/重开并失败 | 用例缺陷状态一致 |
|
||||
| T11 | 发布变更完成/获批回调 | 发布完成状态 | 重复回调不重复推进 |
|
||||
| T12a | 进入发布完成后等待异步窗口 | 停留发布完成 | L01/L02不得误关闭 |
|
||||
| T12b | 完成正式业务验收 | 项目终态 | test或发布不能替代验收 |
|
||||
|
||||
结果只使用:`通过`、`异步通过`、`未触发`、`误触发`、`阻塞`。
|
||||
|
||||
## 5. 故障诊断
|
||||
|
||||
按顺序检查:
|
||||
|
||||
1. 事件是否真实发生,而不是只有相似标题/标签。
|
||||
2. 当前状态是否精确匹配。
|
||||
3. 标签是否加在需求自身。
|
||||
4. 任务、分支、MR、发布变更是否正式关联。
|
||||
5. 仓库是否接入正确项目,Webhook是否有效。
|
||||
6. 全部相关MR是否合并到真实集成分支。
|
||||
7. 规则是否启用,执行账号是否有权限。
|
||||
8. 执行日志是未匹配、失败还是排队。
|
||||
9. 用例是否仍有未执行、阻塞或失败。
|
||||
10. 缺陷是否仅到已修复,尚未复测关闭。
|
||||
11. 目标状态是否触发后继/重复规则。
|
||||
12. 是否发生`测试完成 → 发布完成 → 终态`瞬时连锁。
|
||||
13. 是否等待合理异步窗口。
|
||||
14. 是否误用另一个项目的内部ID或证据。
|
||||
15. 缺陷与原用例在复测后的状态是否一致。
|
||||
16. 需求状态变化是否遗漏对应阶段任务状态,或任务变化是否遗漏需求汇总。
|
||||
17. 自动创建下一阶段任务是否具有需求ID+阶段类型幂等门禁。
|
||||
18. 项目是否混用R/TK阶段模型与Y/A终态模型。
|
||||
19. OneOS主任务是否唯一且可被规则可靠识别。
|
||||
20. 是否缺少`待测试`、`发布中`、`发布失败`或重试流转边。
|
||||
|
||||
## 6. 回滚
|
||||
|
||||
- 先禁用本次新增规则,再恢复修改前条件和动作。
|
||||
- 保留变更前截图/导出和规则ID。
|
||||
- 回调POC通过禁用副本回滚;删除需单独授权。
|
||||
- 分支/MR等测试资产按仓库策略清理;删除需单独授权。
|
||||
- 不覆盖与本次变更无关的规则。
|
||||
|
||||
## 7. 运行治理
|
||||
|
||||
- 每季度复核规则账号、权限、启用状态和失败日志。
|
||||
- 新增仓库时同步验证项目集成、Webhook和多仓库门禁。
|
||||
- 状态、标签或工作项类型重命名后逐条复核依赖规则。
|
||||
- 保护`master/main`和真实`dev/develop`。
|
||||
- 监控超过7天无活动的需求分支,未经授权不删除。
|
||||
- 分开统计自动流转时间与真实业务开始时间。
|
||||
- 持续检查回调签名、时间戳、防重放、幂等和失败重试。
|
||||
@@ -0,0 +1,117 @@
|
||||
# 统一运营管理平台 · 记录需求快路径
|
||||
|
||||
真相源运行时 ID:[assets/oneos-pc-runtime-ids.json](../assets/oneos-pc-runtime-ids.json)
|
||||
验证样例:ONEOS-84「测试需求」(2026-07-21)。
|
||||
|
||||
**用户可复制完整口令(A/B/C 门禁 + 30 标签):** [oneos-pc-record-requirement-prompt.md](oneos-pc-record-requirement-prompt.md)
|
||||
|
||||
本文件只服务**日常 `记录需求` apply**。全量审计/规则配置仍读 `live-operations.md` 等模块。
|
||||
|
||||
## 0. 执行门禁(强制)
|
||||
|
||||
用户口令要求 Plan / 单选「优先级」「推进至」「标签」时:
|
||||
|
||||
1. **必须**先拿到明确的 A(紧急/高/中/低)、B(分析中/设计中/设计完成/开发中)、**C(标签,可多选)**。
|
||||
2. 标签 **C** 只能从 [assets/oneos-pc-tag-catalog.md](../assets/oneos-pc-tag-catalog.md) / `tags.catalog` 点选;**禁止**按 `lines.ts` 或业务条线说明自动推断。
|
||||
3. 来源优先:`AskQuestion` 点选 → Plan 批准时写明的 A+B+C → 用户一句话点明。
|
||||
4. **禁止**:未选时默认「中 + 分析中」并继续建单;**禁止**未选标签时自作主张打标。
|
||||
5. 「批准/Implement 计划」≠ 已选 A+B+C;计划正文若仍是空 ○,必须先问清再执行。
|
||||
|
||||
多条需求名称时:每条或整批共用的优先级/推进至/标签仍须用户点选后再写云效。
|
||||
|
||||
## 1. 加载配方(禁止重探)
|
||||
|
||||
对项目别名含「统一运营管理平台」时:
|
||||
|
||||
1. 读取 `assets/oneos-pc-runtime-ids.json`。
|
||||
2. **禁止**再探测 create URL(不要用 `/workitem/create`)。
|
||||
3. **禁止**对优先级 ID 做猜测重试超过配方表。
|
||||
|
||||
## 2. 标准执行顺序(目标 1~3 分钟)
|
||||
|
||||
```text
|
||||
AutoPRD → API 建需求(含描述) → API 建阶段任务 → API 打标签(COVER) → 状态连跳 → 一次校验回报
|
||||
```
|
||||
|
||||
### 2.1 AutoPRD
|
||||
|
||||
- 先跑 `$oneos-autoprd`(模块/原型来自口令或对话页)。
|
||||
- 描述 HTML 最小块:`【原型】` + `【目标】` + `【非目标】` + `【更新内容】`(无变更记录则「首版定稿」)。
|
||||
|
||||
### 2.2 API 建产品类需求
|
||||
|
||||
`POST`(失败再试一次 `PUT`)
|
||||
`https://devops.aliyun.com/projex/api/workitem/workitem?_input_charset=utf-8`
|
||||
|
||||
要点:
|
||||
|
||||
- `workitemTypeIdentifier` **与** `workitemType` 都设为产品类需求 ID。
|
||||
- `document: { content: html, formatType: 'RICHTEXT' }` 创建时写入。
|
||||
- `fieldValueList` 带 `priority`、`assignedTo`、可选 `79`(计划开始,中国时区中午 epoch 字符串)。
|
||||
- 开发中:需求负责人用何斐 ID;分析中/设计中/设计完成:创建人 ID。
|
||||
|
||||
### 2.3 API 建同名阶段任务
|
||||
|
||||
同一 create 接口,`category=Task`,`parentIdentifier=需求ID`,标题与需求**完全同名**。
|
||||
|
||||
| 推进至 | 任务标签目标 | 负责人 |
|
||||
|---|---|---|
|
||||
| 分析中 | 分析 | 创建人 |
|
||||
| 设计中 | 设计 | 创建人 |
|
||||
| 开发中 / 待开发 | 开发或交付 | 何斐 |
|
||||
|
||||
查重:同需求 + 同阶段标签 + 未取消 → 复用,不建第二条。详见 [auto-stage-task.md](auto-stage-task.md)。
|
||||
|
||||
### 2.4 状态连跳(开发中)
|
||||
|
||||
从「待处理」到「开发中」UI 固定路径:
|
||||
|
||||
```text
|
||||
待处理 → 设计完成 → 待开发 → 开发中
|
||||
```
|
||||
|
||||
每跳点开状态按钮选目标;不要在「待处理」下拉里找「开发中」(没有)。
|
||||
|
||||
其它推进至:
|
||||
|
||||
- 分析中 / 设计中 / 设计完成:待处理下拉通常可直达。
|
||||
|
||||
### 2.5 标签(选择器点选 + API)
|
||||
|
||||
1. 用户已从 `tags.catalog` 点选标签名(见门禁 C);用 `tags.by_name` 得到 identifier。
|
||||
2. **优先 API**(已验证):
|
||||
|
||||
```http
|
||||
PATCH https://devops.aliyun.com/projex/api/workitem/workitem/{identifier}?_input_charset=utf-8
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"workitemIdentifier": "{identifier}",
|
||||
"propertyKey": "tag",
|
||||
"propertyValue": "{id1,id2}",
|
||||
"operateType": "COVER"
|
||||
}
|
||||
```
|
||||
|
||||
3. API 失败 1 次 → UI 标签选择器勾选 → **确定**;仍失败 → **停止并请用户回复标签名**,禁止报「已打上」。
|
||||
4. **禁止**对照 `lines.ts` / 业务条线说明自动映射;刷新全量标签:`GET` `api.list_tags`(`spaceType=Space`)。
|
||||
|
||||
### 2.6 校验与回报(只做一次)
|
||||
|
||||
核对:编号、链接、状态、优先级、负责人、计划开始、描述含原型、任务父项、标签。
|
||||
截图最多:建单后、终态后各一次。
|
||||
|
||||
## 3. 性能红线
|
||||
|
||||
| 允许 | 禁止 |
|
||||
|---|---|
|
||||
| 配方内 ID / URL | 每步 3+ 端点盲探 |
|
||||
| create 带描述 | 默认先开富文本再粘贴 |
|
||||
| 状态固定三跳 | React fiber 乱调 onChange(易白屏) |
|
||||
| 标签失败问用户 | 标签失败仍声称成功 |
|
||||
| API 失败 1 次改 UI | 同失败 payload 循环重试 |
|
||||
|
||||
## 4. 浏览器会话
|
||||
|
||||
- 复用已登录 `devops.aliyun.com`;不记录 Cookie/Token。
|
||||
- 优先已打开的「统一运营管理平台」项目页;锁 tab 后执行,结束解锁。
|
||||
@@ -0,0 +1,139 @@
|
||||
# 统一运营管理平台 · 记录需求到云效(完整提示词)
|
||||
|
||||
复制下方「用户口令」整段发给 Agent;Agent 须先弹出 Plan / 单选卡让你点选 **A 优先级 + B 推进至 + C 标签**,**未点选前不得建单**。
|
||||
|
||||
---
|
||||
|
||||
## 用户口令(复制整段)
|
||||
|
||||
```text
|
||||
使用 $yunxiao-requirement-lifecycle + $oneos-autoprd
|
||||
记录需求到云效 · 统一运营管理平台
|
||||
|
||||
【需求名】
|
||||
(填写 1 条或多条,例:车辆资产功能优化)
|
||||
|
||||
【描述来源】
|
||||
$oneos-autoprd,原型/模块:(填写,例:oneos-h5-vehicle-assets)
|
||||
|
||||
【已锁定】
|
||||
- 项目:统一运营管理平台
|
||||
- 计划开始:今日
|
||||
- 负责人:分析中 / 设计中 / 设计完成 → 创建人;开发中 → 何斐
|
||||
|
||||
【请你先 Plan 点选,未选完禁止写云效】
|
||||
|
||||
A. 优先级(单选)
|
||||
○ 紧急
|
||||
○ 高
|
||||
○ 中
|
||||
○ 低
|
||||
|
||||
B. 推进至(单选)
|
||||
○ 分析中(建同名「分析」任务,负责人=创建人)
|
||||
○ 设计中(建同名「设计」任务,负责人=创建人)
|
||||
○ 设计完成(只改状态,负责人=创建人;不强制建阶段任务)
|
||||
○ 开发中(建同名开发/交付任务,负责人=何斐;状态连跳:待处理→设计完成→待开发→开发中)
|
||||
|
||||
C. 标签(可多选;只能从下列 30 个云效标签中选,禁止自造、禁止按业务条线/lines.ts 自动推断)
|
||||
○ 运维管理条线
|
||||
○ 报表中心
|
||||
○ 能源管理
|
||||
○ 维修站管理
|
||||
○ 充电站管理
|
||||
○ 还车应结款
|
||||
○ 交车应收款
|
||||
○ 加氢站管理
|
||||
○ 保险管理
|
||||
○ 合同管理
|
||||
○ 租赁账单
|
||||
○ 供应商管理
|
||||
○ 客户管理
|
||||
○ 还车任务
|
||||
○ 安全培训
|
||||
○ 备件管理
|
||||
○ 停车场管理
|
||||
○ 备车管理
|
||||
○ 异动管理
|
||||
○ 调拨管理
|
||||
○ 上牌管理
|
||||
○ 替换车管理
|
||||
○ 还车管理
|
||||
○ 交车管理
|
||||
○ 车辆管理
|
||||
○ 审批中心
|
||||
○ 故障管理
|
||||
○ 车辆年审
|
||||
○ 维修管理
|
||||
○ 工作台
|
||||
|
||||
【我确认 A+B+C 后你再执行】
|
||||
批准 / Implement 计划时,我会写明所选值,例如:
|
||||
优先级:高;推进至:设计完成;标签:运维管理条线
|
||||
|
||||
【执行要求(Agent)】
|
||||
1. 读快路径:references/oneos-pc-fast-path.md + assets/oneos-pc-runtime-ids.json
|
||||
2. 先 $oneos-autoprd 生成描述(【原型】【目标】【非目标】【更新内容】;无增量则「首版定稿」)
|
||||
3. API 建产品类需求(POST /workitem/workitem,创建时写入 document)
|
||||
4. 计划开始=今日;按 B 推进状态;按规则建/复用同名阶段任务
|
||||
5. 标签:用 tags.by_name 查 identifier,PATCH propertyKey=tag operateType=COVER;API 失败 1 次再 UI 兜底
|
||||
6. 一次校验回报:编号、链接、状态、优先级、负责人、计划开始、标签、描述、阶段任务
|
||||
7. 禁止默认「中+分析中」;禁止未选标签时自作主张打标;标签失败不得报「已成功」
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 单条需求示例(填好即用)
|
||||
|
||||
```text
|
||||
使用 $yunxiao-requirement-lifecycle + $oneos-autoprd
|
||||
记录需求到云效 · 统一运营管理平台
|
||||
|
||||
【需求名】
|
||||
测试需求3
|
||||
|
||||
【描述来源】
|
||||
$oneos-autoprd,原型/模块:oneos-h5-vehicle-assets
|
||||
|
||||
【已锁定】
|
||||
- 项目:统一运营管理平台
|
||||
- 计划开始:今日
|
||||
- 负责人:分析中/设计中/设计完成=创建人;开发中=何斐
|
||||
|
||||
请先 Plan 让我点选 A 优先级、B 推进至、C 标签(30 项 catalog,禁止 lines.ts 推断)。
|
||||
我确认后再执行快路径建单。
|
||||
```
|
||||
|
||||
确认回复示例:
|
||||
|
||||
```text
|
||||
优先级:高;推进至:设计完成;标签:运维管理条线
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent 执行清单(内部,勿省略)
|
||||
|
||||
| 步 | 动作 |
|
||||
|---|---|
|
||||
| 0 | 门禁:A+B+C 已明确;Implement ≠ 已选 |
|
||||
| 1 | `$oneos-autoprd` → HTML 描述 |
|
||||
| 2 | `POST …/workitem/workitem` 建需求 + document |
|
||||
| 3 | 字段:priority、assignedTo、79=今日 |
|
||||
| 4 | 若 B∈{分析中,设计中,开发中} → 同名阶段任务(查重复用) |
|
||||
| 5 | 状态推进至 B(开发中三跳) |
|
||||
| 6 | `PATCH …/workitem/{id}` 打标签 COVER |
|
||||
| 7 | 回报 ONEOS-xxx + 链接 + 核对表 |
|
||||
|
||||
标签 API 形态:
|
||||
|
||||
```json
|
||||
{
|
||||
"workitemIdentifier": "{id}",
|
||||
"propertyKey": "tag",
|
||||
"propertyValue": "{identifier1},{identifier2}",
|
||||
"operateType": "COVER"
|
||||
}
|
||||
```
|
||||
|
||||
参考:`assets/oneos-pc-tag-catalog.md`、`assets/oneos-pc-runtime-ids.json` → `tags.by_name`
|
||||
@@ -0,0 +1,192 @@
|
||||
# OneOS终态交付模型与迁移规则
|
||||
|
||||
## 目录
|
||||
|
||||
1. 模型选择与边界
|
||||
2. 工作项和状态
|
||||
3. 原生Y规则
|
||||
4. AI与Webhook桥接
|
||||
5. 迁移就绪门禁
|
||||
6. 分阶段实施
|
||||
7. 验收与负向测试
|
||||
8. 已知风险和回滚
|
||||
|
||||
## 1. 模型选择与边界
|
||||
|
||||
OneOS终态模型以需求为阶段看板唯一真相:
|
||||
|
||||
```text
|
||||
迭代
|
||||
├─ 多个产品类需求
|
||||
├─ 按需求分包的测试计划
|
||||
└─ 一条【发版】任务
|
||||
|
||||
产品类需求
|
||||
└─ 一条【交付】主任务
|
||||
├─ 多条【开发】任务
|
||||
└─ 一条【测试】任务
|
||||
```
|
||||
|
||||
每个项目只能选择一个主模型:
|
||||
|
||||
- `stage_tasks`:分析、设计、开发、测试各建阶段任务,使用R/TK控制。
|
||||
- `oneos_delivery`:一条交付主任务镜像需求,多开发任务汇总,使用Y/A控制。
|
||||
|
||||
不得同时启用两套进场规则。若项目仍有分析/设计标签驱动的R规则,先记录为`legacy_active`,完成迁移就绪检查后再停用。
|
||||
|
||||
## 2. 工作项和状态
|
||||
|
||||
### 2.1 任务身份
|
||||
|
||||
| 任务 | 数量 | 必须关系 | 识别优先级 |
|
||||
|---|---:|---|---|
|
||||
| `【交付】`主任务 | 每需求1条 | 正式关联需求 | 独立工作项类型 > 任务标签`交付` > 标题前缀流程约束 |
|
||||
| `【开发】`任务 | 每需求1..N条 | 正式关联需求和主任务 | 独立类型 > 标签`开发` > 标题前缀 |
|
||||
| `【测试】`任务 | 每需求1条 | 正式关联需求和主任务 | 独立类型 > 标签`测试` > 标题前缀 |
|
||||
| `【发版】`任务 | 每迭代1条 | 正式关联迭代和本批需求 | 独立类型 > 标签`发版` > 标题前缀 |
|
||||
|
||||
标题关键字本身不构成正式关系。规则编辑器不能读取关联任务的类型、标签或标题时,不得声称原生规则能区分主任务和开发任务;使用桥接或人工门禁。
|
||||
|
||||
### 2.2 需求状态
|
||||
|
||||
目标状态顺序:
|
||||
|
||||
```text
|
||||
待处理 → 已确认 → 分析中 → 分析完成 → 设计中 → 设计完成
|
||||
→ 待开发 → 开发中 → 开发完成 → 待测试 → 测试中 → 测试完成
|
||||
→ 发布中 → 发布完成 / 发布失败 → 已关闭
|
||||
```
|
||||
|
||||
允许的快轨至少包括`已确认 → 待开发`和`设计完成 → 待开发`。`发布失败 → 发布中`用于重试。已有近义状态先映射,禁止静默创建`完成发布/发布完成`、`已完成/已关闭`等重复状态。
|
||||
|
||||
### 2.3 任务状态
|
||||
|
||||
- 主任务:首选与需求同名镜像状态。
|
||||
- 开发任务:`待处理 → 处理中 → 已完成`,另有取消态。
|
||||
- 测试任务:`待处理/待测试 → 测试中 → 已完成`。
|
||||
- 发版任务:`待处理 → 发布中 → 发布完成/发布失败`。
|
||||
|
||||
若任务工作流不能承载主任务全量状态,不配置Y02–Y16原生镜像;由桥接更新需求并记录主任务显示口径。
|
||||
|
||||
## 3. 原生Y规则
|
||||
|
||||
统一规则前缀:`[OneOS终态] Yxx ...`。保存后重新打开核对事件、条件、动作、顺序、启用状态和执行账号。
|
||||
|
||||
### 3.1 主任务镜像
|
||||
|
||||
| ID | 触发 | 条件 | 动作 | 默认方式 |
|
||||
|---|---|---|---|---|
|
||||
| Y01 | 新增正式关联主任务 | 主任务可可靠识别;需求为待处理/已确认 | 需求与主任务进入分析中或获批快轨 | 原生/桥接 |
|
||||
| Y02 | 主任务到分析完成 | 正式关联唯一需求 | 需求到分析完成 | 原生 |
|
||||
| Y03 | 主任务到设计中 | 正式关联唯一需求 | 需求到设计中 | 原生 |
|
||||
| Y04 | 主任务到设计完成 | 正式关联唯一需求 | 需求到设计完成 | 原生 |
|
||||
| Y05 | 主任务到待开发 | 正式关联唯一需求 | 需求到待开发;负责人按项目配置 | 原生/桥接 |
|
||||
| Y06 | 主任务到开发中 | 正式关联唯一需求 | 需求到开发中 | 原生 |
|
||||
| Y07 | 主任务到开发完成 | 正式关联唯一需求 | 需求到开发完成 | 原生 |
|
||||
| Y08 | 主任务到待测试 | 正式关联唯一需求 | 需求到待测试 | 原生 |
|
||||
| Y09 | 主任务到测试中 | 正式关联唯一需求 | 需求到测试中 | 原生 |
|
||||
| Y10 | 主任务到测试完成 | Testhub门禁通过 | 需求到测试完成 | 桥接优先 |
|
||||
| Y11 | 主任务到发布中 | 发版任务和迭代范围有效 | 需求到发布中 | 原生/桥接 |
|
||||
| Y12 | 主任务到发布完成 | 生产发布证据有效 | 需求到发布完成 | 桥接 |
|
||||
| Y13 | 主任务到发布失败 | 生产发布失败证据有效 | 需求到发布失败并记录原因 | 桥接 |
|
||||
| Y14 | 主任务到已关闭 | 产品验收证据有效 | 需求到已关闭 | 默认人工 |
|
||||
| Y15 | 需求状态变化 | 存在唯一主任务且无循环风险 | 主任务同名镜像 | 可选原生 |
|
||||
| Y16 | 需求快轨到待开发 | 存在唯一主任务 | 主任务到待开发 | 原生/桥接 |
|
||||
|
||||
每条双向镜像规则必须有防循环条件。平台无法区分“由本规则写入”的事件时,只保留一个权威方向。
|
||||
|
||||
### 3.2 开发任务汇总
|
||||
|
||||
| ID | 触发 | 条件 | 动作 | 默认方式 |
|
||||
|---|---|---|---|---|
|
||||
| Y20 | 新增第一条开发任务或正式关联开发分支 | 需求=待开发;开发任务数量≥1;任务已指派 | 需求和主任务到开发中;对应开发任务处理中 | 原生/桥接 |
|
||||
| Y21 | 开发任务完成 | 非取消开发任务数量≥1且全部完成;全部相关MR已合并 | 需求和主任务到开发完成 | 桥接优先 |
|
||||
| Y22 | 开发完成 | 同需求无未取消测试任务;幂等键未处理 | 创建并关联唯一测试任务,进入待测试 | 桥接 |
|
||||
|
||||
现有的“任务添加分支且标签=开发,待处理→处理中”和“全部MR已合并且标签=开发,处理中→已完成”可作为Y20/Y21的任务侧子规则保留。需求侧“全部MR已合并”只能看到正式关联资产,必须执行多仓库负向测试。
|
||||
|
||||
### 3.3 测试、发布和迭代
|
||||
|
||||
| ID | 触发 | 条件 | 动作 | 默认方式 |
|
||||
|---|---|---|---|---|
|
||||
| Y30 | 创建测试任务 | 正式关联需求和主任务 | 需求/主任务到待测试 | 桥接 |
|
||||
| Y31 | 首条用例执行或测试负责人开工 | 测试任务=待处理/待测试 | 测试任务、需求、主任务到测试中 | 原生/人工 |
|
||||
| Y32 | 测试验收完成 | 本需求用例包全部通过且缺陷闭环 | 三个对象完成测试阶段 | 桥接/人工 |
|
||||
| Y33 | 发版任务提交 | 发版任务正式关联迭代与本批需求 | 发版任务和本批需求到发布中 | 原生/桥接 |
|
||||
| Y34 | 生产发布成功 | 流水线执行、环境、范围、签名和幂等均有效 | 到发布完成 | 桥接 |
|
||||
| Y35 | 生产发布失败 | 同上;失败证据有效 | 到发布失败并记录原因 | 桥接 |
|
||||
| Y40 | 需求到待开发 | 未关联迭代 | 通知产品负责人,不自动写入未知迭代 | 通知/人工 |
|
||||
|
||||
test部署只能允许测试开始,不能触发Y34。发布完成后必须停留,等待产品/业务验收;禁止自动进入已关闭。
|
||||
|
||||
## 4. AI与Webhook桥接
|
||||
|
||||
| ID | 能力 | 最小门禁 |
|
||||
|---|---|---|
|
||||
| A01 | 发布原型/附件并回写需求描述 | 外部存储授权;链接可审计 |
|
||||
| A02 | 生成并保留更新内容历史 | 不覆盖历史段;**OneOS 默认调用 `$oneos-autoprd` 第 10 章「功能变更记录」(自上次定稿以来);旧段挪到「更新内容·历史」** |
|
||||
| A02B | 生成产品可读需求说明 | 不写实现机密;保留来源;**OneOS 默认调用 `$oneos-autoprd` 全文/交付口径写入「需求说明」** |
|
||||
| A03 | 建需求和唯一主任务并快轨待开发 | 幂等查询;正式关系可见;**建单写描述前先完成 A02+A02B(AutoPRD)**;进入待开发时按 [auto-stage-task.md](auto-stage-task.md) 建/复用与需求同名任务,负责人何斐,时间沿用需求 |
|
||||
| A03B | 需求进入分析中/设计中自动建同名阶段任务 | 标签分析/设计;负责人=需求创建人;正式关联;时间沿用需求;与 A03 同一套查重 |
|
||||
| A04 | 拆分多条开发任务 | 负责人、范围和父子关系明确 |
|
||||
| A05 | 按MR证据关闭开发任务 | 所有仓库、目标分支和关联关系完整 |
|
||||
| A06 | 全部开发完成后建唯一测试任务 | 幂等键=`项目ID+需求ID+create_test_task` |
|
||||
| A07 | 按Testhub结果完成测试 | 用例与缺陷口径满足TC/DF控制 |
|
||||
| A08 | 按迭代创建发版任务和更新说明 | 正式关联迭代和本批需求 |
|
||||
| A09 | 发布成败回写 | 验证环境、签名、时间戳、执行ID和范围 |
|
||||
| A10 | 产品验收后关闭 | 明确验收人和证据;不得由发布成功替代 |
|
||||
|
||||
所有桥接必须具备:最小权限服务账号、签名、时间戳、防重放、幂等、失败重试、审计日志和人工回退。不得把未部署的AI能力写成已自动化。
|
||||
|
||||
## 5. 迁移就绪门禁
|
||||
|
||||
只有全部满足时才停用旧R/TK进场规则:
|
||||
|
||||
1. 需求状态已包含`待测试`、`发布中`、`发布失败`,并配置必要流转边。
|
||||
2. 主任务能被可靠识别;若只能靠标题前缀,已明确由桥接处理,不使用无过滤原生镜像。
|
||||
3. 主任务和开发/测试/发版任务的工作流已选定并验证。
|
||||
4. Y/A控制逐项指定`native_rule`、`integration_bridge`、`manual`或`not_supported`。
|
||||
5. A03、A06、A08、A09具备幂等键、负责人和回退方案。
|
||||
6. Codeup前后端仓库、真实`dev/develop`、正式关联和多仓库MR门禁已验证。
|
||||
7. Testhub按需求分包;未执行、阻塞、失败用例和仅“已修复”缺陷会阻塞测试完成。
|
||||
8. test、生产发布、发布变更和产品验收证据已分离。
|
||||
9. 已保存旧规则名称、条件、顺序、启用状态和执行账号证据。
|
||||
10. `L01`、`L02`已停用,发布完成不会瞬时关闭。
|
||||
|
||||
未通过门禁时:保留当前可用规则,只停用会绕过验收或造成误触发的规则,并输出迁移计划。
|
||||
|
||||
## 6. 分阶段实施
|
||||
|
||||
- P0:补齐工作流、唯一主任务、快轨待开发、负责人和旧规则备份。
|
||||
- P1:开发任务拆分、分支/MR双关联、多仓库完成门禁、唯一测试任务。
|
||||
- P2:迭代、Testhub按需求分包、用例和缺陷复测闭环。
|
||||
- P3:发版任务、生产流水线成败回写、发布失败重试、产品验收关闭。
|
||||
|
||||
每阶段先在隔离测试需求验证,再启用下一阶段。新规则不补触发历史事件;历史数据修正必须单独记录。
|
||||
|
||||
## 7. 验收与负向测试
|
||||
|
||||
| ID | 操作 | 期望 | 负向检查 |
|
||||
|---|---|---|---|
|
||||
| V01 | 创建需求和唯一主任务 | 正式关联且不重复 | 重放不得建第二条主任务 |
|
||||
| V02 | 未建开发任务 | 保持待开发 | 主任务本身不得被当作开发任务 |
|
||||
| V03 | 建两条开发任务 | 进入开发中 | 无正式关系或无负责人不触发 |
|
||||
| V04 | 只完成一条开发任务/MR | 保持开发中 | 不得提前完成 |
|
||||
| V05 | 所有开发任务和MR完成 | 开发完成并只建一条测试任务 | 重复回调不重复创建 |
|
||||
| V06 | 同迭代其他需求未测完 | 本需求仍可独立完成测试 | 不按整个计划全绿判断 |
|
||||
| V07 | 用例失败且缺陷仅已修复 | 保持测试中 | 未复测不得完成 |
|
||||
| V08 | 复测通过/失败 | 用例通过+缺陷关闭 / 用例失败+缺陷重开 | 双方状态必须一致 |
|
||||
| V09 | test部署成功 | 允许测试开始 | 不得发布完成 |
|
||||
| V10 | 生产发布成功 | 发布完成并停留 | 不得自动已关闭 |
|
||||
| V11 | 生产发布失败后重试 | 发布失败→发布中 | 原因和执行ID可审计 |
|
||||
| V12 | 产品验收 | 已关闭 | 无验收证据不关闭 |
|
||||
|
||||
## 8. 已知风险和回滚
|
||||
|
||||
- “全部关联任务完成”若无法按任务身份过滤,会把主任务或测试任务纳入,默认交桥接。
|
||||
- 仅标题前缀不能替代正式关系,也不能保证原生规则可过滤。
|
||||
- 需求和主任务双向镜像可能循环,只保留一个权威方向或增加来源防重。
|
||||
- 分支/MR只关联需求而未关联开发任务,会导致任务状态不更新。
|
||||
- 发布完成自动关闭会绕过产品验收;`L01`、`L02`默认停用。
|
||||
|
||||
回滚顺序:先禁用本次新增Y/A入口,恢复旧规则启用状态和顺序,再恢复状态流转边;保留已创建任务、关系和执行日志,删除任何资产需单独授权。
|
||||
@@ -0,0 +1,66 @@
|
||||
# 流水线、发布与最终验收
|
||||
|
||||
## 目录
|
||||
|
||||
1. 环境证据边界
|
||||
2. R11发布完成
|
||||
3. CI02回调POC
|
||||
4. CI03证据分离
|
||||
5. R12最终验收
|
||||
6. L01–L02历史规则
|
||||
7. 连锁触发验证
|
||||
|
||||
## 1. 环境证据边界
|
||||
|
||||
分别记录以下事件,未经批准不得互相替代:
|
||||
|
||||
- test环境部署成功。
|
||||
- 生产环境流水线发布成功。
|
||||
- 云效关联发布变更达到完成状态。
|
||||
- 产品/业务验收完成。
|
||||
|
||||
流水线成功不等于测试用例通过,test成功也不默认等于生产发布完成。
|
||||
|
||||
## 2. R11发布完成
|
||||
|
||||
优先使用“关联发布变更达到项目真实完成状态”作为R11证据。规则要求当前状态=`测试完成`,目标环境和发布证据满足组织口径。
|
||||
|
||||
如果组织明确把test部署定义为“完成发布”,必须记录该口径的批准证据;否则test回调只记录测试部署成功,不推进发布完成。
|
||||
|
||||
## 3. CI02回调POC
|
||||
|
||||
1. 复制现有test流水线并使用明显POC名称。
|
||||
2. 保留原test与prod流水线不变。
|
||||
3. 只在副本Docker部署成功路径末尾添加回调。
|
||||
4. 普通变量传工作项ID、环境、流水线执行ID。
|
||||
5. 密钥变量保存认证材料,不写入仓库、命令输出或文档。
|
||||
6. 请求携带时间戳、签名和执行ID幂等键。
|
||||
7. 服务端校验需求当前状态,只允许预期流转执行一次。
|
||||
8. 测试成功、重复回调、错误签名、超时、部署失败和安全重试。
|
||||
9. POC通过后走推广评审,不直接修改prod。
|
||||
|
||||
## 4. CI03证据分离
|
||||
|
||||
CI03要求项目配置分别保存test、prod、发布变更和验收证据。任何桥接都必须记录实现方式、启用状态、负责人、证据和人工回退。
|
||||
|
||||
回调失败保留失败日志并可安全重试,不得伪造成功。同一执行ID重试不得多次推进需求。
|
||||
|
||||
## 5. R12最终验收
|
||||
|
||||
发布成功不等于业务验收。默认由产品/业务负责人确认后人工进入`已完成`或`已关闭`。只有可靠、可审计的验收任务才允许条件化自动关闭。
|
||||
|
||||
## 6. L01–L02历史规则
|
||||
|
||||
- `L01`:`发布完成 + 全部关联任务完成/关闭 → 项目终态`。默认停用或收紧为独立验收任务,避免早期阶段任务误满足。
|
||||
- `L02`:监听`测试完成 → 发布完成`状态路径并立即进入项目终态。默认停用,防止发布完成成为瞬时状态。
|
||||
|
||||
逐项目记录两条规则是否存在、是否启用、真实条件、顺序和证据;不存在也要有盘点证据。
|
||||
|
||||
## 7. 连锁触发验证
|
||||
|
||||
- 不按规则名称推断行为,逐条读取触发、条件、动作、顺序和执行账号。
|
||||
- 每个目标状态都检查后继规则和重复规则。
|
||||
- R11后至少观察两个时点:刚进入发布完成时,以及等待5–30秒后。
|
||||
- 未经验收自动进入终态时,记录实际后继规则并判定`误触发`。
|
||||
- 跨两级以上流转必须有独立负向测试,不能只验证最终状态。
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# 云效自动化执行报告模板
|
||||
|
||||
```markdown
|
||||
# 云效产品需求全生命周期自动化执行报告
|
||||
|
||||
## 1. 基本信息
|
||||
|
||||
- 组织:
|
||||
- 项目:
|
||||
- 工作项类型:
|
||||
- 流程模型:stage_tasks / oneos_delivery
|
||||
- 迁移阶段(仅oneos_delivery):P0 / P1 / P2 / P3
|
||||
- 执行模式:audit / plan / apply / test / document
|
||||
- 执行时间:
|
||||
- 执行账号:仅记录显示名,不记录凭据
|
||||
|
||||
## 2. 观察到的当前配置
|
||||
|
||||
| 项目 | 生命周期 | 标签 | 仓库/集成分支 | test流水线 | 发布证据 |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## 3. 规则变更
|
||||
|
||||
| 项目 | 规则 | 变更前 | 变更后 | 启用状态 | 复开核验 |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
### 3.0 项目级规则实例
|
||||
|
||||
| 项目 | 规则ID | 云效规则显示名/人工门禁 | 精确触发事件 | 源状态 | 必要条件 | 动作/目标状态 | 顺序 | 启用 | 观察证据 |
|
||||
|---|---|---|---|---|---|---|---:|---|---|
|
||||
|
||||
每个项目必须分别覆盖`R01`–`R12`和`TK01`–`TK08`。不得把一个项目的规则ID、显示名、顺序或证据复制为另一个项目的观察结果。
|
||||
|
||||
### 3.1 完整性台账
|
||||
|
||||
| 控制ID | 对象/流转 | 实现方式 | 启用状态 | 观察证据 | 负责人 | 回退方案 | 结论 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
|
||||
每个项目必须分别覆盖全部31项:`R01`–`R12`、`TK01`–`TK08`、`TC01`–`TC03`、`DF01`–`DF03`、`CI01`–`CI03`、`L01`–`L02`。不得把“规则编辑器不支持”写成“已完成”;应记录为`not_supported`并给出人工门禁或集成桥接方案。
|
||||
|
||||
`oneos_delivery`模型改用48项台账:`Y01`–`Y16`、`Y20`–`Y22`、`Y30`–`Y35`、`Y40`、`A01`、`A02`、`A02B`、`A03`–`A10`、`TC01`–`TC03`、`DF01`–`DF03`、`CI01`–`CI03`、`L01`–`L02`。
|
||||
|
||||
### 3.1a OneOS迁移就绪度
|
||||
|
||||
| 项目 | 目标状态缺口 | 主任务身份方式 | 原生规则能否过滤 | 任务工作流方案 | 桥接幂等/负责人 | 旧规则处置 | 结论 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
|
||||
旧R/TK规则只有在目标状态、任务身份、任务工作流、桥接和回滚证据全部就绪后才能停用。`L01`、`L02`不受此等待条件影响,应保持停用以保护产品验收。
|
||||
|
||||
### 3.2 规则顺序与连锁触发
|
||||
|
||||
| 前置规则 | 目标状态 | 后继规则 | 是否自动二次流转 | 是否绕过验收 | 处理结论 |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## 4. 验证结果
|
||||
|
||||
| 用例 | 触发事件 | 期望状态 | 实际状态 | 结果分类 | 执行日志/证据 |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## 5. 研发资产覆盖
|
||||
|
||||
| 仓库 | dev/develop | 分支关联 | 提交关联 | MR关联 | 全部合并判断 |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## 6. 流水线与发布
|
||||
|
||||
| 流水线 | 环境 | 是否副本 | Docker部署 | 回调 | 是否影响prod |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## 7. 测试计划与缺陷闭环
|
||||
|
||||
| 测试计划 | 用例总数 | 已通过 | 未执行/待测试 | 阻塞 | 未通过 | 结论 |
|
||||
|---|---:|---:|---:|---:|---:|---|
|
||||
|
||||
| 缺陷 | 关联用例/需求 | 修复状态 | 复测结果 | 最终状态 | 证据 |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
| 失败用例 | 关联缺陷 | 缺陷已修复后用例状态 | 复测通过后用例状态 | 复测失败后用例状态 | 证据 |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## 8. 人工节点与剩余风险
|
||||
|
||||
- 真实开发开始时间:
|
||||
- 业务验收:
|
||||
- 发布完成是否被后继规则立即关闭:
|
||||
- 全部关联任务范围:
|
||||
- 多仓库漏关联风险:
|
||||
- 其他:
|
||||
- 范围外通用自动化(通知、指派、SLA、字段同步等):
|
||||
|
||||
## 9. 回滚
|
||||
|
||||
- 禁用新增规则:
|
||||
- 恢复旧规则:
|
||||
- 停用回调POC:
|
||||
- 测试资产清理(需授权):
|
||||
```
|
||||
@@ -0,0 +1,499 @@
|
||||
# 云效全流程角色与触发节点沟通话术
|
||||
|
||||
> 本文是异常处理和精细交接使用的详细版。日常操作优先读取`simple-role-commands.md`,使用“记录需求、开始分析、开始开发、提交测试、测试完成、发布成功、验收通过”等短口令。
|
||||
|
||||
## 目录
|
||||
|
||||
1. 使用规则
|
||||
2. 通用话术骨架
|
||||
3. 项目管理员与流程负责人
|
||||
4. 产品负责人
|
||||
5. 需求分析人员
|
||||
6. 设计人员
|
||||
7. 交付负责人
|
||||
8. 技术负责人
|
||||
9. 开发人员
|
||||
10. 代码评审人员
|
||||
11. 测试负责人和测试人员
|
||||
12. 缺陷修复人员
|
||||
13. 运维与发布负责人
|
||||
14. 业务验收人员
|
||||
15. 集成桥接负责人
|
||||
16. 异常和催办话术
|
||||
17. 节点交接清单
|
||||
|
||||
## 1. 使用规则
|
||||
|
||||
这套话术用于让不同角色把真实业务事件准确告诉Codex,再由Codex审计、规划、执行获批修改、验证或形成报告。默认使用`yunxiao-requirement-lifecycle`。
|
||||
|
||||
每次沟通至少提供:
|
||||
|
||||
- 精确项目名和需求编号。
|
||||
- 当前角色、当前状态和刚刚发生的真实事件。
|
||||
- 关联任务、仓库、分支、MR、测试计划、缺陷或流水线执行ID;没有就明确写“无”。
|
||||
- 希望Codex采用的权限:`audit`、`plan`、`apply`、`test`或`document`。
|
||||
- 期望停在哪个状态,不要只说“继续流转”。
|
||||
|
||||
权限含义:
|
||||
|
||||
- `audit`:只核查和报告,不修改云效。
|
||||
- `plan`:只输出修改方案和影响,不执行。
|
||||
- `apply`:只修改话术中明确列出的项目、工作项或规则。
|
||||
- `test`:使用隔离测试资产触发并收集证据。
|
||||
- `document`:只形成说明、记录或报告。
|
||||
|
||||
必须遵守:
|
||||
|
||||
- 标题、需求编号、分支名或标签不能代替云效正式关联。
|
||||
- 首次提交不代表真实开始开发;人工“开始处理”是效能主口径,分支关联只是自动兜底。
|
||||
- `已修复`只表示交给测试复测,不是缺陷关闭。
|
||||
- test部署成功不等于测试通过,更不等于生产发布完成。
|
||||
- 发布成功后停留`发布完成`,必须有产品或业务验收才能进入`已关闭`。
|
||||
- 未明确授权时,不改生产流水线、不直接推送保护分支、不删除规则或测试资产。
|
||||
- 项目仍为`stage_tasks`时使用R/TK规则;只有迁移门禁通过后才使用完整`oneos_delivery`链路。
|
||||
|
||||
## 2. 通用话术骨架
|
||||
|
||||
### 2.1 请求执行
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle。
|
||||
权限:【audit/plan/apply/test/document】
|
||||
项目:【精确项目名】
|
||||
流程模型:【未知/stage_tasks/oneos_delivery】
|
||||
我的角色:【角色】
|
||||
需求:【需求编号+标题】
|
||||
当前状态:【状态】
|
||||
刚发生的事件:【真实事件】
|
||||
相关资产:【任务/MR/分支/测试计划/缺陷/流水线执行ID;没有写无】
|
||||
期望结果:【目标状态或需要生成的资产】
|
||||
请先核查当前状态和正式关联,再执行授权范围内动作;输出实际动作、触发结果、证据、未完成项和下一责任人。不要用手工改状态冒充自动化通过。
|
||||
```
|
||||
|
||||
### 2.2 只核查为什么没有流转
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
项目:【项目名】,需求:【编号】,当前状态:【状态】。
|
||||
我已执行:【事件】,相关对象:【对象编号或链接】;等待了【秒数】仍未流转。
|
||||
请按事件、当前状态、标签位置、正式关联、规则启用、执行账号、执行日志、后继规则和异步窗口逐项诊断。不要直接替我改状态。
|
||||
```
|
||||
|
||||
### 2.3 请求修改规则
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
只允许修改项目【项目名】中的规则【精确规则名/控制ID】,目标是【目标】。
|
||||
修改前备份触发、条件、动作、顺序、启用状态和执行账号;一次只改一条,保存后重新打开核对,并用隔离需求做正向和负向测试。禁止修改生产流水线及无关规则。
|
||||
```
|
||||
|
||||
## 3. 项目管理员与流程负责人
|
||||
|
||||
### ADM01 项目首次审计或接管
|
||||
|
||||
触发:新项目接入、规则无人维护或准备复制到另一个项目。
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
请审计项目【项目名】,先判断是stage_tasks、oneos_delivery还是迁移中。盘点工作流状态、标签、工作项类型、全部自动化规则、执行账号、前后端仓库、dev/develop、Testhub、缺陷状态、test/prod流水线、发布证据和验收门禁。
|
||||
输出当前—目标差距、规则控制ID映射、风险、P0-P3实施顺序和不能自动化的人工门禁。不要修改线上配置。
|
||||
```
|
||||
|
||||
完成证据:项目画像、规则清单、模型判断、缺口和迁移阶段齐全。
|
||||
|
||||
### ADM02 迁移到OneOS终态模型
|
||||
|
||||
触发:准备从阶段任务切换到交付主任务模型。
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=plan。
|
||||
项目【项目名】准备迁移到oneos_delivery。请检查待测试、发布中、发布失败状态,交付主任务识别方式,开发/测试/发版任务工作流,A03/A06/A08/A09幂等与负责人,多仓库MR门禁、Testhub分包、生产证据和L01/L02状态。
|
||||
只有十项迁移门禁全部通过才给出停用旧R/TK规则清单;否则保留现网主链,只列安全可做项和回滚方案。
|
||||
```
|
||||
|
||||
完成证据:迁移门禁逐项结论,而不是笼统“可以迁移”。
|
||||
|
||||
### ADM03 规则变更后回归
|
||||
|
||||
触发:新建、修改、启停任何生命周期规则。
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=test。
|
||||
项目【项目名】刚修改规则【规则名/控制ID】。请创建或复用隔离测试需求,验证事件【事件】、源状态【状态】、目标状态【状态】,并增加错误状态、无正式关联、部分MR、重复回调或后继连锁等负向场景。
|
||||
结果只能记录为通过、异步通过、未触发、误触发或阻塞,并附执行日志和回滚入口。
|
||||
```
|
||||
|
||||
### ADM04 定期治理
|
||||
|
||||
触发:季度检查、状态/标签重命名、新增仓库或人员离职。
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
请对项目【项目名】执行运行治理检查:规则执行账号和权限、失败日志、状态/标签重命名影响、新增仓库Webhook、多仓库MR覆盖、超过7天无活动分支、回调签名/幂等/重试、L01/L02以及生产和验收证据分离。
|
||||
输出需要立即修复、计划修复和仅观察三类清单。
|
||||
```
|
||||
|
||||
## 4. 产品负责人
|
||||
|
||||
### PO01 新建并确认需求
|
||||
|
||||
触发:需求范围、负责人、优先级和验收标准已明确。
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需要建立需求【标题】。范围:【范围】;不包含:【排除项】;负责人:【姓名】;优先级:【级别】;目标迭代:【迭代】;验收标准:【标准】。
|
||||
请先查重,再创建或完善产品类需求。若项目为oneos_delivery,校验唯一【交付】主任务;若仍为stage_tasks,不要擅自创建主任务。完成后告诉我需求编号、正式关系、当前状态和下一责任人。
|
||||
```
|
||||
|
||||
### PO02 批准进入分析或快轨待开发
|
||||
|
||||
触发:需求确认后决定正常分析,或低风险小改走快轨。
|
||||
|
||||
正常路径:
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】已确认,批准进入分析。使用 yunxiao-requirement-lifecycle,权限=apply。请核查范围、负责人和验收标准齐全,再按当前流程模型触发分析入口;不要跳过缺失信息。
|
||||
```
|
||||
|
||||
快轨路径:
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】批准快轨到待开发,批准人【姓名】,原因【原因】,验收标准【标准】。使用 yunxiao-requirement-lifecycle,权限=apply。请确认快轨边存在、唯一交付主任务已同步且没有循环风险;缺少任一条件就停止并报告。
|
||||
```
|
||||
|
||||
### PO03 需求范围变更
|
||||
|
||||
触发:开发或测试中修改范围。
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=plan。
|
||||
项目【项目名】需求【编号】当前【状态】,拟变更:【新增/删除内容】;原因:【原因】;受影响资产:【开发任务/MR/用例/缺陷/发布范围】。
|
||||
请先做影响分析,列出需要重开或新增的任务、MR、用例和验收项,以及是否应回退需求状态。未经我确认不要直接改状态或删除已有证据。
|
||||
```
|
||||
|
||||
### PO04 产品验收通过并关闭
|
||||
|
||||
触发:生产发布完成,业务验收通过。
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需求【编号】当前为发布完成。验收人【姓名】,验收时间【时间】,验收环境【生产】,验收结果【通过】,证据【链接/附件/记录】。
|
||||
请核查生产发布证据与需求范围一致,并确认不存在未关闭验收项;满足后执行已关闭并回报审计证据。不得用流水线成功替代本次验收。
|
||||
```
|
||||
|
||||
### PO05 验收不通过
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=plan。
|
||||
项目【项目名】需求【编号】生产验收不通过。问题:【问题】;证据:【证据】;影响:【影响】。
|
||||
请创建或关联可追踪缺陷/改进任务,建议应回到的真实状态和责任人;未经批准不要直接关闭需求或覆盖原发布记录。
|
||||
```
|
||||
|
||||
## 5. 需求分析人员
|
||||
|
||||
### BA01 开始分析
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】由我开始分析,实际开始时间【时间】。使用 yunxiao-requirement-lifecycle,权限=apply。请核查当前状态和我的关联任务,将分析任务改为处理中并确认需求进入分析中;如果项目为oneos_delivery,则按主任务权威方向同步,避免双向循环。
|
||||
```
|
||||
|
||||
### BA02 分析完成
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】分析已完成。产物:【需求说明/流程/字段/边界链接】;未决项:【无或列表】;验收口径:【口径】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请先检查分析任务非零且全部完成、产物可访问、未决项已处理,再推进分析完成;回报触发的是原生规则、桥接还是人工门禁。
|
||||
```
|
||||
|
||||
## 6. 设计人员
|
||||
|
||||
### UX01 开始设计
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】开始设计,设计负责人【姓名】,原型地址【地址】,实际开始时间【时间】。使用 yunxiao-requirement-lifecycle,权限=apply。请核查正式关联设计任务和当前状态,再更新设计任务为处理中并确认需求进入设计中。
|
||||
```
|
||||
|
||||
### UX02 设计完成并交接开发
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】设计完成。原型版本【版本】;设计稿【链接】;交互说明【链接】;响应式范围【范围】;已确认人【姓名】;遗留项【无或列表】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请核查设计任务和交付物后完成设计阶段,并检查是否只创建一组正确的开发任务。不要因为标题含“开发”就视为已正式关联。
|
||||
```
|
||||
|
||||
完成证据:设计任务已完成、需求到设计完成;后继开发任务创建成功后才到待开发。
|
||||
|
||||
## 7. 交付负责人
|
||||
|
||||
### DL01 创建或核查唯一交付主任务
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需求【编号】需要建立【交付】主任务,负责人【姓名】。请先按正式关系和交付标签查重;不存在才创建,存在一条则复用,存在多条则停止并列出冲突。主任务必须正式关联需求,不能只靠标题前缀。
|
||||
```
|
||||
|
||||
### DL02 阶段同步异常
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
项目【项目名】需求【编号】状态【需求状态】,交付主任务【任务编号】状态【任务状态】,两者不一致。
|
||||
请判断权威方向、检查Y01-Y16、来源防重和执行日志,说明应补偿哪个对象。不要同时双向写入造成循环。
|
||||
```
|
||||
|
||||
## 8. 技术负责人
|
||||
|
||||
### TL01 拆分开发任务
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需求【编号】已到待开发。请按以下范围拆分开发任务:前端【范围/负责人/仓库】,后端【范围/负责人/仓库】,其他【范围】。每条任务必须正式关联需求和交付主任务、标签=开发、状态=待处理,并记录目标dev/develop分支。查重后创建,禁止重复任务。
|
||||
```
|
||||
|
||||
### TL02 仅前端或仅后端需求
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】只涉及【前端/后端】,不涉及【另一端】。使用 yunxiao-requirement-lifecycle,权限=apply。请只创建实际需要的开发任务,并把不涉及的仓库明确记为不适用;不要用缺少另一端MR阻塞,也不要把未盘点仓库默认为已完成。
|
||||
```
|
||||
|
||||
### TL03 开发完成门禁核查
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
项目【项目名】需求【编号】准备判定开发完成。相关仓库:【列表】;开发任务:【列表】;MR:【列表及目标分支】。
|
||||
请确认所有非取消开发任务完成、所有相关MR正式双关联并合并到真实dev/develop;任一仓库未完成都保持开发中。输出缺口,不要手工改成开发完成。
|
||||
```
|
||||
|
||||
## 9. 开发人员
|
||||
|
||||
### DEV01 真实开始开发并创建分支
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
我开始处理项目【项目名】需求【编号】的开发任务【任务编号】,真实开始时间【时间】,仓库【仓库名】,基线【dev/develop】,拟建分支【feature/编号或fix/编号】。
|
||||
请先确认基线真实存在且任务为待处理,再从基线创建或指导创建分支,并确保分支同时正式关联需求和开发任务。完成后核查需求=开发中、任务=处理中;不要用首次提交时间覆盖真实开始时间。
|
||||
```
|
||||
|
||||
### DEV02 提交代码并创建MR
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需求【编号】,开发任务【任务编号】,仓库【仓库】,分支【分支】,目标分支【dev/develop】。改动摘要:【摘要】;本地验证:【命令和结果】。
|
||||
请先拉取并检查冲突,再按仓库规范提交、推送并创建MR;MR必须正式关联需求和开发任务。不要直接推保护分支,不要因为提交成功就把开发标为完成。
|
||||
```
|
||||
|
||||
### DEV03 多仓库部分完成
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】目前仅仓库【仓库A】的MR【编号】已合并,仓库【仓库B】仍在开发。使用 yunxiao-requirement-lifecycle,权限=audit。请确认需求和交付主任务仍保持开发中,并检查是否存在“部分MR导致提前完成”的误触发规则。
|
||||
```
|
||||
|
||||
## 10. 代码评审人员
|
||||
|
||||
### REV01 评审要求修改
|
||||
|
||||
```text
|
||||
项目【项目名】MR【编号】评审不通过,需要修改:【问题列表】。使用 yunxiao-requirement-lifecycle,权限=document。请把评审结论关联到需求【编号】和开发任务【编号】,确认MR保持未合并、开发任务保持处理中、需求保持开发中。
|
||||
```
|
||||
|
||||
### REV02 MR合并后核查
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
项目【项目名】MR【编号】已合并到【dev/develop】,关联需求【编号】、开发任务【编号】。请等待异步窗口后核查任务状态;再汇总同需求所有仓库和MR。只有全部相关任务和MR完成才允许需求进入开发完成,并确认测试任务只创建一条。
|
||||
```
|
||||
|
||||
## 11. 测试负责人和测试人员
|
||||
|
||||
### QA01 建立测试任务和用例范围
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需求【编号】开发已完成,目标迭代【迭代】,测试负责人【姓名】。请查重后创建或复用唯一【测试】任务,并将测试计划按需求编号建立独立用例包。范围:【功能/接口/权限/兼容/回归】;不适用:【列表】。正式关联需求、主任务和测试计划后进入待测试。
|
||||
```
|
||||
|
||||
### QA02 test部署成功,开始测试
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】的test部署成功,流水线【名称】,执行ID【ID】,环境【test】,部署时间【时间】。使用 yunxiao-requirement-lifecycle,权限=apply。请校验执行ID、范围和重复回调,再允许测试任务和需求进入测试中。明确记录:本事件不能推进发布完成。
|
||||
```
|
||||
|
||||
### QA03 用例失败并提交缺陷
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需求【编号】,测试计划【计划】,用例【用例ID】执行失败。环境【环境】;前置条件【条件】;步骤【步骤】;实际结果【实际】;期望结果【期望】;证据【截图/日志】;严重程度【级别】。
|
||||
请查重后创建或关联缺陷,并把缺陷与原用例、需求正式关联。保持用例未通过、测试任务和需求测试中;不要把创建缺陷当作测试完成。
|
||||
```
|
||||
|
||||
### QA04 缺陷复测通过
|
||||
|
||||
```text
|
||||
项目【项目名】缺陷【编号】已在环境【环境】复测,原用例【用例ID】重新执行通过,证据【证据】,复测人【姓名】,时间【时间】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请将原用例更新为通过,并把缺陷进入项目真实关闭态;随后重新核查本需求是否仍有未执行、阻塞、失败用例或未关闭缺陷。
|
||||
```
|
||||
|
||||
### QA05 缺陷复测失败
|
||||
|
||||
```text
|
||||
项目【项目名】缺陷【编号】复测失败,原用例【用例ID】仍未通过。实际结果【结果】;证据【证据】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请重开缺陷或转回项目真实处理中状态,保持原用例未通过、需求测试中,并通知原修复负责人。不要保留“已修复/已关闭”。
|
||||
```
|
||||
|
||||
### QA06 测试验收完成
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】需求【编号】申请完成测试。测试计划【计划】;用例包【范围】;执行统计【通过/失败/阻塞/未执行】;缺陷清单【编号+状态】;测试负责人【姓名】。
|
||||
请按TC01-TC03和DF01-DF03核查。只有全部范围用例已执行并通过、失败用例已复测、缺陷由测试关闭时,才完成测试任务并推进需求测试完成;否则列出阻塞项。
|
||||
```
|
||||
|
||||
## 12. 缺陷修复人员
|
||||
|
||||
### BUG01 接收缺陷并开始修复
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
我接收项目【项目名】缺陷【编号】,关联需求【编号】、原用例【ID】,开始修复时间【时间】,仓库【仓库】,拟用分支【fix/缺陷编号】。
|
||||
请核查缺陷、需求和用例的正式关联,再把缺陷转入真实处理中状态;分支和MR同时关联缺陷、需求及对应开发任务。
|
||||
```
|
||||
|
||||
### BUG02 修复完成交给测试
|
||||
|
||||
```text
|
||||
项目【项目名】缺陷【编号】已修复。分支【分支】;MR【编号】;合并目标【dev/develop】;验证结果【结果】;部署环境【test/尚未部署】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请核查代码和部署证据后把缺陷改为已修复并交给测试复测。不要关闭缺陷,不要把原失败用例改为通过。
|
||||
```
|
||||
|
||||
## 13. 运维与发布负责人
|
||||
|
||||
### OPS01 test部署回报
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
项目【项目名】test流水线【名称】执行【成功/失败】,执行ID【ID】,提交/MR范围【范围】,时间【时间】。
|
||||
请核查本次执行关联的需求和幂等记录。成功只允许测试开始;失败保持原状态并记录原因。不要触发生产发布完成。
|
||||
```
|
||||
|
||||
### OPS02 创建迭代发版任务
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】迭代【迭代名/ID】准备发版,计划窗口【时间】,负责人【姓名】,本批需求【编号列表】,生产流水线【名称】。
|
||||
请查重后创建或复用唯一【发版】任务,正式关联迭代和本批需求,并生成发布范围与回滚项。需求未测试完成或范围不一致时停止,不进入发布中。
|
||||
```
|
||||
|
||||
### OPS03 开始生产发布
|
||||
|
||||
```text
|
||||
项目【项目名】发版任务【编号】批准开始生产发布。审批人【姓名】;窗口【时间】;生产流水线【名称】;本批需求【列表】;回滚方案【链接】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请核查全部需求测试完成、范围一致和审批证据,再进入发布中。只操作明确的生产发布,不修改流水线定义。
|
||||
```
|
||||
|
||||
### OPS04 生产发布成功
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】生产流水线【名称】执行成功,执行ID【ID】,环境【prod】,完成时间【时间】,发布范围【需求/MR/版本】,证据【链接】。
|
||||
请验证签名、时间戳、执行ID幂等和范围,推进发版任务及本批需求到发布完成并停留,通知产品验收。禁止自动进入已关闭。
|
||||
```
|
||||
|
||||
### OPS05 生产发布失败或重试
|
||||
|
||||
失败:
|
||||
|
||||
```text
|
||||
项目【项目名】生产流水线【名称】执行失败,执行ID【ID】,失败阶段【阶段】,原因【原因】,影响需求【列表】,回滚结果【结果】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请记录失败证据并推进到发布失败;不得伪造成功或关闭需求。
|
||||
```
|
||||
|
||||
重试:
|
||||
|
||||
```text
|
||||
项目【项目名】发版任务【编号】批准从发布失败重试,批准人【姓名】,新执行ID【ID】,已修正原因【说明】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请核查发布失败→发布中流转边、幂等键和范围后开始重试,保留原失败记录。
|
||||
```
|
||||
|
||||
## 14. 业务验收人员
|
||||
|
||||
### ACC01 验收通过
|
||||
|
||||
```text
|
||||
我是业务验收人【姓名】。项目【项目名】需求【编号】已在生产环境按验收项【列表】验证通过,时间【时间】,证据【链接/附件】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。请核查身份、发布完成状态和证据范围后关闭需求,并保留验收记录。
|
||||
```
|
||||
|
||||
### ACC02 验收不通过
|
||||
|
||||
```text
|
||||
我是业务验收人【姓名】。项目【项目名】需求【编号】生产验收不通过,失败项【列表】,实际结果【结果】,证据【证据】。
|
||||
使用 yunxiao-requirement-lifecycle,权限=plan。请建立可追踪问题清单,分析应回到测试、开发还是重新发布,并指定下一责任人;不要关闭需求。
|
||||
```
|
||||
|
||||
## 15. 集成桥接负责人
|
||||
|
||||
### INT01 回调未生效或重复
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=audit。
|
||||
桥接控制【A03/A06/A08/A09/其他】,项目【项目名】,需求【编号】,事件ID【ID】,时间戳【时间】,回调结果【未生效/重复/失败】,日志摘要【摘要】。
|
||||
请核查签名、时间窗、防重放、幂等键、当前状态、权限、重试和审计日志。不要绕过校验直接补写状态;给出安全重试或人工回退方案。
|
||||
```
|
||||
|
||||
### INT02 幂等补偿
|
||||
|
||||
```text
|
||||
使用 yunxiao-requirement-lifecycle,权限=apply。
|
||||
项目【项目名】控制【控制ID】需要补偿,原事件ID【ID】,期望对象【任务/状态】,当前对象【实际】。已确认原操作未成功且不存在重复对象。
|
||||
请再次查询幂等键和正式关系,只补偿缺失动作;完成后输出新旧事件关联、对象ID和重复检查证据。
|
||||
```
|
||||
|
||||
## 16. 异常和催办话术
|
||||
|
||||
### 状态长时间不动
|
||||
|
||||
```text
|
||||
项目【项目名】需求/任务【编号】在【状态】停留【时长】,最后真实事件【事件】,负责人【姓名】,关联资产【列表】。使用 yunxiao-requirement-lifecycle,权限=audit。请判断是业务未完成、自动化未触发还是异步失败,并给出下一责任人和最小恢复动作;不要直接跳状态。
|
||||
```
|
||||
|
||||
### 状态被提前推进
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】从【源状态】意外进入【目标状态】,时间【时间】。使用 yunxiao-requirement-lifecycle,权限=audit。请检查后继规则、全部关联项口径、部分MR、未复测缺陷、test/prod混用和L01/L02,定位真实触发规则并给出回滚建议。未经确认不要回退状态。
|
||||
```
|
||||
|
||||
### 任务重复创建
|
||||
|
||||
```text
|
||||
项目【项目名】需求【编号】出现重复【交付/开发/测试】任务:【任务列表】。使用 yunxiao-requirement-lifecycle,权限=audit。请检查幂等键、正式关系和重复回调,确定保留项及合并/取消方案。未经授权不要删除任务。
|
||||
```
|
||||
|
||||
## 17. 节点交接清单
|
||||
|
||||
每次角色交接都让Codex确认以下内容:
|
||||
|
||||
1. 项目、需求、任务和流程模型准确。
|
||||
2. 源状态与目标状态明确。
|
||||
3. 事件真实发生,且正式关联可见。
|
||||
4. 当前责任人和下一责任人明确。
|
||||
5. 交付物、代码、用例、缺陷或发布证据可访问。
|
||||
6. 正向条件全部满足,负向阻塞项为零。
|
||||
7. 自动化结果与人工准备动作分开记录。
|
||||
8. 异步等待后重新核查状态和执行日志。
|
||||
9. 没有误触发后继规则或重复创建。
|
||||
10. 回滚入口、人工回退和未完成事项已记录。
|
||||
|
||||
Codex的节点回报统一使用:
|
||||
|
||||
```text
|
||||
项目:
|
||||
流程模型:
|
||||
工作项:
|
||||
源状态 → 实际状态:
|
||||
本次真实事件:
|
||||
执行动作:
|
||||
触发方式:原生规则/桥接/人工门禁/未触发
|
||||
验证结果:通过/异步通过/未触发/误触发/阻塞
|
||||
证据:
|
||||
未完成项:
|
||||
下一责任角色:
|
||||
允许的下一动作:
|
||||
回滚入口:
|
||||
```
|
||||
@@ -0,0 +1,417 @@
|
||||
# 云效日常口语化操作口令
|
||||
|
||||
## 1. 先说一次项目
|
||||
|
||||
每次新会话先输入:
|
||||
|
||||
```text
|
||||
项目:统一运营管理平台PC端
|
||||
```
|
||||
|
||||
之后直接说口令即可。项目没有明确、同一编号可能属于多个项目或用户切换项目时,Codex必须先问清项目,不能猜。
|
||||
|
||||
口令可以自然表达,不要求标点完全一致。例如:
|
||||
|
||||
- `记录需求:增加车辆批量导出`
|
||||
- `帮我记录一个需求,增加车辆批量导出`
|
||||
|
||||
两句话含义相同。
|
||||
|
||||
## 2. 产品和需求
|
||||
|
||||
### 记录需求
|
||||
|
||||
产品输入:
|
||||
|
||||
```text
|
||||
记录需求:增加车辆批量导出
|
||||
```
|
||||
|
||||
Codex执行:查重后在当前项目创建产品类需求,初始状态为`待处理`,返回需求编号。只有标题也可以先记录;云效必填字段缺失时只追问缺少的字段。
|
||||
|
||||
**带模块/原型的记录(OneOS):** 若口令含模块名或原型(如「记录需求:保险采购」),创建前**先调用 `$oneos-autoprd`**,把「需求说明」写入描述;若用户同时定稿,一并写入「更新内容」。
|
||||
|
||||
**统一运营管理平台 · 快路径(强制):** 项目为「统一运营管理平台 / PC 端」时,先读 [oneos-pc-fast-path.md](oneos-pc-fast-path.md) 与 [oneos-pc-runtime-ids.json](../assets/oneos-pc-runtime-ids.json)。用已验证 `POST /workitem/workitem` 建单(创建时带 `document`),不要探测 `/workitem/create`。推进至「开发中」时状态连跳:`待处理 → 设计完成 → 待开发 → 开发中`。
|
||||
|
||||
**优先级 / 推进至 Plan 单选门禁(强制):** 用户要求 Plan 模式单选,或口令含「优先级」「推进至」需点选时:
|
||||
|
||||
1. 先用 `AskQuestion`(可用时)或 Plan 确认拿到 A(紧急/高/中/低)与 B(分析中/设计中/设计完成/开发中)。
|
||||
2. **禁止**未选时默认「中 + 分析中」并继续建单。
|
||||
3. 「批准 / Implement 计划」本身不等于已选 A+B;计划里仍是空选项时必须先问清。
|
||||
4. 标签须用户从云效标签 catalog(`oneos-pc-tag-catalog.md`)点选;禁止按业务条线说明/`lines.ts` 自动推断。点选后 API 打标,失败则停并请用户回复标签名,禁止假装成功。
|
||||
|
||||
**完整可复制提示词(推荐):** 统一运营管理平台建单优先用 [oneos-pc-record-requirement-prompt.md](oneos-pc-record-requirement-prompt.md)——含 `$yunxiao-requirement-lifecycle` + `$oneos-autoprd`、A/B/C 三门禁与 30 项标签枚举。复制该文件「用户口令」整段发给 Agent;确认后再执行快路径。
|
||||
|
||||
简短版(填需求名与模块后复制):
|
||||
|
||||
```text
|
||||
使用 $yunxiao-requirement-lifecycle + $oneos-autoprd
|
||||
记录需求到云效 · 统一运营管理平台
|
||||
|
||||
【需求名】(填写)
|
||||
【描述来源】$oneos-autoprd,原型/模块:(填写)
|
||||
|
||||
请先 Plan 让我点选 A 优先级、B 推进至、C 标签(30 项 catalog,见 oneos-pc-record-requirement-prompt.md;禁止 lines.ts 推断)。
|
||||
我确认后再执行快路径建单。
|
||||
```
|
||||
|
||||
确认回复示例:`优先级:高;推进至:设计完成;标签:运维管理条线`
|
||||
|
||||
### 确认需求
|
||||
|
||||
产品输入:
|
||||
|
||||
```text
|
||||
确认需求:ONEOSP-123
|
||||
```
|
||||
|
||||
Codex执行:检查范围、负责人、优先级和验收标准,齐全后把需求改为`已确认`。
|
||||
|
||||
`确认需求`默认不创建分析任务。确认和真实开始分析是两个事件,防止开始时间失真。由分析人员下一步输入`开始分析`。
|
||||
|
||||
### 需求定稿(原型 + AutoPRD)
|
||||
|
||||
产品输入:
|
||||
|
||||
```text
|
||||
保险采购需求定稿
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```text
|
||||
需求定稿:ONEOSP-123;模块=保险采购
|
||||
```
|
||||
|
||||
Codex执行:
|
||||
|
||||
1. 调用 `$oneos-autoprd` 定稿流程:更新原型 `.spec/requirements-prd.md` 第 10 章「功能变更记录」与 `autoprd-baseline.json`(只记功能/逻辑,不记样式/UI/表结构)。
|
||||
2. 若已有云效需求编号:刷新描述中的「需求说明」与「更新内容」;旧更新内容 →「更新内容·历史」。
|
||||
3. 若口令要求进待开发/发对象存储:继续走下方「快速进入开发」或 OneOS 快轨口令,不得跳过 AutoPRD。
|
||||
|
||||
### 快速进入开发
|
||||
|
||||
产品输入:
|
||||
|
||||
```text
|
||||
快速开发:ONEOSP-123;原因=小改动;负责人=张三;范围=只改后端
|
||||
```
|
||||
|
||||
Codex执行:检查项目是否允许快轨、验收标准是否明确,再跳过不需要的分析/设计阶段并建立必要开发任务。条件不齐时不跳状态,只提示缺少什么。
|
||||
|
||||
**OneOS 模块已定型进待开发(推荐):**
|
||||
|
||||
```text
|
||||
「保险采购」需求已经确定:先 AutoPRD 写需求说明与更新内容;发布对象存储并写入描述;
|
||||
推进到待开发;自动建与需求同名的【交付】任务并正式关联;负责人何斐。
|
||||
```
|
||||
|
||||
执行前必须先跑 `$oneos-autoprd`(含定稿变更记录若用户刚定稿)。进入待开发时**必须**执行 [auto-stage-task.md](auto-stage-task.md)(同名任务、标签交付、负责人何斐、时间沿用需求);已有唯一交付任务则复用并改负责人为何斐,禁止建第二条。
|
||||
|
||||
### 需求变更
|
||||
|
||||
产品输入:
|
||||
|
||||
```text
|
||||
变更需求:ONEOSP-123;增加导出时间筛选
|
||||
```
|
||||
|
||||
Codex执行:先分析受影响的任务、MR、用例、缺陷和发布范围,给出应回到哪个阶段;产品确认后再修改,不直接覆盖历史说明。涉及原型功能变更时,同步 `$oneos-autoprd` 更新 PRD;用户再说「定稿」时写入第 10 章。
|
||||
|
||||
## 3. 分析和设计
|
||||
|
||||
### 开始分析
|
||||
|
||||
分析人员输入:
|
||||
|
||||
```text
|
||||
开始分析:ONEOSP-123;负责人=李四
|
||||
```
|
||||
|
||||
Codex执行:
|
||||
|
||||
1. 将需求推进到`分析中`(或确认已在分析中)。
|
||||
2. **按 [auto-stage-task.md](auto-stage-task.md)**:查重后创建/复用与需求**同名**任务;标签=`分析`;正式关联需求;负责人默认=需求**创建人**(口令写了负责人则用口令);计划开始/创建时间沿用需求。
|
||||
3. 任务进入`处理中`(或项目约定的开工态)。
|
||||
4. `stage_tasks`:需求标签含`分析`。`oneos_delivery`:不因此再建第二条【交付】主任务。
|
||||
|
||||
### 分析完成
|
||||
|
||||
分析人员输入:
|
||||
|
||||
```text
|
||||
分析完成:ONEOSP-123;说明=https://...
|
||||
```
|
||||
|
||||
Codex执行:检查分析产物和未决项;完成对应任务或主任务阶段,核查需求进入`分析完成`。
|
||||
|
||||
### 开始设计
|
||||
|
||||
设计人员输入:
|
||||
|
||||
```text
|
||||
开始设计:ONEOSP-123;负责人=王五
|
||||
```
|
||||
|
||||
Codex执行:
|
||||
|
||||
1. 将需求推进到`设计中`。
|
||||
2. **按 auto-stage-task**:同名任务;标签=`设计`;正式关联;负责人默认=需求**创建人**(口令可覆盖);时间沿用需求。
|
||||
3. 任务开工态同上。
|
||||
4. `oneos_delivery`:不同时新建交付主任务(交付主任务在待开发时建/复用)。
|
||||
|
||||
### 设计完成
|
||||
|
||||
设计人员输入:
|
||||
|
||||
```text
|
||||
设计完成:ONEOSP-123;原型=https://...
|
||||
```
|
||||
|
||||
Codex执行:检查原型或设计说明,完成设计任务/主任务阶段,核查需求进入`设计完成`。
|
||||
|
||||
如果项目已经部署并验证自动建开发任务的桥接,而且负责人、仓库和范围明确,Codex继续查重并创建开发任务;否则停在`设计完成`,提示技术负责人输入`安排开发`。不得创建无负责人或重复任务。
|
||||
|
||||
**进入待开发时(含快轨):** 必须再跑 auto-stage-task:同名任务、标签`交付`(或`开发`)、负责人**何斐**、正式关联、时间沿用需求。
|
||||
|
||||
## 4. 开发
|
||||
|
||||
### 安排开发
|
||||
|
||||
技术负责人输入:
|
||||
|
||||
```text
|
||||
安排开发:ONEOSP-123;前端=张三/ln-one-os-web;后端=李四/ln-cloud
|
||||
```
|
||||
|
||||
只改一端时可以说:
|
||||
|
||||
```text
|
||||
安排开发:ONEOSP-123;只改后端=李四/ln-cloud
|
||||
```
|
||||
|
||||
Codex执行:按实际范围创建一条或多条`【开发】`任务,正式关联需求;`oneos_delivery`还要关联交付主任务。任务初始为`待处理`,需求进入`待开发`。明确“不涉及”的仓库不会成为完成门禁。
|
||||
|
||||
### 开始开发
|
||||
|
||||
开发人员输入:
|
||||
|
||||
```text
|
||||
开始开发:ONEOSP-123;任务=TASK-456;仓库=ln-cloud
|
||||
```
|
||||
|
||||
Codex执行:记录真实开始时间,确认真实基线`dev`或`develop`,创建`feature/ONEOSP-123`并同时正式关联需求和开发任务。任务进入`处理中`,需求进入`开发中`。
|
||||
|
||||
首次提交只作兜底,不覆盖这次真实开始时间。
|
||||
|
||||
### 提交代码
|
||||
|
||||
开发人员输入:
|
||||
|
||||
```text
|
||||
提交代码:ONEOSP-123;任务=TASK-456;仓库=ln-cloud
|
||||
```
|
||||
|
||||
Codex执行:检查改动和测试,先拉取并处理冲突,再提交、推送并创建合并到真实`dev/develop`的MR;MR同时关联需求和开发任务。提交成功不等于开发完成。
|
||||
|
||||
### 开发完成
|
||||
|
||||
开发人员或技术负责人输入:
|
||||
|
||||
```text
|
||||
开发完成:ONEOSP-123
|
||||
```
|
||||
|
||||
Codex执行:汇总该需求所有非取消开发任务、前后端仓库和正式关联MR。只有全部任务完成、全部相关MR已合并,才确认需求进入`开发完成`;少一个仓库也不提前完成。
|
||||
|
||||
条件满足后,如果项目已配置并验证唯一测试任务桥接,则查重创建测试任务;否则提示测试负责人输入`提交测试`。
|
||||
|
||||
## 5. 测试和缺陷
|
||||
|
||||
### 提交测试
|
||||
|
||||
测试负责人或开发负责人输入:
|
||||
|
||||
```text
|
||||
提交测试:ONEOSP-123;test流水线=oneos-web-test
|
||||
```
|
||||
|
||||
Codex执行:确认开发完成,触发或核查明确的test流水线。部署成功后,查重创建并正式关联唯一测试任务和需求用例包,进入`待测试/测试中`的项目真实状态。
|
||||
|
||||
test成功只允许开始测试,不能变成发布完成。
|
||||
|
||||
### 开始测试
|
||||
|
||||
测试人员输入:
|
||||
|
||||
```text
|
||||
开始测试:ONEOSP-123;负责人=赵六
|
||||
```
|
||||
|
||||
Codex执行:核查test部署和测试任务,把测试任务改为`测试中/处理中`,需求进入`测试中`。
|
||||
|
||||
### 发现Bug
|
||||
|
||||
测试人员输入:
|
||||
|
||||
```text
|
||||
发现Bug:ONEOSP-123;用例=CASE-12;现象=车辆来源显示0;期望=显示中文来源
|
||||
```
|
||||
|
||||
Codex执行:查重后创建缺陷,正式关联需求、原失败用例和必要开发任务;用例保持未通过,需求保持`测试中`。
|
||||
|
||||
### Bug修复完成
|
||||
|
||||
开发人员输入:
|
||||
|
||||
```text
|
||||
Bug修复完成:BUG-123;MR=456;已部署test
|
||||
```
|
||||
|
||||
Codex执行:核查代码、MR和test部署后,把缺陷改为`已修复`并交给测试复测。不会关闭缺陷,也不会把原用例直接改成通过。
|
||||
|
||||
### 复测通过
|
||||
|
||||
测试人员输入:
|
||||
|
||||
```text
|
||||
复测通过:BUG-123;用例=CASE-12
|
||||
```
|
||||
|
||||
Codex执行:重新记录原用例为通过,把缺陷改为项目真实关闭态,再检查该需求是否还有失败、阻塞、未执行用例或未关闭缺陷。
|
||||
|
||||
### 复测失败
|
||||
|
||||
测试人员输入:
|
||||
|
||||
```text
|
||||
复测失败:BUG-123;用例=CASE-12;现象=仍显示0
|
||||
```
|
||||
|
||||
Codex执行:原用例保持未通过,缺陷重开或回到处理中,并通知修复负责人;需求保持`测试中`。
|
||||
|
||||
### 测试完成
|
||||
|
||||
测试负责人输入:
|
||||
|
||||
```text
|
||||
测试完成:ONEOSP-123
|
||||
```
|
||||
|
||||
Codex执行:检查本需求用例包,不按整个迭代一刀切。只有没有未执行、阻塞、失败用例,且所有缺陷都经过测试复测关闭,才完成测试任务并推进需求到`测试完成`。
|
||||
|
||||
## 6. 发布和验收
|
||||
|
||||
### 准备发布
|
||||
|
||||
运维或项目负责人输入:
|
||||
|
||||
```text
|
||||
准备发布:迭代V1.3;需求=ONEOSP-123,ONEOSP-124;窗口=今晚22:00
|
||||
```
|
||||
|
||||
Codex执行:检查本批需求均测试完成,查重创建或复用唯一`【发版】`任务,正式关联迭代和需求,生成发布范围及回滚清单。
|
||||
|
||||
### 开始发布
|
||||
|
||||
运维输入:
|
||||
|
||||
```text
|
||||
开始发布:发版任务=TASK-900;prod流水线=oneos-prod;审批人=王经理
|
||||
```
|
||||
|
||||
Codex执行:核查审批、本批范围、生产流水线和回滚方案后进入`发布中`。这句话授权执行明确的本次生产发布,但不授权修改生产流水线定义。
|
||||
|
||||
### 发布成功
|
||||
|
||||
运维输入:
|
||||
|
||||
```text
|
||||
发布成功:执行ID=PIPE-789;发版任务=TASK-900
|
||||
```
|
||||
|
||||
Codex执行:到生产流水线核查环境、执行ID、范围和成功证据,确认后推进本批需求到`发布完成`并停留,等待验收。只说“成功”但没有生产证据时不改状态。
|
||||
|
||||
### 发布失败
|
||||
|
||||
运维输入:
|
||||
|
||||
```text
|
||||
发布失败:执行ID=PIPE-789;原因=镜像拉取失败
|
||||
```
|
||||
|
||||
Codex执行:核查失败证据,记录原因并进入项目真实`发布失败`状态;项目尚无该状态时记录阻塞并给出人工处置,不创建近义重复状态。
|
||||
|
||||
### 验收通过
|
||||
|
||||
产品或业务输入:
|
||||
|
||||
```text
|
||||
验收通过:ONEOSP-123;验收人=王经理;证据=https://...
|
||||
```
|
||||
|
||||
Codex执行:核查生产发布完成和验收证据后,把需求改为`已关闭`。发布成功不能代替本句验收。
|
||||
|
||||
### 验收不通过
|
||||
|
||||
产品或业务输入:
|
||||
|
||||
```text
|
||||
验收不通过:ONEOSP-123;问题=导出字段缺失
|
||||
```
|
||||
|
||||
Codex执行:创建或关联可追踪问题,判断应回到开发、测试还是重新发布,先给出处理建议;不关闭需求,不覆盖原发布记录。
|
||||
|
||||
## 7. 随时可用的查询口令
|
||||
|
||||
```text
|
||||
查状态:ONEOSP-123
|
||||
下一步:ONEOSP-123
|
||||
为什么没流转:ONEOSP-123
|
||||
查重复任务:ONEOSP-123
|
||||
查关联代码:ONEOSP-123
|
||||
查测试闭环:ONEOSP-123
|
||||
给我回滚方案:刚才那条规则
|
||||
```
|
||||
|
||||
这些口令默认只查询或出方案,不修改云效。
|
||||
|
||||
## 8. Codex的简短回报格式
|
||||
|
||||
处理成功:
|
||||
|
||||
```text
|
||||
已处理:ONEOSP-123
|
||||
状态:已确认 → 分析中
|
||||
任务:已创建并关联分析任务 TASK-456
|
||||
下一步:分析人员输入“分析完成:ONEOSP-123;说明=链接”
|
||||
```
|
||||
|
||||
条件不足:
|
||||
|
||||
```text
|
||||
暂未处理:ONEOSP-123
|
||||
缺少:开发负责人
|
||||
请补充:安排开发:ONEOSP-123;只改后端=负责人/仓库
|
||||
```
|
||||
|
||||
自动化未触发:
|
||||
|
||||
```text
|
||||
未触发:ONEOSP-123 仍为待开发
|
||||
原因:分支只写了需求编号,没有在云效正式关联开发任务
|
||||
下一步:我可以补正式关联,然后再核查一次
|
||||
```
|
||||
|
||||
## 9. Codex执行口令的规则
|
||||
|
||||
1. `记录、确认、开始、完成、提交、修复、复测、准备发布、发布、验收`属于当前项目和明确工作项的执行授权。
|
||||
2. `查、看看、为什么、下一步、给方案`只允许读取或规划。
|
||||
3. 写操作前必须确认当前项目;项目不清楚只问项目名。
|
||||
4. 其他必要字段缺失时只追问最少信息,不发送长表单。
|
||||
5. 先查重、再创建;任务幂等键使用项目ID、需求ID和阶段类型。
|
||||
6. 项目使用`stage_tasks`还是`oneos_delivery`由Codex识别,同事不需要记规则编号。
|
||||
7. 当前平台或桥接不支持的动作要明确说“需要人工一步”,不能伪装成已自动化。
|
||||
8. 自动规则执行后等待5–30秒,再回看状态、正式关联和执行日志。
|
||||
9. 不用手工改最终状态冒充自动化通过。
|
||||
10. 不修改生产流水线定义,不删除规则、分支、MR、任务或测试资产,除非用户单独明确授权。
|
||||
@@ -0,0 +1,87 @@
|
||||
# 阶段任务生命周期与自动创建
|
||||
|
||||
## 目录
|
||||
|
||||
1. 目标链路
|
||||
2. TK01–TK08规则
|
||||
3. 关系与代码资产
|
||||
4. 原生规则和桥接边界
|
||||
5. 验证与回滚
|
||||
|
||||
## 1. 目标链路
|
||||
|
||||
```text
|
||||
需求设计中 + 设计任务处理中
|
||||
→ 设计任务已完成
|
||||
→ 需求设计完成
|
||||
→ 创建并关联开发任务
|
||||
→ 需求待开发 + 开发任务待处理
|
||||
→ 正式关联需求分支
|
||||
→ 需求开发中 + 开发任务处理中
|
||||
→ 全部相关MR合并到真实dev/develop
|
||||
→ 需求开发完成 + 开发任务已完成
|
||||
→ test流水线部署成功
|
||||
→ 创建并关联测试任务
|
||||
→ 需求测试中 + 测试任务处理中
|
||||
→ 用例全部通过且缺陷复测关闭
|
||||
→ 需求测试完成 + 测试任务已完成
|
||||
```
|
||||
|
||||
分支只能合并到集成分支,不能“合并到测试环境”。test环境由流水线部署;部署成功用于允许测试开始,不用于证明生产发布。
|
||||
|
||||
## 2. TK01–TK08规则
|
||||
|
||||
| ID | 触发 | 必要条件 | 动作 | 推荐实现 |
|
||||
|---|---|---|---|---|
|
||||
| TK01 | 需求进入设计中 | 已正式关联设计任务;任务标签=`设计` | 设计任务保持待处理,负责人真实开工后人工改为处理中 | 默认+人工 |
|
||||
| TK02 | 设计任务变为已完成 | 需求当前状态=设计中;本阶段设计任务非零且全部完成 | 需求改为设计完成 | 原生规则 |
|
||||
| TK03 | 需求变为设计完成 | 不存在同需求、同阶段、未取消的开发任务 | 创建并正式关联开发任务,标签=`开发`,状态=待处理 | API/Webhook桥接 |
|
||||
| TK04 | 开发任务创建并关联成功 | 需求当前状态=设计完成;开发任务关系可见 | 需求改为待开发 | 原生规则 |
|
||||
| TK05 | 正式关联需求分支/代码资产 | 需求=待开发;开发任务=待处理;资产同时关联需求和开发任务 | 需求改为开发中;开发任务改为处理中 | 原生规则 |
|
||||
| TK06 | 关联合并请求状态变化 | 需求=开发中;开发任务=处理中;全部相关前后端MR已合并到真实集成分支 | 需求改为开发完成;开发任务改为已完成 | 原生规则 |
|
||||
| TK07 | test流水线部署成功 | 需求=开发完成;执行ID未处理;不存在同需求、同阶段、未取消的测试任务 | 创建并关联测试任务,关联测试计划/用例,任务改为处理中,需求改为测试中 | API/Webhook桥接 |
|
||||
| TK08 | 测试验收完成 | 用例全部通过;失败用例已复测;范围内缺陷均由测试关闭 | 测试任务改为已完成;需求改为测试完成 | 桥接/人工门禁 |
|
||||
|
||||
## 3. 关系与代码资产
|
||||
|
||||
- 设计、开发、测试任务必须使用云效正式父子项或关联项关系,不依赖标题关键字。
|
||||
- 开发分支和MR必须同时关联产品需求与对应开发任务;只关联需求不能可靠更新开发任务。
|
||||
- 一个需求涉及多个仓库时,全部相关前端、后端MR都纳入TK06门禁。
|
||||
- 首次提交可能晚于真实开工。任务`处理中`的权威时间是负责人执行“开始处理”;分支关联仅作自动兜底。
|
||||
|
||||
## 4. 原生规则和桥接边界
|
||||
|
||||
优先使用云效原生规则处理状态变化、父子项、关联项和Codeup对象。若当前规则编辑器没有“创建工作项”动作,TK03和TK07必须标记为`integration_bridge`或`not_supported`,不得声称已经自动创建。
|
||||
|
||||
桥接创建任务时使用幂等键:
|
||||
|
||||
```text
|
||||
项目ID + 需求ID + 阶段类型
|
||||
```
|
||||
|
||||
重复回调先查询是否存在未取消的同阶段任务;存在则复用并校验正式关系,不重复创建。流水线回调还要校验签名、时间戳、执行ID、防重放和失败重试。
|
||||
|
||||
## 5. 验证与回滚
|
||||
|
||||
- 正向验证每个TK控制,并检查需求和任务两个对象的状态。
|
||||
- 负向验证零任务、错误标签、错误当前状态、未正式关联分支、只合并部分MR、流水线失败和重复回调。
|
||||
- 新规则不会补触发历史事件。历史任务仅在核对MR/流水线证据后人工修正,并标记为测试准备或数据修复。
|
||||
- 回滚时先禁用新增规则或桥接入口,不删除已有任务和关系;恢复前一状态需要单独业务确认。
|
||||
|
||||
## 6. 进阶段自动建同名任务(与 OneOS / 口令共用)
|
||||
|
||||
完整规则见 [auto-stage-task.md](auto-stage-task.md)。摘要:
|
||||
|
||||
| 需求状态 | 同名任务标签 | 负责人 |
|
||||
|---|---|---|
|
||||
| 分析中 | `分析` | 需求创建人 |
|
||||
| 设计中 | `设计` | 需求创建人 |
|
||||
| 待开发 | `交付` 或 `开发` | 何斐 |
|
||||
|
||||
必须正式关联需求;计划开始/创建时间沿用需求;同阶段幂等查重。
|
||||
|
||||
当创建或修复**分析 / 设计 / 交付(开发)**任务时:
|
||||
|
||||
1. **任务标签(右侧基础字段)**:分析 → `分析`;设计 → `设计`;待开发主任务 → `交付`(或 `开发`)。Title 同名或前缀 alone 不够。
|
||||
2. **正式关联需求**:每条阶段任务必须链接到**产品需求**(父子或关联项)。只关联其他任务不算完成。
|
||||
3. 创建/修复后先核对标签与可见需求关系,再报告成功。
|
||||
@@ -0,0 +1,62 @@
|
||||
# 测试用例与缺陷复测闭环
|
||||
|
||||
## 目录
|
||||
|
||||
1. 测试完成门槛
|
||||
2. TC01–TC03用例控制
|
||||
3. DF01–DF03缺陷控制
|
||||
4. 实现方式
|
||||
5. 验证场景
|
||||
|
||||
## 1. 测试完成门槛
|
||||
|
||||
R10只有同时满足以下条件才能完成:
|
||||
|
||||
1. 范围内用例均已执行并有明确结果。
|
||||
2. 不存在`未执行/待测试`、`阻塞`或`未通过`用例;获批暂缓/不适用项记录批准人与原因。
|
||||
3. 每个未通过用例均正式创建或关联缺陷。
|
||||
4. 修复后重新执行原用例并通过。
|
||||
5. 范围内缺陷均由测试复测并进入项目真实关闭态。
|
||||
6. 测试计划或测试验收任务由测试负责人确认。
|
||||
|
||||
流水线成功只能证明部署/自动检查成功,不能替代用例结果和缺陷复测。
|
||||
|
||||
## 2. TC01–TC03用例控制
|
||||
|
||||
| ID | 控制 | 完成口径 |
|
||||
|---|---|---|
|
||||
| TC01 | 用例执行 | 每条范围内用例有真实执行结果;未执行、待测试、阻塞、未通过均阻塞测试完成 |
|
||||
| TC02 | 失败用例关联缺陷 | 每条未通过用例正式创建或关联缺陷;仅写编号或链接不算关联 |
|
||||
| TC03 | 用例复测更新 | 修复后重新执行原用例;通过才改为已通过,失败保持未通过 |
|
||||
|
||||
## 3. DF01–DF03缺陷控制
|
||||
|
||||
| ID | 控制 | 完成口径 |
|
||||
|---|---|---|
|
||||
| DF01 | 已修复交接 | `已修复`只表示开发交给测试复测,不是终态,不解除测试门禁 |
|
||||
| DF02 | 复测通过 | 缺陷进入项目真实关闭态,原失败用例同时更新为已通过 |
|
||||
| DF03 | 复测失败 | 缺陷进入项目真实重开/处理中状态,原用例保持未通过 |
|
||||
|
||||
不得关闭缺陷却不更新原用例,也不得在缺陷仅为`已修复`时把用例标为通过。
|
||||
|
||||
## 4. 实现方式
|
||||
|
||||
先检查当前规则编辑器是否真的暴露Testhub执行用例和缺陷聚合条件:
|
||||
|
||||
- 支持时:使用原生聚合规则,并保存条件和执行日志证据。
|
||||
- 不支持时:使用测试验收任务/人工门禁。
|
||||
- 需要全自动时:单独评审Testhub OpenAPI/Webhook桥接,要求签名、幂等、失败重试和审计记录。
|
||||
|
||||
不能把`not_supported`写成“已自动化”;必须指定负责人和回退方案。
|
||||
|
||||
## 5. 验证场景
|
||||
|
||||
| 场景 | 操作 | 期望 | 负向检查 |
|
||||
|---|---|---|---|
|
||||
| 用例未执行 | 保留一条未执行用例 | 保持测试中 | 不得完成测试 |
|
||||
| 用例失败 | 记录未通过并正式关联缺陷 | 保持测试中 | 仅建缺陷不得完成 |
|
||||
| 缺陷已修复 | 开发改为已修复 | 等待测试复测 | 不得关闭或通过用例 |
|
||||
| 复测通过 | 重跑原用例并通过 | 用例通过、缺陷关闭 | 两者状态必须一致 |
|
||||
| 复测失败 | 重跑仍失败 | 用例未通过、缺陷重开 | 不得保持已修复/关闭 |
|
||||
| 全部闭环 | 全部用例通过且缺陷关闭 | 允许R10推进 | 检查是否误触发后继规则 |
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate and render a OneOS delivery-model Yunxiao project profile."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
TARGET_LIFECYCLE = [
|
||||
"待处理", "已确认", "分析中", "分析完成", "设计中", "设计完成",
|
||||
"待开发", "开发中", "开发完成", "待测试", "测试中", "测试完成",
|
||||
"发布中", "发布完成", "发布失败", "已关闭",
|
||||
]
|
||||
REQUIRED_TASK_LABELS = {"交付", "开发", "测试", "发版"}
|
||||
REQUIRED_CONTROL_IDS = {
|
||||
*(f"Y{index:02d}" for index in range(1, 17)),
|
||||
"Y20", "Y21", "Y22",
|
||||
*(f"Y{index:02d}" for index in range(30, 36)),
|
||||
"Y40",
|
||||
"A01", "A02", "A02B", *(f"A{index:02d}" for index in range(3, 11)),
|
||||
*(f"TC{index:02d}" for index in range(1, 4)),
|
||||
*(f"DF{index:02d}" for index in range(1, 4)),
|
||||
*(f"CI{index:02d}" for index in range(1, 4)),
|
||||
"L01", "L02",
|
||||
}
|
||||
VALID_MODES = {
|
||||
"manual", "native_rule", "integration_bridge", "disabled_legacy", "not_supported"
|
||||
}
|
||||
VALID_PHASES = {"P0", "P1", "P2", "P3"}
|
||||
VALID_TASK_WORKFLOW_MODES = {"A", "B"}
|
||||
VALID_TASK_IDENTITY_MODES = {"work_item_type", "task_label", "title_prefix_process_control"}
|
||||
VALID_LEGACY_DISPOSITIONS = {"keep_until_ready", "disable", "replace", "already_disabled"}
|
||||
SENSITIVE_FRAGMENTS = (
|
||||
"password", "passwd", "secret", "token", "cookie", "credential",
|
||||
"private_key", "otp", "密码", "密钥",
|
||||
)
|
||||
|
||||
|
||||
def is_text(value: Any) -> bool:
|
||||
return isinstance(value, str) and bool(value.strip())
|
||||
|
||||
|
||||
def require_text(container: dict[str, Any], key: str, label: str, errors: list[str]) -> None:
|
||||
if not is_text(container.get(key)):
|
||||
errors.append(f"{label}.{key} 不能为空")
|
||||
|
||||
|
||||
def walk_sensitive_keys(value: Any, prefix: str = "") -> list[str]:
|
||||
found: list[str] = []
|
||||
if isinstance(value, dict):
|
||||
for key, nested in value.items():
|
||||
current = f"{prefix}.{key}" if prefix else str(key)
|
||||
if any(fragment in str(key).lower() for fragment in SENSITIVE_FRAGMENTS):
|
||||
found.append(current)
|
||||
found.extend(walk_sensitive_keys(nested, current))
|
||||
elif isinstance(value, list):
|
||||
for index, nested in enumerate(value):
|
||||
found.extend(walk_sensitive_keys(nested, f"{prefix}[{index}]"))
|
||||
return found
|
||||
|
||||
|
||||
def validate_controls(controls: Any, label: str, errors: list[str]) -> None:
|
||||
if not isinstance(controls, list):
|
||||
errors.append(f"{label} 必须是数组")
|
||||
return
|
||||
seen: set[str] = set()
|
||||
for index, control in enumerate(controls):
|
||||
item_label = f"{label}[{index}]"
|
||||
if not isinstance(control, dict):
|
||||
errors.append(f"{item_label} 必须是对象")
|
||||
continue
|
||||
control_id = control.get("id")
|
||||
if not is_text(control_id):
|
||||
errors.append(f"{item_label}.id 不能为空")
|
||||
continue
|
||||
if control_id in seen:
|
||||
errors.append(f"{label} 控制项重复:{control_id}")
|
||||
seen.add(control_id)
|
||||
if control_id not in REQUIRED_CONTROL_IDS:
|
||||
errors.append(f"{label} 未知控制项:{control_id}")
|
||||
mode = control.get("mode")
|
||||
if mode not in VALID_MODES:
|
||||
errors.append(f"{item_label}.mode 不合法")
|
||||
if not isinstance(control.get("enabled"), bool):
|
||||
errors.append(f"{item_label}.enabled 必须是布尔值")
|
||||
if mode in {"disabled_legacy", "not_supported"} and control.get("enabled") is not False:
|
||||
errors.append(f"{item_label} 的 {mode} 模式要求 enabled=false")
|
||||
if control_id in {"L01", "L02"}:
|
||||
if mode != "disabled_legacy" or control.get("enabled") is not False:
|
||||
errors.append(f"{item_label} 必须停用,防止发布完成绕过验收")
|
||||
for key in ("owner", "fallback", "evidence"):
|
||||
require_text(control, key, item_label, errors)
|
||||
missing = sorted(REQUIRED_CONTROL_IDS - seen)
|
||||
if missing:
|
||||
errors.append(f"{label} 缺少控制项:" + "、".join(missing))
|
||||
|
||||
|
||||
def validate_project(project: Any, index: int, errors: list[str]) -> None:
|
||||
label = f"projects[{index}]"
|
||||
if not isinstance(project, dict):
|
||||
errors.append(f"{label} 必须是对象")
|
||||
return
|
||||
require_text(project, "name", label, errors)
|
||||
if project.get("migration_phase") not in VALID_PHASES:
|
||||
errors.append(f"{label}.migration_phase 必须是 P0/P1/P2/P3")
|
||||
statuses = project.get("requirement_statuses_observed")
|
||||
if not isinstance(statuses, list) or not statuses or not all(is_text(v) for v in statuses):
|
||||
errors.append(f"{label}.requirement_statuses_observed 必须是非空文本数组")
|
||||
if project.get("task_workflow_mode") not in VALID_TASK_WORKFLOW_MODES:
|
||||
errors.append(f"{label}.task_workflow_mode 必须是 A 或 B")
|
||||
if project.get("task_identity_mode") not in VALID_TASK_IDENTITY_MODES:
|
||||
errors.append(f"{label}.task_identity_mode 不合法")
|
||||
if not isinstance(project.get("native_rule_can_filter_related_task_identity"), bool):
|
||||
errors.append(f"{label}.native_rule_can_filter_related_task_identity 必须是布尔值")
|
||||
for key in (
|
||||
"main_task_prefix", "development_task_prefix", "test_task_prefix", "release_task_prefix"
|
||||
):
|
||||
require_text(project, key, label, errors)
|
||||
|
||||
repositories = project.get("repositories")
|
||||
if not isinstance(repositories, list) or not repositories:
|
||||
errors.append(f"{label}.repositories 至少包含一个仓库")
|
||||
else:
|
||||
for repo_index, repo in enumerate(repositories):
|
||||
repo_label = f"{label}.repositories[{repo_index}]"
|
||||
if not isinstance(repo, dict):
|
||||
errors.append(f"{repo_label} 必须是对象")
|
||||
continue
|
||||
for key in ("name", "role", "integration_branch", "evidence"):
|
||||
require_text(repo, key, repo_label, errors)
|
||||
if repo.get("integration_branch") not in {"dev", "develop"}:
|
||||
errors.append(f"{repo_label}.integration_branch 只能是实际存在的 dev/develop")
|
||||
for key in ("codeup_integrated", "required_for_mr_gate"):
|
||||
if not isinstance(repo.get(key), bool):
|
||||
errors.append(f"{repo_label}.{key} 必须是布尔值")
|
||||
|
||||
test_plan = project.get("test_plan")
|
||||
if not isinstance(test_plan, dict):
|
||||
errors.append(f"{label}.test_plan 必须是对象")
|
||||
else:
|
||||
require_text(test_plan, "name", f"{label}.test_plan", errors)
|
||||
require_text(test_plan, "evidence", f"{label}.test_plan", errors)
|
||||
for key, required in (
|
||||
("iteration_formally_related", True),
|
||||
("cases_partitioned_by_requirement", True),
|
||||
("fixed_defect_is_terminal", False),
|
||||
("tester_retest_required", True),
|
||||
):
|
||||
if test_plan.get(key) is not required:
|
||||
errors.append(f"{label}.test_plan.{key} 必须为 {str(required).lower()}")
|
||||
|
||||
release = project.get("release")
|
||||
if not isinstance(release, dict):
|
||||
errors.append(f"{label}.release 必须是对象")
|
||||
else:
|
||||
require_text(release, "evidence", f"{label}.release", errors)
|
||||
for key in (
|
||||
"release_task_formally_related_to_iteration",
|
||||
"production_evidence_separate_from_test",
|
||||
"acceptance_required_after_release",
|
||||
"production_pipeline_unchanged",
|
||||
):
|
||||
if release.get(key) is not True:
|
||||
errors.append(f"{label}.release.{key} 必须为 true")
|
||||
|
||||
legacy_rules = project.get("legacy_rules")
|
||||
if not isinstance(legacy_rules, list):
|
||||
errors.append(f"{label}.legacy_rules 必须是数组")
|
||||
else:
|
||||
for legacy_index, rule in enumerate(legacy_rules):
|
||||
item_label = f"{label}.legacy_rules[{legacy_index}]"
|
||||
if not isinstance(rule, dict):
|
||||
errors.append(f"{item_label} 必须是对象")
|
||||
continue
|
||||
for key in ("name", "reason", "evidence"):
|
||||
require_text(rule, key, item_label, errors)
|
||||
if rule.get("disposition") not in VALID_LEGACY_DISPOSITIONS:
|
||||
errors.append(f"{item_label}.disposition 不合法")
|
||||
if not isinstance(rule.get("enabled"), bool):
|
||||
errors.append(f"{item_label}.enabled 必须是布尔值")
|
||||
|
||||
validate_controls(project.get("controls"), f"{label}.controls", errors)
|
||||
|
||||
|
||||
def validate(data: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if data.get("schema_version") != 1:
|
||||
errors.append("schema_version 必须为 1")
|
||||
if data.get("flow_model") != "oneos_delivery":
|
||||
errors.append("flow_model 必须为 oneos_delivery")
|
||||
for key in ("organization", "work_item_type"):
|
||||
require_text(data, key, "profile", errors)
|
||||
if data.get("target_lifecycle") != TARGET_LIFECYCLE:
|
||||
errors.append("target_lifecycle 必须与OneOS终态16状态顺序一致")
|
||||
labels = data.get("task_labels")
|
||||
if not isinstance(labels, list) or not REQUIRED_TASK_LABELS.issubset(set(labels)):
|
||||
errors.append("task_labels 必须包含:交付、开发、测试、发版")
|
||||
policies = data.get("policies")
|
||||
if not isinstance(policies, dict):
|
||||
errors.append("policies 必须是对象")
|
||||
else:
|
||||
true_keys = (
|
||||
"require_formal_relations", "require_current_state_condition",
|
||||
"require_nonzero_development_tasks",
|
||||
"require_signed_timestamped_replay_protected_callbacks",
|
||||
)
|
||||
false_keys = ("allow_automatic_final_close_without_acceptance", "test_success_is_production_release")
|
||||
for key in true_keys:
|
||||
if policies.get(key) is not True:
|
||||
errors.append(f"policies.{key} 必须为 true")
|
||||
for key in false_keys:
|
||||
if policies.get(key) is not False:
|
||||
errors.append(f"policies.{key} 必须为 false")
|
||||
parts = policies.get("bridge_idempotency_key_components")
|
||||
if not isinstance(parts, list) or not parts or not all(is_text(v) for v in parts):
|
||||
errors.append("policies.bridge_idempotency_key_components 不能为空")
|
||||
projects = data.get("projects")
|
||||
if not isinstance(projects, list) or not projects:
|
||||
errors.append("projects 至少包含一个项目")
|
||||
else:
|
||||
for index, project in enumerate(projects):
|
||||
validate_project(project, index, errors)
|
||||
sensitive = walk_sensitive_keys(data)
|
||||
if sensitive:
|
||||
errors.append("配置中禁止出现敏感字段:" + "、".join(sensitive))
|
||||
if "请填写" in json.dumps(data, ensure_ascii=False):
|
||||
errors.append("仍存在“请填写”占位符")
|
||||
return errors
|
||||
|
||||
|
||||
def render(data: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"# OneOS云效终态交付模型执行计划", "",
|
||||
f"- 组织:{data['organization']}",
|
||||
f"- 工作项类型:{data['work_item_type']}",
|
||||
f"- 项目数:{len(data['projects'])}",
|
||||
"- 完整性口径:每项目48项Y/A/测试/缺陷/集成/旧规则控制。",
|
||||
"- 安全边界:不修改生产流水线;发布完成后保留产品验收门禁。", "",
|
||||
]
|
||||
for project in data["projects"]:
|
||||
missing_statuses = [s for s in TARGET_LIFECYCLE if s not in project["requirement_statuses_observed"]]
|
||||
lines.extend([
|
||||
f"## 项目:{project['name']}", "",
|
||||
f"- 迁移阶段:{project['migration_phase']}",
|
||||
f"- 任务工作流方案:{project['task_workflow_mode']}",
|
||||
f"- 任务身份方式:{project['task_identity_mode']}",
|
||||
f"- 原生规则可过滤关联任务身份:{'是' if project['native_rule_can_filter_related_task_identity'] else '否'}",
|
||||
f"- 缺少目标状态:{'、'.join(missing_statuses) if missing_statuses else '无'}", "",
|
||||
"### 旧规则处置", "",
|
||||
"| 规则 | 处置 | 启用 | 原因 | 证据 |", "|---|---|---|---|---|",
|
||||
])
|
||||
for rule in project["legacy_rules"]:
|
||||
lines.append(
|
||||
f"| {rule['name']} | {rule['disposition']} | {'是' if rule['enabled'] else '否'} | "
|
||||
f"{rule['reason']} | {rule['evidence']} |"
|
||||
)
|
||||
lines.extend([
|
||||
"", "### 48项控制台账", "",
|
||||
"| ID | 方式 | 启用 | 负责人 | 回退方案 | 证据 |", "|---|---|---|---|---|---|",
|
||||
])
|
||||
for control in sorted(project["controls"], key=lambda item: item["id"]):
|
||||
lines.append(
|
||||
f"| {control['id']} | {control['mode']} | {'是' if control['enabled'] else '否'} | "
|
||||
f"{control['owner']} | {control['fallback']} | {control['evidence']} |"
|
||||
)
|
||||
lines.extend([
|
||||
"", "### 迁移门禁", "",
|
||||
"- [ ] 目标状态与流转边已补齐。",
|
||||
"- [ ] 主任务可可靠识别或已明确交桥接。",
|
||||
"- [ ] 多仓库需求与开发任务均正式关联分支/MR。",
|
||||
"- [ ] Testhub按需求分包且缺陷复测闭环。",
|
||||
"- [ ] test、生产发布和产品验收证据分离。",
|
||||
"- [ ] L01、L02已停用,旧规则已有备份和回滚。", "",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
validate_parser = subparsers.add_parser("validate")
|
||||
validate_parser.add_argument("profile", type=Path)
|
||||
render_parser = subparsers.add_parser("render")
|
||||
render_parser.add_argument("profile", type=Path)
|
||||
render_parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
with args.profile.open("r", encoding="utf-8-sig") as handle:
|
||||
data = json.load(handle)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("配置根节点必须是JSON对象")
|
||||
errors = validate(data)
|
||||
except (OSError, json.JSONDecodeError, ValueError) as error:
|
||||
print(f"配置读取失败:{error}", file=sys.stderr)
|
||||
return 2
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"[错误] {error}", file=sys.stderr)
|
||||
return 1
|
||||
if args.command == "validate":
|
||||
print(f"[通过] OneOS配置有效:{len(data['projects'])}个项目,每项目48项控制。")
|
||||
return 0
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(render(data), encoding="utf-8")
|
||||
print(f"[完成] 已生成执行计划:{args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,536 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a project-scoped Yunxiao lifecycle profile and render an execution plan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
REQUIRED_LABELS = {"分析", "设计", "开发", "测试"}
|
||||
REQUIRED_RULE_IDS = {f"R{index:02d}" for index in range(1, 13)}
|
||||
REQUIRED_TASK_RULE_IDS = {f"TK{index:02d}" for index in range(1, 9)}
|
||||
REQUIRED_CONTROL_IDS = {
|
||||
*REQUIRED_RULE_IDS,
|
||||
*REQUIRED_TASK_RULE_IDS,
|
||||
*(f"TC{index:02d}" for index in range(1, 4)),
|
||||
*(f"DF{index:02d}" for index in range(1, 4)),
|
||||
*(f"CI{index:02d}" for index in range(1, 4)),
|
||||
"L01",
|
||||
"L02",
|
||||
}
|
||||
VALID_MODES = {
|
||||
"manual",
|
||||
"native_rule",
|
||||
"integration_bridge",
|
||||
"disabled_legacy",
|
||||
"not_supported",
|
||||
}
|
||||
VALID_RELEASE_EVIDENCE_MODES = {
|
||||
"native_release_change",
|
||||
"pipeline_callback",
|
||||
"manual_release_confirmation",
|
||||
}
|
||||
SENSITIVE_FRAGMENTS = (
|
||||
"password",
|
||||
"passwd",
|
||||
"secret",
|
||||
"token",
|
||||
"cookie",
|
||||
"credential",
|
||||
"private_key",
|
||||
"otp",
|
||||
"密码",
|
||||
"密钥",
|
||||
)
|
||||
|
||||
|
||||
def load_profile(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8-sig") as handle:
|
||||
data = json.load(handle)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("配置根节点必须是 JSON 对象")
|
||||
return data
|
||||
|
||||
|
||||
def is_text(value: Any) -> bool:
|
||||
return isinstance(value, str) and bool(value.strip())
|
||||
|
||||
|
||||
def require_text(container: dict[str, Any], key: str, label: str, errors: list[str]) -> None:
|
||||
if not is_text(container.get(key)):
|
||||
errors.append(f"{label}.{key} 不能为空")
|
||||
|
||||
|
||||
def walk_sensitive_keys(value: Any, prefix: str = "") -> list[str]:
|
||||
found: list[str] = []
|
||||
if isinstance(value, dict):
|
||||
for key, nested in value.items():
|
||||
current = f"{prefix}.{key}" if prefix else str(key)
|
||||
lower = str(key).lower()
|
||||
if any(fragment in lower for fragment in SENSITIVE_FRAGMENTS):
|
||||
found.append(current)
|
||||
found.extend(walk_sensitive_keys(nested, current))
|
||||
elif isinstance(value, list):
|
||||
for index, nested in enumerate(value):
|
||||
found.extend(walk_sensitive_keys(nested, f"{prefix}[{index}]"))
|
||||
return found
|
||||
|
||||
|
||||
def validate_control_list(
|
||||
controls: Any, label: str, errors: list[str]
|
||||
) -> None:
|
||||
if not isinstance(controls, list):
|
||||
errors.append(f"{label} 必须是数组")
|
||||
return
|
||||
seen: set[str] = set()
|
||||
for index, control in enumerate(controls):
|
||||
item_label = f"{label}[{index}]"
|
||||
if not isinstance(control, dict):
|
||||
errors.append(f"{item_label} 必须是对象")
|
||||
continue
|
||||
control_id = control.get("id")
|
||||
if not is_text(control_id):
|
||||
errors.append(f"{item_label}.id 不能为空")
|
||||
continue
|
||||
if control_id in seen:
|
||||
errors.append(f"{label} 控制项重复:{control_id}")
|
||||
seen.add(control_id)
|
||||
if control_id not in REQUIRED_CONTROL_IDS:
|
||||
errors.append(f"{label} 未知控制项:{control_id}")
|
||||
mode = control.get("mode")
|
||||
if mode not in VALID_MODES:
|
||||
errors.append(f"{item_label}.mode 不合法")
|
||||
if not isinstance(control.get("enabled"), bool):
|
||||
errors.append(f"{item_label}.enabled 必须是布尔值")
|
||||
for key in ("evidence", "owner", "fallback"):
|
||||
require_text(control, key, item_label, errors)
|
||||
if mode in {"disabled_legacy", "not_supported"} and control.get("enabled") is not False:
|
||||
errors.append(f"{item_label} 的 {mode} 模式要求 enabled=false")
|
||||
if control_id in {"L01", "L02"} and mode not in {
|
||||
"disabled_legacy",
|
||||
"native_rule",
|
||||
"manual",
|
||||
}:
|
||||
errors.append(f"{item_label} 必须明确为停用旧规则、获批原生规则或人工处置")
|
||||
missing = sorted(REQUIRED_CONTROL_IDS - seen)
|
||||
if missing:
|
||||
errors.append(f"{label} 缺少控制项:" + "、".join(missing))
|
||||
|
||||
|
||||
def validate_rule_instances(
|
||||
rules: Any, lifecycle: list[str], label: str, errors: list[str]
|
||||
) -> None:
|
||||
if not isinstance(rules, list):
|
||||
errors.append(f"{label} 必须是数组")
|
||||
return
|
||||
seen: set[str] = set()
|
||||
orders: set[int] = set()
|
||||
expected = {
|
||||
f"R{index + 1:02d}": (lifecycle[index], lifecycle[index + 1])
|
||||
for index in range(12)
|
||||
}
|
||||
for index, rule in enumerate(rules):
|
||||
item_label = f"{label}[{index}]"
|
||||
if not isinstance(rule, dict):
|
||||
errors.append(f"{item_label} 必须是对象")
|
||||
continue
|
||||
rule_id = rule.get("id")
|
||||
if not is_text(rule_id):
|
||||
errors.append(f"{item_label}.id 不能为空")
|
||||
continue
|
||||
if rule_id in seen:
|
||||
errors.append(f"{label} 规则实例重复:{rule_id}")
|
||||
seen.add(rule_id)
|
||||
if rule_id not in REQUIRED_RULE_IDS:
|
||||
errors.append(f"{label} 未知规则实例:{rule_id}")
|
||||
mode = rule.get("mode")
|
||||
if mode not in {"manual", "native_rule", "integration_bridge", "not_supported"}:
|
||||
errors.append(f"{item_label}.mode 不合法")
|
||||
for key in ("actual_rule_name", "trigger", "source_status", "target_status", "action", "evidence"):
|
||||
require_text(rule, key, item_label, errors)
|
||||
conditions = rule.get("conditions")
|
||||
if not isinstance(conditions, list) or not conditions or not all(is_text(v) for v in conditions):
|
||||
errors.append(f"{item_label}.conditions 必须是非空文本数组")
|
||||
order = rule.get("order")
|
||||
if not isinstance(order, int) or order < 1:
|
||||
errors.append(f"{item_label}.order 必须是正整数")
|
||||
elif order in orders:
|
||||
errors.append(f"{label} 规则顺序重复:{order}")
|
||||
else:
|
||||
orders.add(order)
|
||||
if not isinstance(rule.get("enabled"), bool):
|
||||
errors.append(f"{item_label}.enabled 必须是布尔值")
|
||||
if rule.get("has_current_state_condition") is not True:
|
||||
errors.append(f"{item_label}.has_current_state_condition 必须为 true")
|
||||
if rule_id in expected:
|
||||
expected_source, expected_target = expected[rule_id]
|
||||
if rule.get("source_status") != expected_source:
|
||||
errors.append(f"{item_label}.source_status 应为 {expected_source}")
|
||||
if rule.get("target_status") != expected_target:
|
||||
errors.append(f"{item_label}.target_status 应为 {expected_target}")
|
||||
missing = sorted(REQUIRED_RULE_IDS - seen)
|
||||
if missing:
|
||||
errors.append(f"{label} 缺少规则实例:" + "、".join(missing))
|
||||
|
||||
|
||||
def validate_task_rule_instances(rules: Any, label: str, errors: list[str]) -> None:
|
||||
if not isinstance(rules, list):
|
||||
errors.append(f"{label} 必须是数组")
|
||||
return
|
||||
seen: set[str] = set()
|
||||
orders: set[int] = set()
|
||||
for index, rule in enumerate(rules):
|
||||
item_label = f"{label}[{index}]"
|
||||
if not isinstance(rule, dict):
|
||||
errors.append(f"{item_label} 必须是对象")
|
||||
continue
|
||||
rule_id = rule.get("id")
|
||||
if not is_text(rule_id):
|
||||
errors.append(f"{item_label}.id 不能为空")
|
||||
continue
|
||||
if rule_id in seen:
|
||||
errors.append(f"{label} 规则实例重复:{rule_id}")
|
||||
seen.add(rule_id)
|
||||
if rule_id not in REQUIRED_TASK_RULE_IDS:
|
||||
errors.append(f"{label} 未知任务规则实例:{rule_id}")
|
||||
if rule.get("mode") not in {"manual", "native_rule", "integration_bridge", "not_supported"}:
|
||||
errors.append(f"{item_label}.mode 不合法")
|
||||
for key in ("actual_rule_name", "trigger", "scope", "source_state", "evidence"):
|
||||
require_text(rule, key, item_label, errors)
|
||||
for key in ("conditions", "actions"):
|
||||
values = rule.get(key)
|
||||
if not isinstance(values, list) or not values or not all(is_text(value) for value in values):
|
||||
errors.append(f"{item_label}.{key} 必须是非空文本数组")
|
||||
order = rule.get("order")
|
||||
if not isinstance(order, int) or order < 1:
|
||||
errors.append(f"{item_label}.order 必须是正整数")
|
||||
elif order in orders:
|
||||
errors.append(f"{label} 规则顺序重复:{order}")
|
||||
else:
|
||||
orders.add(order)
|
||||
if not isinstance(rule.get("enabled"), bool):
|
||||
errors.append(f"{item_label}.enabled 必须是布尔值")
|
||||
missing = sorted(REQUIRED_TASK_RULE_IDS - seen)
|
||||
if missing:
|
||||
errors.append(f"{label} 缺少任务规则实例:" + "、".join(missing))
|
||||
|
||||
|
||||
def validate_project(
|
||||
project: Any, index: int, lifecycle: list[str], errors: list[str]
|
||||
) -> None:
|
||||
label = f"projects[{index}]"
|
||||
if not isinstance(project, dict):
|
||||
errors.append(f"{label} 必须是对象")
|
||||
return
|
||||
for key in ("name", "work_item_type", "release_status", "final_status"):
|
||||
require_text(project, key, label, errors)
|
||||
if project.get("release_status") != lifecycle[11]:
|
||||
errors.append(f"{label}.release_status 必须与 lifecycle[11] 一致")
|
||||
if project.get("final_status") != lifecycle[12]:
|
||||
errors.append(f"{label}.final_status 必须与 lifecycle[12] 一致")
|
||||
|
||||
labels = project.get("labels_observed")
|
||||
if not isinstance(labels, list) or not REQUIRED_LABELS.issubset(set(labels)):
|
||||
errors.append(f"{label}.labels_observed 必须包含:分析、设计、开发、测试")
|
||||
|
||||
repositories = project.get("repositories")
|
||||
if not isinstance(repositories, list) or not repositories:
|
||||
errors.append(f"{label}.repositories 至少包含一个仓库")
|
||||
else:
|
||||
for repo_index, repo in enumerate(repositories):
|
||||
repo_label = f"{label}.repositories[{repo_index}]"
|
||||
if not isinstance(repo, dict):
|
||||
errors.append(f"{repo_label} 必须是对象")
|
||||
continue
|
||||
for key in ("name", "role", "integration_branch", "evidence"):
|
||||
require_text(repo, key, repo_label, errors)
|
||||
if repo.get("integration_branch") not in {"dev", "develop"}:
|
||||
errors.append(f"{repo_label}.integration_branch 只能是实际存在的 dev/develop")
|
||||
for key in ("codeup_integrated", "required_for_mr_gate"):
|
||||
if not isinstance(repo.get(key), bool):
|
||||
errors.append(f"{repo_label}.{key} 必须是布尔值")
|
||||
|
||||
pipeline = project.get("test_pipeline")
|
||||
if not isinstance(pipeline, dict):
|
||||
errors.append(f"{label}.test_pipeline 必须是对象")
|
||||
else:
|
||||
for key in ("name", "environment", "evidence"):
|
||||
require_text(pipeline, key, f"{label}.test_pipeline", errors)
|
||||
for key in (
|
||||
"is_clone_for_callback_poc",
|
||||
"original_pipeline_unchanged",
|
||||
"docker_success_callback_enabled",
|
||||
):
|
||||
if not isinstance(pipeline.get(key), bool):
|
||||
errors.append(f"{label}.test_pipeline.{key} 必须是布尔值")
|
||||
security = pipeline.get("callback_security")
|
||||
if not isinstance(security, dict):
|
||||
errors.append(f"{label}.test_pipeline.callback_security 必须是对象")
|
||||
else:
|
||||
security_keys = (
|
||||
"signed",
|
||||
"timestamp_checked",
|
||||
"replay_protected",
|
||||
"idempotent_by_execution_id",
|
||||
"failure_retry_tested",
|
||||
)
|
||||
for key in security_keys:
|
||||
if not isinstance(security.get(key), bool):
|
||||
errors.append(f"{label}.test_pipeline.callback_security.{key} 必须是布尔值")
|
||||
if pipeline.get("docker_success_callback_enabled") is True:
|
||||
if pipeline.get("is_clone_for_callback_poc") is not True:
|
||||
errors.append(f"{label} 启用回调时必须使用test流水线副本")
|
||||
if pipeline.get("original_pipeline_unchanged") is not True:
|
||||
errors.append(f"{label} 启用回调时必须保持原流水线不变")
|
||||
if any(security.get(key) is not True for key in security_keys):
|
||||
errors.append(f"{label} 启用回调时必须完成全部安全与重试验证")
|
||||
|
||||
release = project.get("release_evidence")
|
||||
if not isinstance(release, dict):
|
||||
errors.append(f"{label}.release_evidence 必须是对象")
|
||||
else:
|
||||
if release.get("mode") not in VALID_RELEASE_EVIDENCE_MODES:
|
||||
errors.append(f"{label}.release_evidence.mode 不合法")
|
||||
for key in ("target_environment", "evidence"):
|
||||
require_text(release, key, f"{label}.release_evidence", errors)
|
||||
if release.get("test_success_is_production_release") is not False:
|
||||
errors.append(f"{label}.release_evidence.test_success_is_production_release 必须为 false")
|
||||
|
||||
test_plan = project.get("test_plan")
|
||||
if not isinstance(test_plan, dict):
|
||||
errors.append(f"{label}.test_plan 必须是对象")
|
||||
else:
|
||||
for key in ("name", "evidence"):
|
||||
require_text(test_plan, key, f"{label}.test_plan", errors)
|
||||
if not isinstance(test_plan.get("requirement_formally_related"), bool):
|
||||
errors.append(f"{label}.test_plan.requirement_formally_related 必须是布尔值")
|
||||
states = test_plan.get("case_states_observed")
|
||||
if not isinstance(states, list) or not states or not all(is_text(v) for v in states):
|
||||
errors.append(f"{label}.test_plan.case_states_observed 必须是非空文本数组")
|
||||
|
||||
defect = project.get("defect_workflow")
|
||||
if not isinstance(defect, dict):
|
||||
errors.append(f"{label}.defect_workflow 必须是对象")
|
||||
else:
|
||||
for key in ("fixed_status", "closed_status", "reopen_status", "evidence"):
|
||||
require_text(defect, key, f"{label}.defect_workflow", errors)
|
||||
if defect.get("fixed_is_terminal") is not False:
|
||||
errors.append(f"{label}.defect_workflow.fixed_is_terminal 必须为 false")
|
||||
if defect.get("tester_retest_required") is not True:
|
||||
errors.append(f"{label}.defect_workflow.tester_retest_required 必须为 true")
|
||||
|
||||
validate_rule_instances(project.get("rule_instances"), lifecycle, f"{label}.rule_instances", errors)
|
||||
validate_task_rule_instances(
|
||||
project.get("task_rule_instances"), f"{label}.task_rule_instances", errors
|
||||
)
|
||||
validate_control_list(project.get("control_coverage"), f"{label}.control_coverage", errors)
|
||||
if not isinstance(project.get("adjunct_automations"), list):
|
||||
errors.append(f"{label}.adjunct_automations 必须是数组,可为空")
|
||||
|
||||
|
||||
def validate(data: dict[str, Any]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if data.get("schema_version") != 3:
|
||||
errors.append("schema_version 必须为 3;旧版台账不包含阶段任务闭环")
|
||||
for key in ("organization", "work_item_type"):
|
||||
require_text(data, key, "profile", errors)
|
||||
|
||||
lifecycle = data.get("lifecycle")
|
||||
if not isinstance(lifecycle, list) or len(lifecycle) != 13 or not all(is_text(v) for v in lifecycle):
|
||||
errors.append("lifecycle 必须包含 13 个非空有序状态")
|
||||
lifecycle = [str(index) for index in range(13)]
|
||||
elif len(set(lifecycle)) != len(lifecycle):
|
||||
errors.append("lifecycle 状态名称不能重复")
|
||||
|
||||
labels = data.get("requirement_labels")
|
||||
if not isinstance(labels, list) or not REQUIRED_LABELS.issubset(set(labels)):
|
||||
errors.append("requirement_labels 必须包含:分析、设计、开发、测试")
|
||||
|
||||
policy = data.get("automation_policy")
|
||||
if not isinstance(policy, dict):
|
||||
errors.append("automation_policy 必须是对象")
|
||||
else:
|
||||
for key in ("require_current_state_condition", "inspect_rule_order_and_cascades"):
|
||||
if policy.get(key) is not True:
|
||||
errors.append(f"automation_policy.{key} 必须为 true")
|
||||
for key in ("allow_automatic_final_close_without_acceptance", "zero_related_items_are_complete"):
|
||||
if policy.get(key) is not False:
|
||||
errors.append(f"automation_policy.{key} 必须为 false")
|
||||
events = policy.get("development_start_event_candidates")
|
||||
if not isinstance(events, list) or not events or not all(is_text(v) for v in events):
|
||||
errors.append("automation_policy.development_start_event_candidates 不能为空")
|
||||
if policy.get("require_code_assets_linked_to_requirement_and_development_task") is not True:
|
||||
errors.append(
|
||||
"automation_policy.require_code_assets_linked_to_requirement_and_development_task 必须为 true"
|
||||
)
|
||||
idempotency = policy.get("stage_task_creation_idempotency_key_components")
|
||||
if not isinstance(idempotency, list) or not idempotency or not all(is_text(v) for v in idempotency):
|
||||
errors.append("automation_policy.stage_task_creation_idempotency_key_components 不能为空")
|
||||
|
||||
test_policy = data.get("test_closure_policy")
|
||||
if not isinstance(test_policy, dict):
|
||||
errors.append("test_closure_policy 必须是对象")
|
||||
else:
|
||||
if test_policy.get("defect_fixed_is_terminal") is not False:
|
||||
errors.append("test_closure_policy.defect_fixed_is_terminal 必须为 false")
|
||||
if test_policy.get("require_tester_retest") is not True:
|
||||
errors.append("test_closure_policy.require_tester_retest 必须为 true")
|
||||
for key in ("accepted_case_states", "rejected_case_states"):
|
||||
values = test_policy.get(key)
|
||||
if not isinstance(values, list) or not values or not all(is_text(v) for v in values):
|
||||
errors.append(f"test_closure_policy.{key} 不能为空")
|
||||
|
||||
projects = data.get("projects")
|
||||
if not isinstance(projects, list) or not projects:
|
||||
errors.append("projects 至少包含一个项目")
|
||||
else:
|
||||
names: set[str] = set()
|
||||
for index, project in enumerate(projects):
|
||||
validate_project(project, index, lifecycle, errors)
|
||||
if isinstance(project, dict) and is_text(project.get("name")):
|
||||
name = project["name"]
|
||||
if name in names:
|
||||
errors.append(f"项目名称重复:{name}")
|
||||
names.add(name)
|
||||
|
||||
sensitive = walk_sensitive_keys(data)
|
||||
if sensitive:
|
||||
errors.append("配置中禁止出现敏感字段:" + "、".join(sensitive))
|
||||
if "请填写" in json.dumps(data, ensure_ascii=False):
|
||||
errors.append("仍存在“请填写”占位符")
|
||||
return errors
|
||||
|
||||
|
||||
def render(data: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"# 云效需求生命周期自动化执行计划",
|
||||
"",
|
||||
f"- 组织:{data['organization']}",
|
||||
f"- 工作项类型:{data['work_item_type']}",
|
||||
f"- 项目数:{len(data['projects'])}",
|
||||
"- 完整性口径:每项目12个需求规则实例、8个任务规则实例、31个闭环控制项。",
|
||||
"- 安全边界:不修改现有生产流水线;不记录任何凭据或密钥。",
|
||||
"",
|
||||
]
|
||||
for project in data["projects"]:
|
||||
lines.extend([
|
||||
f"## 项目:{project['name']}",
|
||||
"",
|
||||
f"- 发布状态:{project['release_status']}",
|
||||
f"- 最终状态:{project['final_status']}",
|
||||
f"- test流水线:{project['test_pipeline']['name']}",
|
||||
f"- 发布证据方式:{project['release_evidence']['mode']}",
|
||||
"",
|
||||
"### 仓库覆盖",
|
||||
"",
|
||||
"| 仓库 | 角色 | 集成分支 | Codeup已集成 | 纳入全部MR门禁 | 证据 |",
|
||||
"|---|---|---|---|---|---|",
|
||||
])
|
||||
for repo in project["repositories"]:
|
||||
lines.append(
|
||||
f"| {repo['name']} | {repo['role']} | {repo['integration_branch']} | "
|
||||
f"{'是' if repo['codeup_integrated'] else '否'} | "
|
||||
f"{'是' if repo['required_for_mr_gate'] else '否'} | {repo['evidence']} |"
|
||||
)
|
||||
lines.extend([
|
||||
"",
|
||||
"### R01–R12实际规则实例",
|
||||
"",
|
||||
"| ID | 实现 | 实际规则/门禁 | 触发 | 流转 | 条件 | 顺序 | 启用 | 证据 |",
|
||||
"|---|---|---|---|---|---|---:|---|---|",
|
||||
])
|
||||
for rule in sorted(project["rule_instances"], key=lambda item: item["order"]):
|
||||
conditions = ";".join(rule["conditions"])
|
||||
lines.append(
|
||||
f"| {rule['id']} | {rule['mode']} | {rule['actual_rule_name']} | {rule['trigger']} | "
|
||||
f"{rule['source_status']} → {rule['target_status']} | {conditions} | {rule['order']} | "
|
||||
f"{'是' if rule['enabled'] else '否'} | {rule['evidence']} |"
|
||||
)
|
||||
lines.extend([
|
||||
"",
|
||||
"### TK01–TK08任务规则实例",
|
||||
"",
|
||||
"| ID | 实现 | 实际规则/门禁 | 触发 | 作用域 | 源状态 | 条件 | 动作 | 顺序 | 启用 | 证据 |",
|
||||
"|---|---|---|---|---|---|---|---|---:|---|---|",
|
||||
])
|
||||
for rule in sorted(project["task_rule_instances"], key=lambda item: item["order"]):
|
||||
conditions = ";".join(rule["conditions"])
|
||||
actions = ";".join(rule["actions"])
|
||||
lines.append(
|
||||
f"| {rule['id']} | {rule['mode']} | {rule['actual_rule_name']} | {rule['trigger']} | "
|
||||
f"{rule['scope']} | {rule['source_state']} | {conditions} | {actions} | {rule['order']} | "
|
||||
f"{'是' if rule['enabled'] else '否'} | {rule['evidence']} |"
|
||||
)
|
||||
lines.extend([
|
||||
"",
|
||||
"### 31项完整性台账",
|
||||
"",
|
||||
"| 控制ID | 实现方式 | 启用 | 观察证据 | 负责人 | 回退方案 |",
|
||||
"|---|---|---|---|---|---|",
|
||||
])
|
||||
for control in sorted(project["control_coverage"], key=lambda item: item["id"]):
|
||||
lines.append(
|
||||
f"| {control['id']} | {control['mode']} | {'是' if control['enabled'] else '否'} | "
|
||||
f"{control['evidence']} | {control['owner']} | {control['fallback']} |"
|
||||
)
|
||||
adjunct = project["adjunct_automations"]
|
||||
lines.extend([
|
||||
"",
|
||||
"### 范围外通用自动化盘点",
|
||||
"",
|
||||
"- " + (";".join(map(str, adjunct)) if adjunct else "未纳入;如需通知、自动指派、SLA、字段同步等,应单独授权和设计。"),
|
||||
"",
|
||||
])
|
||||
lines.extend([
|
||||
"## 执行检查",
|
||||
"",
|
||||
"- [ ] 两个或多个项目的规则实例、控制台账和证据已分别填写。",
|
||||
"- [ ] 已逐条读取真实触发配置、规则顺序、启用状态和执行账号。",
|
||||
"- [ ] 已检查后继规则、重复规则和跨阶段连锁流转。",
|
||||
"- [ ] 已验证失败用例、缺陷已修复、复测通过/失败时双方状态一致。",
|
||||
"- [ ] 已逐仓库确认真实dev/develop分支、正式关联和全部MR门禁。",
|
||||
"- [ ] 已区分test部署、生产发布、发布变更和业务验收。",
|
||||
"- [ ] 已执行正向、负向、异步、多仓库、失败重试和旧规则连锁测试。",
|
||||
"",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
validate_parser = subparsers.add_parser("validate", help="验证项目配置")
|
||||
validate_parser.add_argument("profile", type=Path)
|
||||
render_parser = subparsers.add_parser("render", help="生成 Markdown 执行计划")
|
||||
render_parser.add_argument("profile", type=Path)
|
||||
render_parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
data = load_profile(args.profile)
|
||||
errors = validate(data)
|
||||
except (OSError, json.JSONDecodeError, ValueError) as error:
|
||||
print(f"配置读取失败:{error}", file=sys.stderr)
|
||||
return 2
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"[错误] {error}", file=sys.stderr)
|
||||
return 1
|
||||
if args.command == "validate":
|
||||
print(
|
||||
f"[通过] 配置有效:{len(data['projects'])} 个项目;"
|
||||
"每项目12个需求规则实例、8个任务规则实例、31个闭环控制项。"
|
||||
)
|
||||
return 0
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(render(data), encoding="utf-8")
|
||||
print(f"[完成] 已生成执行计划:{args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -25,6 +25,7 @@
|
||||
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
|
||||
| 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw`、`canvas-assets/` | 原型画布和画布素材 |
|
||||
| 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` |
|
||||
| AutoPRD + 标注目录 | `.spec/requirements-prd.md` + `annotation-source.json`;定稿写第 10 章功能变更 | skill `oneos-autoprd`;规则 `.cursor/rules/oneos-autoprd-sync.mdc` |
|
||||
| 对象存储发布 URL | `.axhub/make/axhub.config.json` → `cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` |
|
||||
| 原型标注布局 | `src/common/prototype-annotation-host.tsx` | `rules/prototype-annotation-layout-guide.md` |
|
||||
|
||||
|
||||
65
docs/superpowers/plans/2026-07-20-h5-vehicle-assets.md
Normal file
65
docs/superpowers/plans/2026-07-20-h5-vehicle-assets.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 车辆资产 H5 Implementation Plan
|
||||
|
||||
> **For agentic workers:** Execute task-by-task. Checkboxes track progress. Prefer inline execution in this repo (React prototype, no separate unit-test harness).
|
||||
|
||||
**Goal:** 新建独立 H5 原型 `oneos-h5-vehicle-assets`:只读查车 + KPI/异常筛选 + 详情多 Tab,口径对齐 PC `vehicle-management`。
|
||||
|
||||
**Architecture:** PhoneShell 手机壳 + 列表/详情双视图;数据与 KPI 规则复用 PC `vehicles.json` 与 `utils/vehicle.ts`(及各记录 utils);UI 不复用 `vm-page` 宽表 DOM。
|
||||
|
||||
**Tech Stack:** React + TypeScript、本地 JSON 种子、Axhub annotation、`nav:sync` 注册导航。
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| Path | Responsibility |
|
||||
|------|----------------|
|
||||
| `src/prototypes/oneos-h5-vehicle-assets/index.tsx` | 入口:列表/详情状态、筛选、分页 |
|
||||
| `styles/index.css` | H5 样式(ONE-OS 绿) |
|
||||
| `components/PhoneShell.tsx` | 手机预览壳 |
|
||||
| `components/VehicleList.tsx` | 搜索、KPI、筛选抽屉、卡片 |
|
||||
| `components/VehicleCard.tsx` | 单车卡片 |
|
||||
| `components/FilterSheet.tsx` | 证照/保险状态抽屉 |
|
||||
| `components/VehicleDetail.tsx` | 详情顶栏 + Tab |
|
||||
| `components/detailTabs.tsx` | 11 个只读 Tab 内容 |
|
||||
| `utils/filter.ts` | 列表过滤封装 |
|
||||
| `.spec/requirements-prd.md` | AutoPRD |
|
||||
| `annotation-source.json` | 标注目录 |
|
||||
| `nav-menu.json` + `nav:sync` | 导航注册 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Scaffold + list shell
|
||||
|
||||
- [ ] Create prototype files with PhoneShell, list layout, import vehicles
|
||||
- [ ] Wire KPI via `matchKpi` / `countKpi` / `isLicenseExpired`
|
||||
- [ ] Search + license/insurance filter + cards + page size 20
|
||||
|
||||
### Task 2: Detail multi-tab readonly
|
||||
|
||||
- [ ] Detail view with 11 tabs matching PC `DETAIL_TABS`
|
||||
- [ ] Basic/model/license as field grids; record tabs via PC utils filtered by plate
|
||||
- [ ] No edit/action buttons
|
||||
|
||||
### Task 3: Nav + PRD + annotation
|
||||
|
||||
- [ ] Add nav item under 车辆资产 folder as「车辆资产(H5)」
|
||||
- [ ] Full AutoPRD + annotation-source PRD node
|
||||
- [ ] Run `npm run nav:sync -- --prototype oneos-h5-vehicle-assets`
|
||||
|
||||
### Task 4: Smoke check
|
||||
|
||||
- [ ] Open `/prototypes/oneos-h5-vehicle-assets` and verify list → detail → tabs
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage
|
||||
|
||||
| Spec item | Task |
|
||||
|-----------|------|
|
||||
| Independent H5 prototype | 1 |
|
||||
| Search + KPI + anomaly filter + cards | 1 |
|
||||
| Detail 11 tabs readonly | 2 |
|
||||
| No edit including admin | 1–2 |
|
||||
| Nav + PRD | 3 |
|
||||
| KPI/anomaly口径 = PC | 1 (shared utils) |
|
||||
189
docs/superpowers/specs/2026-07-20-h5-vehicle-assets-design.md
Normal file
189
docs/superpowers/specs/2026-07-20-h5-vehicle-assets-design.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# 车辆资产 H5 适配 · 设计规格
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 日期 | 2026-07-20 |
|
||||
| 状态 | 已确认(2026-07-20) |
|
||||
| 原型 ID | `oneos-h5-vehicle-assets` |
|
||||
| 关联 PC | `vehicle-management`(车辆资产) |
|
||||
| 参考 H5 | `oneos-h5-h2-order`(PhoneShell、交互密度) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
App 除推送外全部接入 H5。PC「车辆资产」为宽表中后台,不适合同一页响应式塞进手机。采用**独立 H5 原型**,口径与 PC 对齐,交互按手机重做。
|
||||
|
||||
### 成功标准
|
||||
|
||||
1. App / 手机浏览器打开 H5 地址,不进入 PC 宽表页。
|
||||
2. KPI 分类与证照/保险异常口径与 PC 一致。
|
||||
3. 详情多 Tab 可浏览,**全程无编辑入口**(含 Admin)。
|
||||
4. 按车牌搜索可定位车辆。
|
||||
|
||||
---
|
||||
|
||||
## 2. 已确认产品决策
|
||||
|
||||
| 决策 | 结论 |
|
||||
|---|---|
|
||||
| 落地方式 | 独立原型 `oneos-h5-vehicle-assets`(方案 1) |
|
||||
| 角色 | 全角色同一套界面 |
|
||||
| 编辑 | **手机端不做编辑**(含 Admin;改数仅 PC) |
|
||||
| 异常 | 只看:可筛、详情标红/提示;不催办、不跳转业务处理页 |
|
||||
| 列表 | 搜索 + 横滑 KPI + 筛选抽屉 + 车辆卡片(对齐 PC 信息量) |
|
||||
| 详情 | 多 Tab 对齐 PC,只读 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 范围
|
||||
|
||||
### 3.1 第一期做
|
||||
|
||||
| 模块 | 内容 |
|
||||
|---|---|
|
||||
| 列表 | 顶栏标题「车辆资产」+ 车牌搜索;KPI 横滑;证照/保险等筛选;卡片列表;**触底异步加载更多**(无翻页、不展示总辆数) |
|
||||
| 详情 | 返回 + 车牌(透明顶栏贴一体背景);浮起白卡摘要(营运状态、标签、里程、证照/保险异常);横向 Tab + 只读内容;**无**底部悬浮 Tab |
|
||||
| 数据 | 复用 PC `vehicles.json` 及关联记录 JSON;KPI/异常规则与 PC 同源工具函数(可抽共享或复制后标注同源) |
|
||||
| 壳层 | PhoneShell 桌面预览;真机/WebView 全屏内容区 |
|
||||
| 导航 | 注册到原型导航「运维管理」下,与 PC 车辆资产并列区分(如「车辆资产(H5)」) |
|
||||
|
||||
### 3.2 第一期不做
|
||||
|
||||
- 任意字段编辑、运维负责人修改、运营城市修改
|
||||
- 批量导入 / 导出真实能力(不做入口,或仅隐藏)
|
||||
- 催办、抄送、跳转证照管理/保险采购等业务页(不做;外链 Toast 占位也不做,避免误导)
|
||||
- 在租车型占比弹窗(PC 尾部 KPI;H5 第一期可不做,避免干扰主路径)
|
||||
- PC 页响应式改造
|
||||
|
||||
---
|
||||
|
||||
## 4. 信息架构
|
||||
|
||||
```text
|
||||
列表 List
|
||||
├── 顶栏:标题 + 搜索(车牌)
|
||||
├── KPI 横滑条(点选切换分类,计数与 PC 一致)
|
||||
├── 筛选入口 → 底部抽屉(证照状态、保险状态;可扩展运营状态等)
|
||||
└── 车辆卡片列表 → 进入详情
|
||||
|
||||
详情 Detail
|
||||
├── 顶栏:返回 + 车牌号(透明,贴一体渐变底;无底栏)
|
||||
├── 摘要卡:车牌 / 品牌型号 / 营运状态 / 来源·城市·项目标签 / 里程 / 证照·保险异常
|
||||
└── Tab(横滑):与 PC DETAIL_TABS 一致
|
||||
基本信息 / 型号参数 / 证照信息 / 保险记录 / 租赁记录 /
|
||||
事故记录 / 故障记录 / 违章记录 / 异动记录 / 调拨记录 / 年审记录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. KPI 与异常口径(与 PC 一致)
|
||||
|
||||
| KPI | 口径 |
|
||||
|---|---|
|
||||
| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |
|
||||
| 租赁车辆 | `operateStatus === '租赁'` |
|
||||
| 物流车辆 | `operateStatus === '物流'` |
|
||||
| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |
|
||||
| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |
|
||||
| 退出运营车辆 | `operateStatus === '退出运营'` |
|
||||
|
||||
| 异常筛选 | 口径 |
|
||||
|---|---|
|
||||
| 证照异常 | `inspectExpire` 早于今天 |
|
||||
| 保险异常 | `insuranceStatus === '异常'` |
|
||||
|
||||
筛选与 KPI **叠加**:先 KPI,再筛选条件。
|
||||
|
||||
---
|
||||
|
||||
## 6. 列表卡片字段(建议)
|
||||
|
||||
Boutique 横卡布局:左侧信息、右侧状态区(无车辆照片)。至少展示:
|
||||
|
||||
- 车牌(主标题)
|
||||
- 品牌·型号
|
||||
- 营运状态标签(语义色)
|
||||
- 证照/保险异常角标(有异常时显示)
|
||||
- 车辆来源 / 运营城市 / 项目或「车辆在库」标签
|
||||
- 里程(有数据时,右侧次要数字)
|
||||
|
||||
点击整卡进入详情。
|
||||
|
||||
### 6.1 详情摘要卡(与列表同语义)
|
||||
|
||||
浮起白卡(同列表卡片圆角/阴影),**不加**车辆实景大图:
|
||||
|
||||
- 车牌(大标题)+ 品牌·型号
|
||||
- 营运状态(圆点 + 与列表一致的文案)
|
||||
- 车辆来源 / 运营城市 / 项目或备车等上下文标签
|
||||
- 里程(有数据时)
|
||||
- 证照/保险异常角标(有异常时)
|
||||
|
||||
Tab 选中态对齐 Chip(主色字 + 浅底),内容区为白卡 Section / 记录列表;字段 label 12 / value 15。
|
||||
|
||||
---
|
||||
|
||||
## 7. 视觉与设计基底
|
||||
|
||||
| 项 | 约定 |
|
||||
|---|---|
|
||||
| 交互壳 | 对齐 `oneos-h5-h2-order` PhoneShell 结构(桌面预览);真机全屏内容区 |
|
||||
| 主题 | **SaaS Boutique App 壳**:对齐参考图 A(顶栏左标题 + 铃铛/加号、KPI 图标卡、筛选项行、实景横卡、底部五 Tab) |
|
||||
| 色彩 | Primary `#007AFF`;页面灰 `#F2F3F5`;卡片白底;语义绿/橙/红 |
|
||||
| 风格 | 一体式浅蓝渐变底(列表与详情共用;顶栏透明不分色带);KPI 图标卡 + Chip;列表/详情浮起白卡(无车辆照片) |
|
||||
| 字体 | **苹方 PingFang SC**(回退 Hiragino Sans GB / 微软雅黑 / system) |
|
||||
| 字号阶梯 | 标题 22 / 详情车牌 26 / KPI 数 20 / 列表车牌 17 / 正文 15 / 车型与 Chip 12 / 标签与底栏 11 |
|
||||
| 字重 | 标题/车牌 800(系统会映射为 Semibold/Medium);区块标题 700;正文 400–500;标签 600 |
|
||||
| 壳层约定 | **列表**底部为悬浮胶囊 Tab(毛玻璃);「工作台/任务/消息/我的」Toast 占位;本页「车辆」选中。**详情**不挂底栏,仅顶栏返回 + 车牌 |
|
||||
| 触控 | ≥ 44×44px(Chip/图标区可视觉更紧,点击热区保留);`safe-area-inset` |
|
||||
| 动效 | ≤ 240ms;尊重 `prefers-reduced-motion` |
|
||||
|
||||
---
|
||||
|
||||
## 8. 技术约定(实现阶段)
|
||||
|
||||
| 项 | 约定 |
|
||||
|---|---|
|
||||
| 目录 | `src/prototypes/oneos-h5-vehicle-assets/` |
|
||||
| 入口 | `index.tsx`;样式独立 `styles/index.css` |
|
||||
| 标注 | `annotation-source.json` + `.spec/requirements-prd.md`(实现时按 oneos-autoprd 同步) |
|
||||
| 与 PC 关系 | 逻辑口径同源;UI 组件不直接复用 `vm-page` 宽表 DOM |
|
||||
| App 打开 | 固定 H5 URL(发布后 `{baseUrl}/oneos-h5-vehicle-assets/index.html` 形态以云发布规则为准) |
|
||||
|
||||
---
|
||||
|
||||
## 9. 用户故事(摘要)
|
||||
|
||||
1. **作为**任意登录用户,**我想**在手机上按车牌找到车,**以便**外勤快速核对车辆信息。
|
||||
2. **作为**运维/业务,**我想**用 KPI 与证照/保险异常筛选缩小范围,**以便**发现需跟进车辆(跟进在 PC 或其他模块完成)。
|
||||
3. **作为**任意用户,**我想**在详情里切换各业务 Tab 只读浏览,**以便**不回 PC 也能看全档案摘要与记录。
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收清单
|
||||
|
||||
- [ ] 独立原型可在导航打开,桌面 PhoneShell / 窄屏可用
|
||||
- [ ] KPI 六类计数与同数据下 PC 一致
|
||||
- [ ] 证照异常、保险异常筛选结果与 PC 规则一致
|
||||
- [ ] 搜索车牌可过滤列表
|
||||
- [ ] 列表无总辆数、无翻页;触底异步加载更多
|
||||
- [ ] 列表进详情:一体渐变底连续,无白顶栏色带割裂;摘要为浮起白卡(状态/标签/里程/异常与列表同语义)
|
||||
- [ ] 详情无底部悬浮 Tab;顶栏仅返回 + 车牌
|
||||
- [ ] 详情 11 个 Tab 均可进入且只读,无编辑按钮
|
||||
- [ ] 无导入/导出/修改运维负责人等 PC 写操作入口
|
||||
|
||||
---
|
||||
|
||||
## 11. 修订记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|---|---|---|
|
||||
| v0.7 | 2026-07-21 | 列表改为触底异步加载;移除翻页与总辆数展示 |
|
||||
| v0.6 | 2026-07-21 | 对齐参考图 A 结构:App 顶栏/底栏、KPI 图标卡、筛选项行、实景横卡;写操作仍 Toast 占位 |
|
||||
| v0.5 | 2026-07-21 | 视觉切换至 SaaS Boutique(Electric Blue + KPI 摘要卡 + 横卡列表);产品范围不变 |
|
||||
| v0.5 | 2026-07-21 | H5 按参考图标注尺寸落地(16/8/12/44 栅格) |
|
||||
| v0.4 | 2026-07-20 | 手机交互增强:粘性工具栏、safe-area、系统返回、空态引导 |
|
||||
| v0.3 | 2026-07-20 | 视觉切换至 `src/themes/apple`(Action Blue + utility card) |
|
||||
| v0.2 | 2026-07-20 | 视觉基底补充:Premium SaaS Mobile 质感(品牌绿不变) |
|
||||
| v0.1 | 2026-07-20 | 需求访谈确认后首版设计规格 |
|
||||
@@ -0,0 +1,16 @@
|
||||
# H5 车辆卡片 · 里程任务区设计
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 日期 | 2026-07-21 |
|
||||
| 状态 | 已确认 |
|
||||
| 原型 | `oneos-h5-vehicle-assets` |
|
||||
| 规格全文 | `src/prototypes/oneos-h5-vehicle-assets/.spec/mileage-task.md` |
|
||||
|
||||
## 结论摘要
|
||||
|
||||
- 列表每张车卡在标签行下展示「里程任务」区块(全宽)。
|
||||
- 有任务:标题 + 完成百分比 + 进度条 +「剩余 xx km · 预计 xx 天内完成」。
|
||||
- 无任务:标题 +「暂无里程任务」。
|
||||
- 预计天数 = 剩余里程 ÷ 近 7 个活跃日(日里程 > 0)平均日里程,向上取整;无活跃日则「暂无法预计」。
|
||||
- 业务来源:采购合同关联批次车辆里程要求,经任务工单下发(原型用本地演示种子,未接真实 API)。
|
||||
110
docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.md
Normal file
110
docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 车辆资产 UI · 边距与尺寸标注(估算)
|
||||
|
||||
> 来源:参考截图标注
|
||||
> 基准宽:**390pt**(iPhone 常见逻辑宽)
|
||||
> 方法:按屏内容像素测量后换算,并按 **8pt 栅格**圆整
|
||||
> 标注图:`docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.png`
|
||||
|
||||
## 1. 页面骨架
|
||||
|
||||
| 元素 | 尺寸 / 边距 | 备注 |
|
||||
|------|-------------|------|
|
||||
| 内容区宽度 | **390** | 逻辑宽 |
|
||||
| 左右页边距 | **16** | 搜索、KPI、列表、Chip 统一 |
|
||||
| 主内容可用宽 | **358** | 390 − 16×2 |
|
||||
| 区块纵向间距 | **12–16** | 搜索→KPI、Chip→列表 |
|
||||
|
||||
## 2. 顶栏
|
||||
|
||||
| 元素 | 尺寸 | 备注 |
|
||||
|------|------|------|
|
||||
| 标题行高 | **40** | 「车辆资产」+ 右侧操作 |
|
||||
| 标题字号 | **≈22–24 / Semibold** | SF Pro Display 观感 |
|
||||
| 通知铃 / 加号 | **36×36** | 触控建议扩到 44 |
|
||||
| 右侧图标间距 | **8** | |
|
||||
|
||||
## 3. 搜索栏
|
||||
|
||||
| 元素 | 尺寸 | 备注 |
|
||||
|------|------|------|
|
||||
| 高度 | **44** | 满足触控下限 |
|
||||
| 宽度 | **358** | 满宽 − 页边距 |
|
||||
| 圆角 | **12** | |
|
||||
| 左右内边距 | **12** | 左放大镜 / 右扫码 |
|
||||
| 占位字号 | **15–17** | |
|
||||
|
||||
## 4. KPI 横滑卡片
|
||||
|
||||
| 元素 | 尺寸 | 备注 |
|
||||
|------|------|------|
|
||||
| 卡片宽 × 高 | **90 × 100** | 约露出 4 张 |
|
||||
| 卡片间距 | **8** | |
|
||||
| 圆角 | **12–16** | |
|
||||
| 主数字 | **≈20–22 / Bold** | tabular-nums |
|
||||
| 分类标题 | **12** | |
|
||||
| 底部分页点 | **Ø6 / 间距 6** | |
|
||||
|
||||
## 5. 筛选 Chip 行
|
||||
|
||||
| 元素 | 尺寸 | 备注 |
|
||||
|------|------|------|
|
||||
| Chip 高 | **32** | 若偏难点可提到 36 |
|
||||
| Chip 宽 | **≈78–90** | 随文案 |
|
||||
| Chip 间距 | **8** | |
|
||||
| 圆角 | **8** | |
|
||||
| 字号 | **13–14** | 选中态浅蓝底 |
|
||||
|
||||
## 6. 车辆列表卡片
|
||||
|
||||
| 元素 | 尺寸 | 备注 |
|
||||
|------|------|------|
|
||||
| 卡片宽 × 高 | **358 × 118** | |
|
||||
| 卡片圆角 | **16** | |
|
||||
| 卡片垂直间距 | **12** | |
|
||||
| 卡片内边距 | **12** | 四边一致 |
|
||||
| 车辆缩略图 | **100 × 74** | 圆角 ≈8 |
|
||||
| 图文间距 | **12** | 缩略图 ↔ 文案 |
|
||||
| 车牌字号 | **≈17–18 / Semibold** | |
|
||||
| 车型副文 | **13–14** | 次要灰 |
|
||||
| Tag 高 | **≈22** | 圆角 6 |
|
||||
| Tag 间距 | **6–8** | |
|
||||
| 右侧状态区 | **≈72 宽** | 状态点 + 里程 + 异常标 |
|
||||
| 右箭头 | **12–16** | |
|
||||
|
||||
## 7. 底栏
|
||||
|
||||
| 元素 | 尺寸 | 备注 |
|
||||
|------|------|------|
|
||||
| 底栏总高 | **≈83** | 含 Home Indicator ≈34 |
|
||||
| 可点区域高 | **≈49** | |
|
||||
| Tab 等分 | **5** | |
|
||||
| 图标 | **24×24** | |
|
||||
| 文案 | **10** | |
|
||||
| 中心「工作台」 | **⌀56** | 略浮起 |
|
||||
| 消息红点 | **≈16–18** | |
|
||||
|
||||
## 8. 推荐实现 Token(可直接写 CSS)
|
||||
|
||||
```css
|
||||
--page-pad: 16px;
|
||||
--gap-sm: 8px;
|
||||
--gap-md: 12px;
|
||||
--gap-lg: 16px;
|
||||
--radius-chip: 8px;
|
||||
--radius-control: 12px;
|
||||
--radius-card: 16px;
|
||||
--search-h: 44px;
|
||||
--chip-h: 32px;
|
||||
--kpi-w: 90px;
|
||||
--kpi-h: 100px;
|
||||
--thumb-w: 100px;
|
||||
--thumb-h: 74px;
|
||||
--card-pad: 12px;
|
||||
--touch-min: 44px;
|
||||
```
|
||||
|
||||
## 9. 说明
|
||||
|
||||
- 数值为**视觉估算**,不是设计工具导出的精确标注。
|
||||
- 若要以设计稿为准,请用 Figma / Sketch 再量一次关键项(页边距、卡片高、缩略图)。
|
||||
- 手机实现时:**触控热区 ≥ 44×44**,即使视觉图标只有 24/36。
|
||||
BIN
docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.png
Normal file
BIN
docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 457 KiB |
413
src/common/oneos-app-shell/OneOsAppShell.tsx
Normal file
413
src/common/oneos-app-shell/OneOsAppShell.tsx
Normal file
@@ -0,0 +1,413 @@
|
||||
import './oneos-app-shell.css';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
LayoutDashboard,
|
||||
Menu,
|
||||
Moon,
|
||||
Sun,
|
||||
PanelLeftClose,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Maximize,
|
||||
X,
|
||||
Sparkles,
|
||||
type LucideIcon,
|
||||
FolderOpen,
|
||||
FileText,
|
||||
Truck,
|
||||
Shield,
|
||||
Wallet,
|
||||
Fuel,
|
||||
BatteryCharging,
|
||||
Users,
|
||||
Workflow,
|
||||
Settings,
|
||||
ClipboardList,
|
||||
BarChart3,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import navMenuJson from '../../prototypes/oneos-prototype-nav/nav-menu.json';
|
||||
import {
|
||||
buildShellMenuFromNav,
|
||||
collectOpenKeys,
|
||||
findMenuPath,
|
||||
hrefPathname,
|
||||
type ShellMenuItem,
|
||||
} from './nav-from-prototypes';
|
||||
import {
|
||||
readStoredOneOsTheme,
|
||||
setOneOsTheme,
|
||||
type OneOsTheme,
|
||||
} from './theme';
|
||||
import { ShellNoticeCenter } from './ShellNoticeCenter';
|
||||
import {
|
||||
isNoticesSync,
|
||||
postNoticeAction,
|
||||
type ShellNoticeItem,
|
||||
} from './notice-bridge';
|
||||
|
||||
const AVATAR_URL = 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp';
|
||||
|
||||
/** 演示壳自身,不在内容区嵌套打开 */
|
||||
export const PROTOTYPE_DEMO_HREF = '/prototypes/oneos-prototype-demo';
|
||||
|
||||
function iconForLabel(label: string): LucideIcon {
|
||||
if (/原型演示|工作台/.test(label)) return LayoutDashboard;
|
||||
if (/审批/.test(label)) return ClipboardList;
|
||||
if (/车辆|运维|资产/.test(label)) return Truck;
|
||||
if (/安全/.test(label)) return Shield;
|
||||
if (/财务|应收|收款|应结/.test(label)) return Wallet;
|
||||
if (/加氢|氢/.test(label)) return Fuel;
|
||||
if (/充电/.test(label)) return BatteryCharging;
|
||||
if (/用户|客户|机构|角色|部门/.test(label)) return Users;
|
||||
if (/工作流|流程/.test(label)) return Workflow;
|
||||
if (/系统|菜单|字典|参数|日志/.test(label)) return Settings;
|
||||
if (/BI|统计|分析|台账|明细|盈亏|回款/.test(label)) return BarChart3;
|
||||
if (/合同|业务|保险|供应商/.test(label)) return FileText;
|
||||
if (/任务|工单|调度/.test(label)) return Wrench;
|
||||
if (/配置|模板/.test(label)) return FolderOpen;
|
||||
return FolderOpen;
|
||||
}
|
||||
|
||||
export type ShellTab = {
|
||||
href: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type OneOsAppShellProps = {
|
||||
children: React.ReactNode;
|
||||
activeHref?: string;
|
||||
pageTitle?: string;
|
||||
breadcrumb?: string[];
|
||||
/** 自定义导航:用于演示壳在内容区切换;不传则整页跳转 */
|
||||
onNavigate?: (item: ShellMenuItem) => void;
|
||||
/** 多页签(演示壳) */
|
||||
tabs?: ShellTab[];
|
||||
onTabSelect?: (href: string) => void;
|
||||
onTabClose?: (href: string) => void;
|
||||
onRefresh?: () => void;
|
||||
brandHref?: string;
|
||||
/** 原型演示:顶栏「版本更新」图标,打开更新日志 */
|
||||
onOpenReleaseNotes?: () => void;
|
||||
/** 是否有未读版本更新(显示角标) */
|
||||
releaseNotesUnread?: boolean;
|
||||
};
|
||||
|
||||
export function OneOsAppShell({
|
||||
children,
|
||||
activeHref = PROTOTYPE_DEMO_HREF,
|
||||
pageTitle = '原型演示',
|
||||
breadcrumb,
|
||||
onNavigate: onNavigateProp,
|
||||
tabs,
|
||||
onTabSelect,
|
||||
onTabClose,
|
||||
onRefresh,
|
||||
brandHref = PROTOTYPE_DEMO_HREF,
|
||||
onOpenReleaseNotes,
|
||||
releaseNotesUnread = false,
|
||||
}: OneOsAppShellProps) {
|
||||
const menu = useMemo(
|
||||
() => buildShellMenuFromNav(navMenuJson as Parameters<typeof buildShellMenuFromNav>[0]),
|
||||
[],
|
||||
);
|
||||
const path = useMemo(() => findMenuPath(menu, activeHref) || [], [menu, activeHref]);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [openKeys, setOpenKeys] = useState<string[]>(() => collectOpenKeys(path));
|
||||
const [theme, setTheme] = useState<OneOsTheme>(() => readStoredOneOsTheme());
|
||||
const [shellNotices, setShellNotices] = useState<ShellNoticeItem[]>([]);
|
||||
const [shellUnread, setShellUnread] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setOneOsTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMsg = (e: MessageEvent) => {
|
||||
if (!isNoticesSync(e.data)) return;
|
||||
setShellNotices(e.data.notices);
|
||||
setShellUnread(e.data.unreadCount);
|
||||
};
|
||||
window.addEventListener('message', onMsg);
|
||||
return () => window.removeEventListener('message', onMsg);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setShellNotices([]);
|
||||
setShellUnread(0);
|
||||
}, [activeHref]);
|
||||
|
||||
const frameWindow = useCallback((): Window | null => {
|
||||
if (typeof document === 'undefined') return null;
|
||||
const frame = document.querySelector<HTMLIFrameElement>('.oneos-shell-frame');
|
||||
return frame?.contentWindow || null;
|
||||
}, []);
|
||||
|
||||
const onNoticeRead = useCallback(
|
||||
(notice: ShellNoticeItem) => {
|
||||
setShellNotices((prev) =>
|
||||
prev.map((n) => (n.id === notice.id ? { ...n, read: true } : n)),
|
||||
);
|
||||
setShellUnread((c) => Math.max(0, c - (notice.read ? 0 : 1)));
|
||||
const win = frameWindow();
|
||||
if (win) postNoticeAction(win, 'read', notice.id);
|
||||
},
|
||||
[frameWindow],
|
||||
);
|
||||
|
||||
const onNoticeOpen = useCallback(
|
||||
(notice: ShellNoticeItem) => {
|
||||
const win = frameWindow();
|
||||
if (win) postNoticeAction(win, 'open', notice.id);
|
||||
},
|
||||
[frameWindow],
|
||||
);
|
||||
|
||||
const onNoticeHandle = useCallback(
|
||||
(notice: ShellNoticeItem) => {
|
||||
setShellNotices((prev) =>
|
||||
prev.map((n) => (n.id === notice.id ? { ...n, read: true } : n)),
|
||||
);
|
||||
setShellUnread((c) => Math.max(0, c - (notice.read ? 0 : 1)));
|
||||
const win = frameWindow();
|
||||
if (win) postNoticeAction(win, 'handle', notice.id);
|
||||
},
|
||||
[frameWindow],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenKeys((prev) => {
|
||||
const next = collectOpenKeys(path);
|
||||
return Array.from(new Set([...prev, ...next]));
|
||||
});
|
||||
}, [path]);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme((prev) => {
|
||||
const next: OneOsTheme = prev === 'dark' ? 'light' : 'dark';
|
||||
setOneOsTheme(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const crumbs = breadcrumb?.length
|
||||
? breadcrumb
|
||||
: path.length
|
||||
? path.map((p) => p.label)
|
||||
: ['OneOS', pageTitle];
|
||||
|
||||
const selectedKey = path[path.length - 1]?.key;
|
||||
const showTabs = Array.isArray(tabs);
|
||||
|
||||
const handleNavigate = useCallback(
|
||||
(item: ShellMenuItem) => {
|
||||
if (onNavigateProp) {
|
||||
onNavigateProp(item);
|
||||
return;
|
||||
}
|
||||
if (item.href && typeof window !== 'undefined') {
|
||||
window.location.href = item.href;
|
||||
}
|
||||
},
|
||||
[onNavigateProp],
|
||||
);
|
||||
|
||||
const toggleOpen = useCallback((key: string) => {
|
||||
setOpenKeys((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]));
|
||||
}, []);
|
||||
|
||||
const renderItems = (items: ShellMenuItem[], level = 0): React.ReactNode =>
|
||||
items.map((item) => {
|
||||
const hasChildren = !!item.children?.length;
|
||||
const opened = openKeys.includes(item.key);
|
||||
const selected =
|
||||
item.key === selectedKey ||
|
||||
(!!item.href && hrefPathname(item.href) === hrefPathname(activeHref));
|
||||
const Icon = iconForLabel(item.label);
|
||||
|
||||
return (
|
||||
<li key={item.key} className={`oneos-shell-menu__item oneos-shell-menu__item--lv${level}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={`oneos-shell-menu__btn${selected ? ' is-active' : ''}${hasChildren ? ' is-parent' : ''}`}
|
||||
title={item.label}
|
||||
aria-expanded={hasChildren ? opened : undefined}
|
||||
onClick={() => {
|
||||
if (hasChildren) toggleOpen(item.key);
|
||||
else handleNavigate(item);
|
||||
}}
|
||||
>
|
||||
<Icon className="oneos-shell-menu__icon" size={16} strokeWidth={1.75} aria-hidden />
|
||||
{!collapsed && <span className="oneos-shell-menu__label">{item.label}</span>}
|
||||
{!collapsed && hasChildren && (
|
||||
<span className="oneos-shell-menu__arrow" aria-hidden>
|
||||
{opened ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{!collapsed && hasChildren && opened && (
|
||||
<ul className="oneos-shell-menu__sub">{renderItems(item.children!, level + 1)}</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`oneos-shell${collapsed ? ' oneos-shell--collapsed' : ''}`}
|
||||
data-oneos-theme={theme}
|
||||
>
|
||||
<aside className="oneos-shell-aside" aria-label="羚牛 OneOS 导航">
|
||||
<div className="oneos-shell-brand">
|
||||
<a className="oneos-shell-brand__link" href={brandHref} title="羚牛OneOS">
|
||||
<span className="oneos-shell-brand__mark" aria-hidden>
|
||||
<svg viewBox="0 0 32 32" width="28" height="28">
|
||||
<defs>
|
||||
<linearGradient id="oneosBrandGrad" x1="0" y1="0" x2="32" y2="32">
|
||||
<stop offset="0%" stopColor="#3FB87C" />
|
||||
<stop offset="100%" stopColor="#2B9260" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="9" fill="url(#oneosBrandGrad)" />
|
||||
<path
|
||||
d="M8 20.5L12.2 9h3.1l4.2 11.5h-3.1l-.8-2.3h-4.7l-.8 2.3H8zm4.2-4.7h3.2L14 11.2h-.1L12.2 15.8zM21 9h2.8v11.5H21V9z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
{!collapsed && <span className="oneos-shell-brand__text">羚牛OneOS</span>}
|
||||
</a>
|
||||
</div>
|
||||
<nav className="oneos-shell-nav">
|
||||
<ul className="oneos-shell-menu">{renderItems(menu)}</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="oneos-shell-main">
|
||||
<div className="oneos-shell-chrome">
|
||||
<header className="oneos-shell-header">
|
||||
<div className="oneos-shell-header__left">
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-icon-btn"
|
||||
aria-label={collapsed ? '展开侧栏' : '收起侧栏'}
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
>
|
||||
{collapsed ? <Menu size={18} /> : <PanelLeftClose size={18} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-icon-btn"
|
||||
aria-label="刷新"
|
||||
onClick={() => (onRefresh ? onRefresh() : window.location.reload())}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
</button>
|
||||
<nav className="oneos-shell-breadcrumb" aria-label="面包屑">
|
||||
<ol>
|
||||
{crumbs.map((c, i) => (
|
||||
<li key={`${c}-${i}`}>
|
||||
{i > 0 && <span className="oneos-shell-breadcrumb__sep">/</span>}
|
||||
<span className={i === crumbs.length - 1 ? 'is-current' : undefined}>{c}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="oneos-shell-header__right">
|
||||
<label className="oneos-shell-search">
|
||||
<Search size={14} aria-hidden />
|
||||
<input type="search" placeholder="搜索" aria-label="搜索" />
|
||||
<kbd>⌘ K</kbd>
|
||||
</label>
|
||||
{onOpenReleaseNotes ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`oneos-shell-release-btn${releaseNotesUnread ? ' has-unread' : ''}`}
|
||||
onClick={onOpenReleaseNotes}
|
||||
aria-label="版本更新"
|
||||
title="版本更新日志"
|
||||
data-annotation-id="shell-release-notes"
|
||||
>
|
||||
<span className="oneos-shell-release-btn__ring" aria-hidden />
|
||||
<span className="oneos-shell-release-btn__icon" aria-hidden>
|
||||
<Sparkles size={16} strokeWidth={2.25} />
|
||||
</span>
|
||||
{releaseNotesUnread ? (
|
||||
<span className="oneos-shell-release-btn__dot" aria-hidden />
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-icon-btn"
|
||||
aria-label={theme === 'dark' ? '切换浅色模式' : '切换暗色模式'}
|
||||
title={theme === 'dark' ? '浅色模式' : '暗色模式'}
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
{theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</button>
|
||||
<button type="button" className="oneos-shell-icon-btn" aria-label="全屏" title="全屏(演示)">
|
||||
<Maximize size={16} />
|
||||
</button>
|
||||
<ShellNoticeCenter
|
||||
notices={shellNotices}
|
||||
unreadCount={shellUnread}
|
||||
onRead={onNoticeRead}
|
||||
onOpen={onNoticeOpen}
|
||||
onHandle={onNoticeHandle}
|
||||
/>
|
||||
<button type="button" className="oneos-shell-avatar" aria-label="超级管理员">
|
||||
<img src={AVATAR_URL} alt="" width={28} height={28} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{showTabs && tabs!.length > 0 && (
|
||||
<div className="oneos-shell-tabs" role="tablist" aria-label="打开的页签">
|
||||
{tabs!.map((tab) => {
|
||||
const active = hrefPathname(tab.href) === hrefPathname(activeHref);
|
||||
return (
|
||||
<div
|
||||
key={tab.href}
|
||||
className={`oneos-shell-tab${active ? ' is-active' : ''}`}
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-tab__label"
|
||||
onClick={() => onTabSelect?.(tab.href)}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
{onTabClose && (
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-tab__close"
|
||||
aria-label={`关闭 ${tab.title}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTabClose(tab.href);
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="oneos-shell-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OneOsAppShell;
|
||||
12
src/common/oneos-app-shell/README.md
Normal file
12
src/common/oneos-app-shell/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 羚牛 OneOS 应用外壳
|
||||
|
||||
> 实现:`src/common/oneos-app-shell/`
|
||||
> 宿主页:`/prototypes/oneos-prototype-demo`(原型演示)
|
||||
|
||||
## 用法
|
||||
|
||||
- **带外壳浏览所有原型**:打开「原型演示」,从左侧菜单点选;内容区 iframe 嵌入。
|
||||
- **单独打开某原型**:访问 `/prototypes/<id>`,无侧栏顶栏。
|
||||
- **浅色 / 暗色**:顶栏月亮/太阳按钮切换;暗色参照原型导航页风格。对 OneOS 目录下原型生效(含直链),**不包括「旧 ONEOS」**。
|
||||
|
||||
菜单数据:`oneos-prototype-nav/nav-menu.json`。
|
||||
364
src/common/oneos-app-shell/ShellNoticeCenter.tsx
Normal file
364
src/common/oneos-app-shell/ShellNoticeCenter.tsx
Normal file
@@ -0,0 +1,364 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Bell, BellOff, X } from 'lucide-react';
|
||||
import type { ShellNoticeItem } from './notice-bridge';
|
||||
|
||||
/** 气泡/标签:x条新通知,请尽快处理(阿拉伯数字) */
|
||||
function formatNewMessageTip(count: number): string {
|
||||
return `${count}条新通知,请尽快处理`;
|
||||
}
|
||||
|
||||
function compareNoticePriority(a: ShellNoticeItem, b: ShellNoticeItem): number {
|
||||
const aUrge = a.type === '催办提醒' ? 0 : 1;
|
||||
const bUrge = b.type === '催办提醒' ? 0 : 1;
|
||||
if (aUrge !== bUrge) return aUrge - bUrge;
|
||||
return b.time.localeCompare(a.time);
|
||||
}
|
||||
|
||||
type AllTab = 'unread' | 'read';
|
||||
|
||||
type ShellNoticeCenterProps = {
|
||||
notices: ShellNoticeItem[];
|
||||
unreadCount: number;
|
||||
onRead: (notice: ShellNoticeItem) => void;
|
||||
onOpen: (notice: ShellNoticeItem) => void;
|
||||
onHandle: (notice: ShellNoticeItem) => void;
|
||||
};
|
||||
|
||||
function NoticeListItem({
|
||||
notice,
|
||||
showHandle,
|
||||
showSummary = false,
|
||||
onSelect,
|
||||
onHandle,
|
||||
}: {
|
||||
notice: ShellNoticeItem;
|
||||
showHandle: boolean;
|
||||
showSummary?: boolean;
|
||||
onSelect: (notice: ShellNoticeItem) => void;
|
||||
onHandle: (notice: ShellNoticeItem) => void;
|
||||
}) {
|
||||
const canHandle = !!(notice.href || notice.taskId);
|
||||
return (
|
||||
<li className={`oneos-shell-notify__item${!notice.read ? ' is-unread' : ''}`}>
|
||||
<button type="button" className="oneos-shell-notify__item-main" onClick={() => onSelect(notice)}>
|
||||
<div className="oneos-shell-notify__meta">
|
||||
<span className="oneos-shell-notify__dot" aria-hidden />
|
||||
<span className="oneos-shell-notify__time">{notice.time}</span>
|
||||
{notice.type === '催办提醒' ? (
|
||||
<span className="oneos-shell-notify__chip oneos-shell-notify__chip--urge">催办提醒</span>
|
||||
) : null}
|
||||
<span className="oneos-shell-notify__chip">{notice.bizTag}</span>
|
||||
</div>
|
||||
<p className="oneos-shell-notify__text">{notice.detail || notice.title}</p>
|
||||
{showSummary && notice.summary ? (
|
||||
<p className="oneos-shell-notify__summary">{notice.summary}</p>
|
||||
) : null}
|
||||
</button>
|
||||
{showHandle && canHandle ? (
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-notify__handle"
|
||||
onClick={() => onHandle(notice)}
|
||||
>
|
||||
去处理
|
||||
</button>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** 顶栏铃铛 + 新消息闪烁提示 + 下拉通知中心(对齐工作台 NoticePanel) */
|
||||
export function ShellNoticeCenter({
|
||||
notices,
|
||||
unreadCount,
|
||||
onRead,
|
||||
onOpen: _onOpen,
|
||||
onHandle,
|
||||
}: ShellNoticeCenterProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [allOpen, setAllOpen] = useState(false);
|
||||
const [allTab, setAllTab] = useState<AllTab>('unread');
|
||||
const [detailNotice, setDetailNotice] = useState<ShellNoticeItem | null>(null);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const allModalRef = useRef<HTMLDivElement>(null);
|
||||
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const shell = wrapRef.current?.closest('.oneos-shell') as HTMLElement | null;
|
||||
setPortalRoot(shell || document.body);
|
||||
}, []);
|
||||
|
||||
const pool = useMemo(
|
||||
() => notices.filter((n) => !n.read).slice().sort(compareNoticePriority),
|
||||
[notices],
|
||||
);
|
||||
const unreadList = pool;
|
||||
const readList = useMemo(
|
||||
() => notices.filter((n) => n.read).slice().sort(compareNoticePriority),
|
||||
[notices],
|
||||
);
|
||||
const allTabList = allTab === 'unread' ? unreadList : readList;
|
||||
|
||||
const closeAll = () => {
|
||||
setOpen(false);
|
||||
setAllOpen(false);
|
||||
setDetailNotice(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open && !detailNotice && !allOpen) return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
if (allOpen) {
|
||||
if (allModalRef.current && !allModalRef.current.contains(target)) {
|
||||
setAllOpen(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!wrapRef.current?.contains(target)) closeAll();
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (detailNotice) setDetailNotice(null);
|
||||
else if (allOpen) setAllOpen(false);
|
||||
else setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDoc);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [open, detailNotice, allOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allOpen) return;
|
||||
const lockEl = portalRoot || document.body;
|
||||
const prev = lockEl.style.overflow;
|
||||
lockEl.style.overflow = 'hidden';
|
||||
return () => {
|
||||
lockEl.style.overflow = prev;
|
||||
};
|
||||
}, [allOpen, portalRoot]);
|
||||
|
||||
const handleSelect = (notice: ShellNoticeItem) => {
|
||||
onRead(notice);
|
||||
setDetailNotice(notice);
|
||||
setOpen(false);
|
||||
setAllOpen(false);
|
||||
};
|
||||
|
||||
const handleAction = (notice: ShellNoticeItem) => {
|
||||
onRead(notice);
|
||||
onHandle(notice);
|
||||
closeAll();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="oneos-shell-notify" ref={wrapRef} data-annotation-id="wb-notice">
|
||||
{unreadCount > 0 && !open && !detailNotice && !allOpen ? (
|
||||
<span className="oneos-shell-notify__callout" aria-live="polite">
|
||||
{formatNewMessageTip(unreadCount)}
|
||||
</span>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={`oneos-shell-icon-btn oneos-shell-icon-btn--notify${unreadCount > 0 ? ' has-unread' : ''}${open || detailNotice || allOpen ? ' is-open' : ''}`}
|
||||
aria-label={unreadCount > 0 ? formatNewMessageTip(unreadCount) : '通知'}
|
||||
aria-expanded={open || !!detailNotice || allOpen}
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => {
|
||||
if (detailNotice || allOpen) {
|
||||
setDetailNotice(null);
|
||||
setAllOpen(false);
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
setOpen((v) => !v);
|
||||
}}
|
||||
>
|
||||
<Bell size={16} />
|
||||
</button>
|
||||
|
||||
{open && !detailNotice && !allOpen ? (
|
||||
<div className="oneos-shell-notify__panel" role="dialog" aria-label="通知中心">
|
||||
<div className="oneos-shell-notify__header">
|
||||
<h3 className="oneos-shell-notify__title">
|
||||
通知中心
|
||||
{unreadCount > 0 ? (
|
||||
<span className="oneos-shell-notify__tag">{formatNewMessageTip(unreadCount)}</span>
|
||||
) : null}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-notify__link"
|
||||
onClick={() => {
|
||||
setAllTab(unreadCount > 0 ? 'unread' : 'read');
|
||||
setOpen(false);
|
||||
setAllOpen(true);
|
||||
}}
|
||||
>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="oneos-shell-notify__body">
|
||||
{pool.length ? (
|
||||
<ul className="oneos-shell-notify__list">
|
||||
{pool.map((notice) => (
|
||||
<NoticeListItem
|
||||
key={notice.id}
|
||||
notice={notice}
|
||||
showHandle
|
||||
onSelect={handleSelect}
|
||||
onHandle={handleAction}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="oneos-shell-notify__empty" role="status">
|
||||
<div className="oneos-shell-notify__empty-visual" aria-hidden="true">
|
||||
<span className="oneos-shell-notify__empty-ring" />
|
||||
<span className="oneos-shell-notify__empty-icon">
|
||||
<BellOff size={22} strokeWidth={1.75} />
|
||||
</span>
|
||||
</div>
|
||||
<p className="oneos-shell-notify__empty-title">暂无新消息</p>
|
||||
<p className="oneos-shell-notify__empty-desc">
|
||||
当前通知均已读完
|
||||
<br />
|
||||
有新催办或业务提醒时会显示在这里
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{allOpen && portalRoot
|
||||
? createPortal(
|
||||
<div className="oneos-shell-notify__all-overlay" role="presentation">
|
||||
<div
|
||||
className="oneos-shell-notify__all-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="全部通知"
|
||||
ref={allModalRef}
|
||||
>
|
||||
<div className="oneos-shell-notify__all-head">
|
||||
<h3 className="oneos-shell-notify__all-title">全部通知</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-notify__all-close"
|
||||
aria-label="关闭"
|
||||
onClick={() => setAllOpen(false)}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="oneos-shell-notify__all-tabs" role="tablist" aria-label="通知分类">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={allTab === 'unread'}
|
||||
className={`oneos-shell-notify__all-tab${allTab === 'unread' ? ' is-active' : ''}`}
|
||||
onClick={() => setAllTab('unread')}
|
||||
>
|
||||
未读
|
||||
<span className="oneos-shell-notify__all-count">{unreadList.length}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={allTab === 'read'}
|
||||
className={`oneos-shell-notify__all-tab${allTab === 'read' ? ' is-active' : ''}`}
|
||||
onClick={() => setAllTab('read')}
|
||||
>
|
||||
已读
|
||||
<span className="oneos-shell-notify__all-count">{readList.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="oneos-shell-notify__all-body" role="tabpanel">
|
||||
{allTabList.length ? (
|
||||
<ul className="oneos-shell-notify__list">
|
||||
{allTabList.map((notice) => (
|
||||
<NoticeListItem
|
||||
key={notice.id}
|
||||
notice={notice}
|
||||
showHandle={allTab === 'unread'}
|
||||
showSummary
|
||||
onSelect={handleSelect}
|
||||
onHandle={handleAction}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="oneos-shell-notify__empty" role="status">
|
||||
<div className="oneos-shell-notify__empty-visual" aria-hidden="true">
|
||||
<span className="oneos-shell-notify__empty-ring" />
|
||||
<span className="oneos-shell-notify__empty-icon">
|
||||
<BellOff size={22} strokeWidth={1.75} />
|
||||
</span>
|
||||
</div>
|
||||
<p className="oneos-shell-notify__empty-title">
|
||||
{allTab === 'unread' ? '暂无未读通知' : '暂无已读通知'}
|
||||
</p>
|
||||
<p className="oneos-shell-notify__empty-desc">
|
||||
{allTab === 'unread'
|
||||
? '当前通知均已读完,有新催办或业务提醒时会显示在这里'
|
||||
: '已读通知会显示在这里'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
portalRoot,
|
||||
)
|
||||
: null}
|
||||
|
||||
{detailNotice ? (
|
||||
<div className="oneos-shell-notify__detail" role="dialog" aria-modal="true" aria-label="通知详情">
|
||||
<div className="oneos-shell-notify__detail-head">
|
||||
<h3 className="oneos-shell-notify__detail-title">
|
||||
{detailNotice.type === '催办提醒' ? (
|
||||
<span className="oneos-shell-notify__chip oneos-shell-notify__chip--urge">催办提醒</span>
|
||||
) : null}
|
||||
<span className="oneos-shell-notify__chip">{detailNotice.bizTag}</span>
|
||||
<span className="oneos-shell-notify__detail-name">{detailNotice.title}</span>
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-notify__detail-close"
|
||||
onClick={() => setDetailNotice(null)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
<div className="oneos-shell-notify__detail-body">
|
||||
<span className="oneos-shell-notify__time">{detailNotice.time}</span>
|
||||
<p className="oneos-shell-notify__detail-text">{detailNotice.detail || detailNotice.title}</p>
|
||||
{detailNotice.summary ? (
|
||||
<p className="oneos-shell-notify__detail-summary">{detailNotice.summary}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="oneos-shell-notify__detail-foot">
|
||||
<button type="button" className="oneos-shell-notify__detail-secondary" onClick={() => setDetailNotice(null)}>
|
||||
知道了
|
||||
</button>
|
||||
{(detailNotice.href || detailNotice.taskId) && (
|
||||
<button
|
||||
type="button"
|
||||
className="oneos-shell-notify__detail-primary"
|
||||
onClick={() => handleAction(detailNotice)}
|
||||
>
|
||||
去处理
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
src/common/oneos-app-shell/index.ts
Normal file
50
src/common/oneos-app-shell/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export { OneOsAppShell, PROTOTYPE_DEMO_HREF } from './OneOsAppShell';
|
||||
export type { OneOsAppShellProps, ShellTab } from './OneOsAppShell';
|
||||
export { ShellNoticeCenter } from './ShellNoticeCenter';
|
||||
export {
|
||||
ONEOS_NOTICE_ACTION,
|
||||
ONEOS_NOTICES_SYNC,
|
||||
isNoticeAction,
|
||||
isNoticesSync,
|
||||
postNoticeAction,
|
||||
postNoticesSync,
|
||||
type NoticeActionPayload,
|
||||
type NoticesSyncPayload,
|
||||
type ShellNoticeItem,
|
||||
} from './notice-bridge';
|
||||
export {
|
||||
ONEOS_SHELL_NAV,
|
||||
isShellNav,
|
||||
requestShellNav,
|
||||
type ShellNavPayload,
|
||||
} from './nav-bridge';
|
||||
export {
|
||||
buildShellMenuFromNav,
|
||||
findMenuPath,
|
||||
hrefPathname,
|
||||
itemKeyToHref,
|
||||
type ShellMenuItem,
|
||||
} from './nav-from-prototypes';
|
||||
export {
|
||||
ONEOS_RELEASE_DEMO_ACTION,
|
||||
RELEASE_SEEN_STORAGE_KEY,
|
||||
SHELL_CURRENT_RELEASE_VERSION,
|
||||
clearAllReleaseSeen,
|
||||
hasUnseenRelease,
|
||||
isReleaseDemoAction,
|
||||
postReleaseDemoAction,
|
||||
postReleaseDemoOpen,
|
||||
postReleaseDemoReset,
|
||||
type ReleaseDemoAction,
|
||||
type ReleaseDemoActionPayload,
|
||||
} from './release-demo-bridge';
|
||||
export {
|
||||
bootstrapOneOsTheme,
|
||||
broadcastOneOsTheme,
|
||||
isLegacyOneOsPrototype,
|
||||
LEGACY_ONEOS_PROTO_IDS,
|
||||
readStoredOneOsTheme,
|
||||
setOneOsTheme,
|
||||
toggleOneOsTheme,
|
||||
type OneOsTheme,
|
||||
} from './theme';
|
||||
29
src/common/oneos-app-shell/nav-bridge.ts
Normal file
29
src/common/oneos-app-shell/nav-bridge.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/** iframe 内页面 ↔ 外壳侧栏/页签导航 postMessage 协议 */
|
||||
|
||||
export const ONEOS_SHELL_NAV = 'ONEOS_SHELL_NAV';
|
||||
|
||||
export type ShellNavPayload = {
|
||||
type: typeof ONEOS_SHELL_NAV;
|
||||
href: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function isShellNav(data: unknown): data is ShellNavPayload {
|
||||
return (
|
||||
!!data &&
|
||||
typeof data === 'object' &&
|
||||
(data as ShellNavPayload).type === ONEOS_SHELL_NAV &&
|
||||
typeof (data as ShellNavPayload).href === 'string' &&
|
||||
(data as ShellNavPayload).href.startsWith('/prototypes/')
|
||||
);
|
||||
}
|
||||
|
||||
/** 在 iframe 内跳转时优先通知外壳;直链打开则本页跳转 */
|
||||
export function requestShellNav(href: string, title?: string): boolean {
|
||||
if (typeof window === 'undefined' || !href.startsWith('/prototypes/')) return false;
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: ONEOS_SHELL_NAV, href, title } satisfies ShellNavPayload, '*');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
104
src/common/oneos-app-shell/nav-from-prototypes.ts
Normal file
104
src/common/oneos-app-shell/nav-from-prototypes.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 将 oneos-prototype-nav/nav-menu.json 转为羚牛 OneOS 侧栏菜单树
|
||||
*/
|
||||
|
||||
export type NavMenuNode = {
|
||||
id: string;
|
||||
kind: 'folder' | 'item';
|
||||
title: string;
|
||||
itemKey?: string;
|
||||
children?: NavMenuNode[];
|
||||
};
|
||||
|
||||
export type ShellMenuItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
href?: string;
|
||||
children?: ShellMenuItem[];
|
||||
};
|
||||
|
||||
type NavMenuFile = {
|
||||
prototypes?: NavMenuNode[];
|
||||
};
|
||||
|
||||
export function itemKeyToHref(itemKey?: string): string | undefined {
|
||||
if (!itemKey) return undefined;
|
||||
if (itemKey.startsWith('/prototypes/')) return itemKey;
|
||||
if (itemKey.startsWith('prototypes/')) return `/${itemKey}`;
|
||||
return `/prototypes/${itemKey}`;
|
||||
}
|
||||
|
||||
function convertNode(node: NavMenuNode): ShellMenuItem {
|
||||
if (node.kind === 'item') {
|
||||
return {
|
||||
key: node.id || node.itemKey || node.title,
|
||||
label: node.title,
|
||||
href: itemKeyToHref(node.itemKey),
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: node.id || node.title,
|
||||
label: node.title,
|
||||
children: (node.children || []).map(convertNode),
|
||||
};
|
||||
}
|
||||
|
||||
/** 取「OneOS」根目录下的子树作为侧栏;若无则用整棵 prototypes */
|
||||
export function buildShellMenuFromNav(
|
||||
nav: NavMenuFile,
|
||||
options?: { excludeHrefs?: string[] },
|
||||
): ShellMenuItem[] {
|
||||
const roots = nav.prototypes || [];
|
||||
const oneos = roots.find((n) => n.kind === 'folder' && /oneos/i.test(n.title));
|
||||
const source = oneos?.children?.length ? oneos.children : roots;
|
||||
const exclude = new Set(
|
||||
(options?.excludeHrefs ?? ['/prototypes/oneos-prototype-demo']).map((h) => h.replace(/\/$/, '')),
|
||||
);
|
||||
|
||||
function filterTree(items: ShellMenuItem[]): ShellMenuItem[] {
|
||||
return items
|
||||
.map((it) => {
|
||||
if (it.href && exclude.has(it.href.replace(/\/$/, ''))) return null;
|
||||
if (it.children?.length) {
|
||||
const children = filterTree(it.children);
|
||||
if (!it.href && children.length === 0) return null;
|
||||
return { ...it, children };
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.filter(Boolean) as ShellMenuItem[];
|
||||
}
|
||||
|
||||
return filterTree(source.map(convertNode));
|
||||
}
|
||||
|
||||
/** 去掉 query/hash 与尾部斜杠,供菜单高亮与路径匹配 */
|
||||
export function hrefPathname(href: string): string {
|
||||
return href.split(/[?#]/)[0]!.replace(/\/$/, '') || href;
|
||||
}
|
||||
|
||||
export function findMenuPath(
|
||||
items: ShellMenuItem[],
|
||||
activeHref: string,
|
||||
trail: ShellMenuItem[] = [],
|
||||
): ShellMenuItem[] | null {
|
||||
const activePath = hrefPathname(activeHref);
|
||||
for (const item of items) {
|
||||
const next = [...trail, item];
|
||||
if (item.href) {
|
||||
const itemPath = hrefPathname(item.href);
|
||||
if (activePath === itemPath || activePath.endsWith(itemPath)) {
|
||||
return next;
|
||||
}
|
||||
}
|
||||
if (item.children?.length) {
|
||||
const hit = findMenuPath(item.children, activeHref, next);
|
||||
if (hit) return hit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function collectOpenKeys(path: ShellMenuItem[]): string[] {
|
||||
return path.filter((n) => n.children?.length).map((n) => n.key);
|
||||
}
|
||||
60
src/common/oneos-app-shell/notice-bridge.ts
Normal file
60
src/common/oneos-app-shell/notice-bridge.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/** 工作台 iframe ↔ 外壳顶栏通知中心 postMessage 协议 */
|
||||
|
||||
export const ONEOS_NOTICES_SYNC = 'ONEOS_NOTICES_SYNC';
|
||||
export const ONEOS_NOTICE_ACTION = 'ONEOS_NOTICE_ACTION';
|
||||
|
||||
export type ShellNoticeItem = {
|
||||
id: string;
|
||||
type: string;
|
||||
bizTag: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
detail: string;
|
||||
time: string;
|
||||
read: boolean;
|
||||
href?: string;
|
||||
taskId?: string;
|
||||
};
|
||||
|
||||
export type NoticesSyncPayload = {
|
||||
type: typeof ONEOS_NOTICES_SYNC;
|
||||
unreadCount: number;
|
||||
notices: ShellNoticeItem[];
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type NoticeActionPayload = {
|
||||
type: typeof ONEOS_NOTICE_ACTION;
|
||||
action: 'read' | 'handle' | 'open';
|
||||
noticeId: string;
|
||||
};
|
||||
|
||||
export function isNoticesSync(data: unknown): data is NoticesSyncPayload {
|
||||
return (
|
||||
!!data &&
|
||||
typeof data === 'object' &&
|
||||
(data as NoticesSyncPayload).type === ONEOS_NOTICES_SYNC &&
|
||||
Array.isArray((data as NoticesSyncPayload).notices)
|
||||
);
|
||||
}
|
||||
|
||||
export function isNoticeAction(data: unknown): data is NoticeActionPayload {
|
||||
return (
|
||||
!!data &&
|
||||
typeof data === 'object' &&
|
||||
(data as NoticeActionPayload).type === ONEOS_NOTICE_ACTION &&
|
||||
typeof (data as NoticeActionPayload).noticeId === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export function postNoticesSync(target: Window, payload: Omit<NoticesSyncPayload, 'type'>) {
|
||||
target.postMessage({ type: ONEOS_NOTICES_SYNC, ...payload }, '*');
|
||||
}
|
||||
|
||||
export function postNoticeAction(
|
||||
target: Window,
|
||||
action: NoticeActionPayload['action'],
|
||||
noticeId: string,
|
||||
) {
|
||||
target.postMessage({ type: ONEOS_NOTICE_ACTION, action, noticeId }, '*');
|
||||
}
|
||||
1564
src/common/oneos-app-shell/oneos-app-shell.css
Normal file
1564
src/common/oneos-app-shell/oneos-app-shell.css
Normal file
File diff suppressed because it is too large
Load Diff
163
src/common/oneos-app-shell/oneos-theme-content.css
Normal file
163
src/common/oneos-app-shell/oneos-theme-content.css
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* OneOS 内容区暗色 token(参照原型导航 / VoltAgent 深色画布)
|
||||
* 由 html[data-oneos-theme="dark"] 驱动;浅色保持各页原有 :root 定义。
|
||||
*/
|
||||
|
||||
html[data-oneos-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
|
||||
--ln-primary: #00d992;
|
||||
--ln-primary-hover: #2fd6a1;
|
||||
--ln-primary-focus: #10b981;
|
||||
--ln-primary-active: #10b981;
|
||||
--ln-primary-soft: rgba(0, 217, 146, 0.12);
|
||||
--ln-primary-soft-strong: rgba(0, 217, 146, 0.2);
|
||||
--ln-ink: #f2f2f2;
|
||||
--ln-body: #bdbdbd;
|
||||
--ln-body-strong: #f2f2f2;
|
||||
--ln-muted: #8b949e;
|
||||
--ln-muted-soft: #6b7280;
|
||||
--ln-muted-80: #d1d5db;
|
||||
--ln-divider-soft: #1a1a1a;
|
||||
--ln-hairline: #3d3a39;
|
||||
--ln-hairline-strong: #565656;
|
||||
--ln-hairline-tertiary: #3d3a39;
|
||||
--ln-canvas: #101010;
|
||||
--ln-canvas-parchment: #101010;
|
||||
--ln-surface-pearl: #1a1a1a;
|
||||
--ln-surface-card: #1a1a1a;
|
||||
--ln-surface-strong: #222222;
|
||||
--ln-on-primary: #101010;
|
||||
--ln-success: #00d992;
|
||||
--ln-warning: #f59e0b;
|
||||
--ln-error: #f87171;
|
||||
--ln-brand-secure: #2fd6a1;
|
||||
--ln-shadow-soft: none;
|
||||
--ln-shadow-hover: none;
|
||||
--ln-shadow-float: none;
|
||||
--ln-shadow-modal: 0 20px 40px rgba(0, 0, 0, 0.45);
|
||||
--ln-link: #00d992;
|
||||
--ln-canvas-soft: #1a1a1a;
|
||||
|
||||
--vm-primary: var(--ln-primary);
|
||||
--vm-primary-dark: var(--ln-primary-focus);
|
||||
--vm-primary-soft: var(--ln-primary-soft);
|
||||
--vm-bg: var(--ln-canvas-parchment);
|
||||
--vm-surface: var(--ln-surface-card);
|
||||
--vm-border: var(--ln-hairline);
|
||||
--vm-text: var(--ln-ink);
|
||||
--vm-muted: var(--ln-muted);
|
||||
--vm-danger: var(--ln-error);
|
||||
--vm-warning: var(--ln-warning);
|
||||
--vm-link: var(--ln-link);
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'],
|
||||
html[data-oneos-theme='dark'] body {
|
||||
background-color: #101010 !important;
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] #root {
|
||||
background-color: #101010;
|
||||
color: #f2f2f2;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* 常见页面根容器 */
|
||||
html[data-oneos-theme='dark'] .vm-page,
|
||||
html[data-oneos-theme='dark'] .wb-page,
|
||||
html[data-oneos-theme='dark'] .page,
|
||||
html[data-oneos-theme='dark'] .ant-layout,
|
||||
html[data-oneos-theme='dark'] .ant-layout-content {
|
||||
background: #101010 !important;
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .ant-card,
|
||||
html[data-oneos-theme='dark'] .ant-table,
|
||||
html[data-oneos-theme='dark'] .ant-table-container,
|
||||
html[data-oneos-theme='dark'] .ant-table-thead > tr > th,
|
||||
html[data-oneos-theme='dark'] .ant-modal-content,
|
||||
html[data-oneos-theme='dark'] .ant-drawer-content,
|
||||
html[data-oneos-theme='dark'] .ant-input,
|
||||
html[data-oneos-theme='dark'] .ant-input-affix-wrapper,
|
||||
html[data-oneos-theme='dark'] .ant-select-selector,
|
||||
html[data-oneos-theme='dark'] .ant-picker {
|
||||
background: #1a1a1a !important;
|
||||
border-color: #3d3a39 !important;
|
||||
color: #f2f2f2 !important;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .ant-table-thead > tr > th {
|
||||
background: #161616 !important;
|
||||
color: #bdbdbd !important;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .ant-table-tbody > tr > td {
|
||||
border-color: #3d3a39 !important;
|
||||
color: #f2f2f2 !important;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .ant-table-tbody > tr:hover > td {
|
||||
background: #222222 !important;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .ant-btn-default {
|
||||
background: #1a1a1a !important;
|
||||
border-color: #3d3a39 !important;
|
||||
color: #f2f2f2 !important;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .ant-btn-primary {
|
||||
background: #00d992 !important;
|
||||
border-color: #00d992 !important;
|
||||
color: #101010 !important;
|
||||
}
|
||||
|
||||
/* 工作台 / 通用卡片:覆盖硬编码浅色渐变与表头 */
|
||||
html[data-oneos-theme='dark'] .wb-hero--welcome,
|
||||
html[data-oneos-theme='dark'] .wb-hero--personal,
|
||||
html[data-oneos-theme='dark'] .wb-panel,
|
||||
html[data-oneos-theme='dark'] .wb-insights,
|
||||
html[data-oneos-theme='dark'] .wb-section--flow,
|
||||
html[data-oneos-theme='dark'] .wb-section--approval,
|
||||
html[data-oneos-theme='dark'] .wb-kpi-tile,
|
||||
html[data-oneos-theme='dark'] .wb-notice-card {
|
||||
background: #1a1a1a !important;
|
||||
box-shadow: none;
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .wb-kpi-chip {
|
||||
background: #222222 !important;
|
||||
box-shadow: none;
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .wb-panel-header,
|
||||
html[data-oneos-theme='dark'] .wb-section-head,
|
||||
html[data-oneos-theme='dark'] .wb-todo-table thead th,
|
||||
html[data-oneos-theme='dark'] .wb-insight-table th,
|
||||
html[data-oneos-theme='dark'] .wb-insight-metric {
|
||||
background: #161616 !important;
|
||||
color: #bdbdbd;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .wb-page--layout-personal {
|
||||
background: #101010 !important;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .wb-todo-table tbody tr:hover td,
|
||||
html[data-oneos-theme='dark'] .wb-kpi-chip:hover {
|
||||
background: #222222 !important;
|
||||
}
|
||||
|
||||
html[data-oneos-theme='dark'] .wb-tabs,
|
||||
html[data-oneos-theme='dark'] .wb-drawer,
|
||||
html[data-oneos-theme='dark'] .wb-modal,
|
||||
html[data-oneos-theme='dark'] .wb-overlay-panel {
|
||||
background: #1a1a1a;
|
||||
color: #f2f2f2;
|
||||
border-color: #3d3a39;
|
||||
}
|
||||
64
src/common/oneos-app-shell/release-demo-bridge.ts
Normal file
64
src/common/oneos-app-shell/release-demo-bridge.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/** 原型演示外壳 ↔ 工作台 iframe:版本更新日志 */
|
||||
|
||||
export const ONEOS_RELEASE_DEMO_ACTION = 'ONEOS_RELEASE_DEMO_ACTION';
|
||||
|
||||
export type ReleaseDemoAction = 'open' | 'reset';
|
||||
|
||||
export type ReleaseDemoActionPayload = {
|
||||
type: typeof ONEOS_RELEASE_DEMO_ACTION;
|
||||
action: ReleaseDemoAction;
|
||||
};
|
||||
|
||||
export function isReleaseDemoAction(data: unknown): data is ReleaseDemoActionPayload {
|
||||
if (!data || typeof data !== 'object') return false;
|
||||
const payload = data as ReleaseDemoActionPayload;
|
||||
return (
|
||||
payload.type === ONEOS_RELEASE_DEMO_ACTION &&
|
||||
(payload.action === 'open' || payload.action === 'reset')
|
||||
);
|
||||
}
|
||||
|
||||
export function postReleaseDemoAction(target: Window, action: ReleaseDemoAction) {
|
||||
target.postMessage(
|
||||
{ type: ONEOS_RELEASE_DEMO_ACTION, action } satisfies ReleaseDemoActionPayload,
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 postReleaseDemoAction(target, 'open') */
|
||||
export function postReleaseDemoReset(target: Window) {
|
||||
postReleaseDemoAction(target, 'reset');
|
||||
}
|
||||
|
||||
export function postReleaseDemoOpen(target: Window) {
|
||||
postReleaseDemoAction(target, 'open');
|
||||
}
|
||||
|
||||
/** 与工作台 `utils/release-seen.ts` 同源 key */
|
||||
export const RELEASE_SEEN_STORAGE_KEY = 'oneos-wb-release-seen-v1';
|
||||
|
||||
/** 当前发布版本(外壳「未读」点用;与 workbench data/release-notes 保持一致) */
|
||||
export const SHELL_CURRENT_RELEASE_VERSION = '1.1.5';
|
||||
|
||||
export function clearAllReleaseSeen(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
window.localStorage.removeItem(RELEASE_SEEN_STORAGE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function hasUnseenRelease(operatorName?: string): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(RELEASE_SEEN_STORAGE_KEY);
|
||||
if (!raw) return true;
|
||||
const map = JSON.parse(raw) as Record<string, string>;
|
||||
if (!map || typeof map !== 'object') return true;
|
||||
if (operatorName) return map[operatorName] !== SHELL_CURRENT_RELEASE_VERSION;
|
||||
return !Object.values(map).includes(SHELL_CURRENT_RELEASE_VERSION);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
122
src/common/oneos-app-shell/theme.ts
Normal file
122
src/common/oneos-app-shell/theme.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* OneOS 浅色 / 暗色主题(localStorage + 跨 iframe postMessage)
|
||||
* 暗色参照原型导航页(VoltAgent 深色画布)。
|
||||
* 不对「旧 ONEOS」目录下原型生效。
|
||||
*/
|
||||
|
||||
import './oneos-theme-content.css';
|
||||
|
||||
export type OneOsTheme = 'light' | 'dark';
|
||||
|
||||
export const ONEOS_THEME_STORAGE_KEY = 'oneos-ui-theme';
|
||||
export const ONEOS_THEME_ATTR = 'data-oneos-theme';
|
||||
export const ONEOS_THEME_MESSAGE = 'ONEOS_THEME_CHANGE';
|
||||
|
||||
/** sidebar「旧ONEOS」目录下的原型,不接入主题切换 */
|
||||
export const LEGACY_ONEOS_PROTO_IDS = new Set([
|
||||
'oneos-web-business',
|
||||
'oneos-web-data-analysis',
|
||||
'oneos-web-finance',
|
||||
'oneos-web-help-center',
|
||||
'oneos-web-lease-contract',
|
||||
'oneos-web-ledger-data',
|
||||
'oneos-web-ops',
|
||||
'oneos-web-procurement',
|
||||
]);
|
||||
|
||||
export function isLegacyOneOsPrototype(id?: string | null): boolean {
|
||||
if (!id) return false;
|
||||
return LEGACY_ONEOS_PROTO_IDS.has(id);
|
||||
}
|
||||
|
||||
export function readStoredOneOsTheme(): OneOsTheme {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
try {
|
||||
const v = window.localStorage.getItem(ONEOS_THEME_STORAGE_KEY);
|
||||
if (v === 'dark' || v === 'light') return v;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
export function applyOneOsTheme(theme: OneOsTheme, root: HTMLElement = document.documentElement) {
|
||||
root.setAttribute(ONEOS_THEME_ATTR, theme);
|
||||
if (root === document.documentElement) {
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
}
|
||||
}
|
||||
|
||||
export function persistOneOsTheme(theme: OneOsTheme) {
|
||||
try {
|
||||
window.localStorage.setItem(ONEOS_THEME_STORAGE_KEY, theme);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function broadcastOneOsTheme(theme: OneOsTheme) {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new CustomEvent(ONEOS_THEME_MESSAGE, { detail: { theme } }));
|
||||
try {
|
||||
window.parent?.postMessage({ type: ONEOS_THEME_MESSAGE, theme }, '*');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const frames = document.querySelectorAll('iframe');
|
||||
frames.forEach((frame) => {
|
||||
try {
|
||||
frame.contentWindow?.postMessage({ type: ONEOS_THEME_MESSAGE, theme }, '*');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function setOneOsTheme(theme: OneOsTheme) {
|
||||
applyOneOsTheme(theme);
|
||||
persistOneOsTheme(theme);
|
||||
broadcastOneOsTheme(theme);
|
||||
}
|
||||
|
||||
export function toggleOneOsTheme(current?: OneOsTheme): OneOsTheme {
|
||||
const next: OneOsTheme = (current ?? readStoredOneOsTheme()) === 'dark' ? 'light' : 'dark';
|
||||
setOneOsTheme(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
type BootstrapOptions = {
|
||||
prototypeId?: string;
|
||||
/** 强制跳过(如旧 ONEOS) */
|
||||
skip?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 在原型预览入口调用:同步 localStorage / URL / 父页 postMessage。
|
||||
*/
|
||||
export function bootstrapOneOsTheme(options: BootstrapOptions = {}) {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (options.skip || isLegacyOneOsPrototype(options.prototypeId)) return;
|
||||
|
||||
const fromQuery = new URLSearchParams(window.location.search).get('oneosTheme');
|
||||
const initial: OneOsTheme =
|
||||
fromQuery === 'dark' || fromQuery === 'light' ? fromQuery : readStoredOneOsTheme();
|
||||
|
||||
applyOneOsTheme(initial);
|
||||
persistOneOsTheme(initial);
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
if (!data || data.type !== ONEOS_THEME_MESSAGE) return;
|
||||
if (data.theme !== 'dark' && data.theme !== 'light') return;
|
||||
applyOneOsTheme(data.theme);
|
||||
persistOneOsTheme(data.theme);
|
||||
});
|
||||
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key !== ONEOS_THEME_STORAGE_KEY) return;
|
||||
if (event.newValue === 'dark' || event.newValue === 'light') {
|
||||
applyOneOsTheme(event.newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
567
src/common/selfOperatedLogisticsBridge.js
Normal file
567
src/common/selfOperatedLogisticsBridge.js
Normal file
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* 自营物流闭环桥:自营合同 ↔ 轻量调度任务 ↔ 车辆运营态 ↔ 物流业务明细自动生成
|
||||
* 原型本地种子 + localStorage,未接真实 API。
|
||||
* 规则见:src/resources/self-operated-logistics/assumptions.md
|
||||
*/
|
||||
|
||||
export const SO_CONTRACT_STORAGE_KEY = 'oneos.self-operated.contracts.v1';
|
||||
export const SO_DISPATCH_STORAGE_KEY = 'oneos.self-operated.dispatch-tasks.v1';
|
||||
export const SO_LEDGER_AUTO_STORAGE_KEY = 'oneos.self-operated.ledger-auto.v1';
|
||||
export const SO_VEHICLE_OPS_STORAGE_KEY = 'oneos.self-operated.vehicle-ops.v1';
|
||||
|
||||
export const CONTRACT_STATUS = {
|
||||
draft: { key: 'draft', label: '草稿', color: 'default' },
|
||||
active: { key: 'active', label: '生效', color: 'processing' },
|
||||
performing: { key: 'performing', label: '履约中', color: 'success' },
|
||||
ended: { key: 'ended', label: '已结束', color: 'default' },
|
||||
terminated: { key: 'terminated', label: '已终止', color: 'error' },
|
||||
};
|
||||
|
||||
export const DISPATCH_STATUS = {
|
||||
pending_assign: { key: 'pending_assign', label: '待派', color: 'default' },
|
||||
assigned: { key: 'assigned', label: '已派', color: 'processing' },
|
||||
in_progress: { key: 'in_progress', label: '执行中', color: 'warning' },
|
||||
completed: { key: 'completed', label: '已办结', color: 'success' },
|
||||
cancelled: { key: 'cancelled', label: '已取消', color: 'error' },
|
||||
};
|
||||
|
||||
/** inventory | self_operated */
|
||||
export const VEHICLE_OPS = {
|
||||
inventory: 'inventory',
|
||||
self_operated: 'self_operated',
|
||||
};
|
||||
|
||||
export const SEED_VEHICLES = [
|
||||
{ plateNo: '粤B·H2001', brandModel: '佛山飞驰·35T', opsStatus: VEHICLE_OPS.inventory },
|
||||
{ plateNo: '粤B·H2002', brandModel: '佛山飞驰·35T', opsStatus: VEHICLE_OPS.self_operated },
|
||||
{ plateNo: '粤B·H2003', brandModel: '未势能源·49T', opsStatus: VEHICLE_OPS.inventory },
|
||||
{ plateNo: '粤B·H2008', brandModel: '未势能源·49T', opsStatus: VEHICLE_OPS.inventory },
|
||||
];
|
||||
|
||||
export const SEED_DRIVERS = [
|
||||
{ name: '陈志强', phone: '13800138001' },
|
||||
{ name: '李伟', phone: '13800138002' },
|
||||
{ name: '王海涛', phone: '13800138003' },
|
||||
];
|
||||
|
||||
function nowStamp() {
|
||||
const d = new Date();
|
||||
const p = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function todayDate() {
|
||||
return nowStamp().slice(0, 10);
|
||||
}
|
||||
|
||||
function readJson(key, fallback) {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed == null ? fallback : parsed;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(key, value) {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('oneos-self-operated-bridge', { detail: { key } }));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function roundMoney(value) {
|
||||
return Math.round(Number(value || 0) * 100) / 100;
|
||||
}
|
||||
|
||||
function computeAmount(unitPrice, quantity) {
|
||||
return roundMoney(Number(unitPrice || 0) * Number(quantity || 0));
|
||||
}
|
||||
|
||||
function computeTotalCost(row) {
|
||||
return roundMoney(
|
||||
Number(row.hydrogenFee || 0)
|
||||
+ Number(row.manualReimbursement || 0)
|
||||
+ Number(row.etcFee || 0)
|
||||
+ Number(row.electricityFee || 0)
|
||||
+ Number(row.salary || 0)
|
||||
+ Number(row.vehicleFee || 0),
|
||||
);
|
||||
}
|
||||
|
||||
function brandModelOf(plateNo) {
|
||||
const hit = SEED_VEHICLES.find((v) => v.plateNo === plateNo);
|
||||
return hit ? hit.brandModel : '';
|
||||
}
|
||||
|
||||
// —— 车辆运营态 ——
|
||||
|
||||
export function loadVehicleOpsMap() {
|
||||
const stored = readJson(SO_VEHICLE_OPS_STORAGE_KEY, null);
|
||||
if (stored && typeof stored === 'object') return stored;
|
||||
const map = {};
|
||||
SEED_VEHICLES.forEach((v) => {
|
||||
map[v.plateNo] = v.opsStatus;
|
||||
});
|
||||
writeJson(SO_VEHICLE_OPS_STORAGE_KEY, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
export function getVehicleOpsStatus(plateNo) {
|
||||
const map = loadVehicleOpsMap();
|
||||
return map[plateNo] || VEHICLE_OPS.inventory;
|
||||
}
|
||||
|
||||
export function setVehicleOpsStatus(plateNo, status) {
|
||||
const map = loadVehicleOpsMap();
|
||||
map[plateNo] = status;
|
||||
writeJson(SO_VEHICLE_OPS_STORAGE_KEY, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟运维交车:库存 → 自营
|
||||
*/
|
||||
export function simulateHandover(plateNo) {
|
||||
const plate = String(plateNo || '').trim();
|
||||
if (!plate) return { ok: false, message: '请选择车牌' };
|
||||
const current = getVehicleOpsStatus(plate);
|
||||
if (current === VEHICLE_OPS.self_operated) {
|
||||
return { ok: true, message: `${plate} 已在自营运营态`, already: true };
|
||||
}
|
||||
setVehicleOpsStatus(plate, VEHICLE_OPS.self_operated);
|
||||
return { ok: true, message: `已模拟交车:${plate} → 自营运营态` };
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟运维还车:自营 → 库存;存在未办结任务时拦截
|
||||
*/
|
||||
export function simulateReturn(plateNo) {
|
||||
const plate = String(plateNo || '').trim();
|
||||
if (!plate) return { ok: false, message: '请选择车牌' };
|
||||
const open = loadDispatchTasks().filter(
|
||||
(t) => t.plateNo === plate && !['completed', 'cancelled'].includes(t.status),
|
||||
);
|
||||
if (open.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `${plate} 仍有 ${open.length} 条未办结调度任务,无法还车`,
|
||||
};
|
||||
}
|
||||
setVehicleOpsStatus(plate, VEHICLE_OPS.inventory);
|
||||
return { ok: true, message: `已模拟还车:${plate} → 库存` };
|
||||
}
|
||||
|
||||
// —— 合同 ——
|
||||
|
||||
function seedContracts() {
|
||||
return [
|
||||
{
|
||||
id: 'soc-seed-001',
|
||||
contractCode: 'ZY-2026-0001',
|
||||
customerName: '深圳城配物流有限公司',
|
||||
businessName: '深圳南山短驳项目',
|
||||
vehicleType: '35T 氢能重卡',
|
||||
vehicleCount: 2,
|
||||
serviceStart: '2026-07-01',
|
||||
serviceEnd: '2026-12-31',
|
||||
handoverDate: '2026-07-05',
|
||||
handoverPlace: '深圳坪山运维基地',
|
||||
pricingSummary: '线路计价 · 元/趟',
|
||||
unitPriceDefault: 1800,
|
||||
status: 'active',
|
||||
attachmentName: '自营服务合同-南山短驳.pdf',
|
||||
createdAt: '2026-06-28 10:00',
|
||||
activatedAt: '2026-06-28 15:20',
|
||||
remark: '种子合同:已生效,可创建调度任务',
|
||||
},
|
||||
{
|
||||
id: 'soc-seed-002',
|
||||
contractCode: 'ZY-2026-0002',
|
||||
customerName: '广州港务运输集团',
|
||||
businessName: '南沙港区接驳',
|
||||
vehicleType: '49T 氢能重卡',
|
||||
vehicleCount: 1,
|
||||
serviceStart: '2026-08-01',
|
||||
serviceEnd: '2027-01-31',
|
||||
handoverDate: '2026-08-03',
|
||||
handoverPlace: '广州南沙停车场',
|
||||
pricingSummary: '线路计价 · 元/趟',
|
||||
unitPriceDefault: 2200,
|
||||
status: 'draft',
|
||||
attachmentName: '',
|
||||
createdAt: '2026-07-10 09:30',
|
||||
activatedAt: '',
|
||||
remark: '草稿:需业务确认生效',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function loadContracts() {
|
||||
const stored = readJson(SO_CONTRACT_STORAGE_KEY, null);
|
||||
if (Array.isArray(stored) && stored.length > 0) return stored;
|
||||
const seed = seedContracts();
|
||||
writeJson(SO_CONTRACT_STORAGE_KEY, seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
export function saveContracts(list) {
|
||||
writeJson(SO_CONTRACT_STORAGE_KEY, list || []);
|
||||
}
|
||||
|
||||
export function getContractById(id) {
|
||||
return loadContracts().find((c) => c.id === id) || null;
|
||||
}
|
||||
|
||||
export function getActiveContracts() {
|
||||
return loadContracts().filter((c) => c.status === 'active' || c.status === 'performing');
|
||||
}
|
||||
|
||||
export function createContract(payload) {
|
||||
const list = loadContracts();
|
||||
const n = list.length + 1;
|
||||
const row = {
|
||||
id: `soc-${Date.now()}`,
|
||||
contractCode: payload.contractCode || `ZY-2026-${String(n).padStart(4, '0')}`,
|
||||
customerName: String(payload.customerName || '').trim(),
|
||||
businessName: String(payload.businessName || '').trim(),
|
||||
vehicleType: String(payload.vehicleType || '').trim(),
|
||||
vehicleCount: Number(payload.vehicleCount) || 1,
|
||||
serviceStart: payload.serviceStart || '',
|
||||
serviceEnd: payload.serviceEnd || '',
|
||||
handoverDate: payload.handoverDate || '',
|
||||
handoverPlace: payload.handoverPlace || '',
|
||||
pricingSummary: payload.pricingSummary || '线路计价',
|
||||
unitPriceDefault: Number(payload.unitPriceDefault) || 0,
|
||||
status: 'draft',
|
||||
attachmentName: payload.attachmentName || '',
|
||||
createdAt: nowStamp(),
|
||||
activatedAt: '',
|
||||
remark: payload.remark || '',
|
||||
};
|
||||
list.unshift(row);
|
||||
saveContracts(list);
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿 → 生效;进入可派车;标记可发起交车(原型 toast/handoverHint)
|
||||
*/
|
||||
export function activateContract(contractId) {
|
||||
const list = loadContracts();
|
||||
const idx = list.findIndex((c) => c.id === contractId);
|
||||
if (idx < 0) return { ok: false, message: '合同不存在' };
|
||||
const c = list[idx];
|
||||
if (c.status !== 'draft') {
|
||||
return { ok: false, message: `当前状态「${CONTRACT_STATUS[c.status]?.label || c.status}」不可生效` };
|
||||
}
|
||||
list[idx] = {
|
||||
...c,
|
||||
status: 'active',
|
||||
activatedAt: nowStamp(),
|
||||
};
|
||||
saveContracts(list);
|
||||
return {
|
||||
ok: true,
|
||||
contract: list[idx],
|
||||
message: `合同 ${c.contractCode} 已生效,可创建调度任务;交车请在调度侧「模拟交车」或运维交车办理`,
|
||||
handoverHint: {
|
||||
contractCode: c.contractCode,
|
||||
handoverDate: c.handoverDate,
|
||||
handoverPlace: c.handoverPlace,
|
||||
vehicleCount: c.vehicleCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function endContract(contractId) {
|
||||
const list = loadContracts();
|
||||
const idx = list.findIndex((c) => c.id === contractId);
|
||||
if (idx < 0) return { ok: false, message: '合同不存在' };
|
||||
const open = loadDispatchTasks().filter(
|
||||
(t) => t.contractId === contractId && !['completed', 'cancelled'].includes(t.status),
|
||||
);
|
||||
if (open.length > 0) {
|
||||
return { ok: false, message: `仍有 ${open.length} 条未办结调度任务,无法结束合同` };
|
||||
}
|
||||
list[idx] = { ...list[idx], status: 'ended' };
|
||||
saveContracts(list);
|
||||
return { ok: true, message: `合同 ${list[idx].contractCode} 已结束` };
|
||||
}
|
||||
|
||||
// —— 调度任务 ——
|
||||
|
||||
function seedDispatchTasks() {
|
||||
return [
|
||||
{
|
||||
id: 'sdt-seed-001',
|
||||
taskCode: 'DD-2026-0001',
|
||||
contractId: 'soc-seed-001',
|
||||
contractCode: 'ZY-2026-0001',
|
||||
businessName: '深圳南山短驳项目',
|
||||
customerName: '深圳城配物流有限公司',
|
||||
planDate: '2026-07-18',
|
||||
routeDesc: '坪山基地 → 南山仓 A',
|
||||
multiTrip: '否',
|
||||
routePricing: '线路计价',
|
||||
unitPrice: 1800,
|
||||
quantity: 1,
|
||||
plateNo: '粤B·H2002',
|
||||
driver: '陈志强',
|
||||
phone: '13800138001',
|
||||
status: 'assigned',
|
||||
salary: 350,
|
||||
remark: '种子任务:车辆已在自营态,可直接办结演示自动台账',
|
||||
createdAt: '2026-07-15 11:00',
|
||||
assignedAt: '2026-07-15 11:05',
|
||||
completedAt: '',
|
||||
ledgerRowId: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function loadDispatchTasks() {
|
||||
const stored = readJson(SO_DISPATCH_STORAGE_KEY, null);
|
||||
if (Array.isArray(stored) && stored.length > 0) return stored;
|
||||
const seed = seedDispatchTasks();
|
||||
writeJson(SO_DISPATCH_STORAGE_KEY, seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
export function saveDispatchTasks(list) {
|
||||
writeJson(SO_DISPATCH_STORAGE_KEY, list || []);
|
||||
}
|
||||
|
||||
export function createDispatchTask(payload) {
|
||||
const contract = getContractById(payload.contractId);
|
||||
if (!contract) return { ok: false, message: '请选择自营合同' };
|
||||
if (!['active', 'performing'].includes(contract.status)) {
|
||||
return { ok: false, message: '仅生效/履约中的合同可创建调度任务' };
|
||||
}
|
||||
const list = loadDispatchTasks();
|
||||
const n = list.length + 1;
|
||||
const plateNo = String(payload.plateNo || '').trim();
|
||||
const hasPlate = Boolean(plateNo);
|
||||
const row = {
|
||||
id: `sdt-${Date.now()}`,
|
||||
taskCode: `DD-2026-${String(n).padStart(4, '0')}`,
|
||||
contractId: contract.id,
|
||||
contractCode: contract.contractCode,
|
||||
businessName: contract.businessName,
|
||||
customerName: contract.customerName,
|
||||
planDate: payload.planDate || todayDate(),
|
||||
routeDesc: String(payload.routeDesc || '').trim(),
|
||||
multiTrip: payload.multiTrip === '是' ? '是' : '否',
|
||||
routePricing: payload.routePricing || contract.pricingSummary || '线路计价',
|
||||
unitPrice: Number(payload.unitPrice ?? contract.unitPriceDefault) || 0,
|
||||
quantity: Number(payload.quantity) || 1,
|
||||
plateNo,
|
||||
driver: String(payload.driver || '').trim(),
|
||||
phone: String(payload.phone || '').trim(),
|
||||
status: hasPlate ? 'assigned' : 'pending_assign',
|
||||
salary: Number(payload.salary) || 0,
|
||||
remark: String(payload.remark || '').trim(),
|
||||
createdAt: nowStamp(),
|
||||
assignedAt: hasPlate ? nowStamp() : '',
|
||||
completedAt: '',
|
||||
ledgerRowId: '',
|
||||
};
|
||||
list.unshift(row);
|
||||
saveDispatchTasks(list);
|
||||
if (contract.status === 'active') {
|
||||
const contracts = loadContracts();
|
||||
const cidx = contracts.findIndex((c) => c.id === contract.id);
|
||||
if (cidx >= 0) {
|
||||
contracts[cidx] = { ...contracts[cidx], status: 'performing' };
|
||||
saveContracts(contracts);
|
||||
}
|
||||
}
|
||||
return { ok: true, task: row, message: `已创建调度任务 ${row.taskCode}` };
|
||||
}
|
||||
|
||||
export function assignDispatchTask(taskId, payload) {
|
||||
const list = loadDispatchTasks();
|
||||
const idx = list.findIndex((t) => t.id === taskId);
|
||||
if (idx < 0) return { ok: false, message: '任务不存在' };
|
||||
const t = list[idx];
|
||||
if (['completed', 'cancelled'].includes(t.status)) {
|
||||
return { ok: false, message: '已办结/已取消任务不可改派' };
|
||||
}
|
||||
const plateNo = String(payload.plateNo || '').trim();
|
||||
if (!plateNo) return { ok: false, message: '请选择车牌' };
|
||||
list[idx] = {
|
||||
...t,
|
||||
plateNo,
|
||||
driver: String(payload.driver || t.driver || '').trim(),
|
||||
phone: String(payload.phone || t.phone || '').trim(),
|
||||
status: 'assigned',
|
||||
assignedAt: nowStamp(),
|
||||
};
|
||||
saveDispatchTasks(list);
|
||||
return { ok: true, task: list[idx], message: `已派车 ${plateNo}` };
|
||||
}
|
||||
|
||||
export function startDispatchTask(taskId) {
|
||||
const list = loadDispatchTasks();
|
||||
const idx = list.findIndex((t) => t.id === taskId);
|
||||
if (idx < 0) return { ok: false, message: '任务不存在' };
|
||||
const t = list[idx];
|
||||
if (t.status !== 'assigned') {
|
||||
return { ok: false, message: '仅「已派」任务可开始执行' };
|
||||
}
|
||||
if (!t.plateNo) return { ok: false, message: '请先派车' };
|
||||
list[idx] = { ...t, status: 'in_progress' };
|
||||
saveDispatchTasks(list);
|
||||
return { ok: true, task: list[idx], message: '任务已进入执行中' };
|
||||
}
|
||||
|
||||
export function cancelDispatchTask(taskId) {
|
||||
const list = loadDispatchTasks();
|
||||
const idx = list.findIndex((t) => t.id === taskId);
|
||||
if (idx < 0) return { ok: false, message: '任务不存在' };
|
||||
const t = list[idx];
|
||||
if (t.status === 'completed') {
|
||||
return { ok: false, message: '已办结任务不可取消(台账行已生成)' };
|
||||
}
|
||||
if (t.status === 'cancelled') return { ok: true, task: t, message: '任务已是取消状态' };
|
||||
list[idx] = { ...t, status: 'cancelled' };
|
||||
saveDispatchTasks(list);
|
||||
return { ok: true, task: list[idx], message: `已取消 ${t.taskCode}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* 办结 → 自动生成台账行(严格:车辆须已交车)
|
||||
*/
|
||||
export function completeDispatchTask(taskId, confirmPayload) {
|
||||
const list = loadDispatchTasks();
|
||||
const idx = list.findIndex((t) => t.id === taskId);
|
||||
if (idx < 0) return { ok: false, message: '任务不存在' };
|
||||
const t = list[idx];
|
||||
if (t.status === 'completed') {
|
||||
return { ok: false, message: '任务已办结', ledgerRowId: t.ledgerRowId };
|
||||
}
|
||||
if (t.status === 'cancelled') {
|
||||
return { ok: false, message: '已取消任务不可办结' };
|
||||
}
|
||||
if (!t.plateNo) return { ok: false, message: '请先派车再办结' };
|
||||
|
||||
const ops = getVehicleOpsStatus(t.plateNo);
|
||||
if (ops !== VEHICLE_OPS.self_operated) {
|
||||
return {
|
||||
ok: false,
|
||||
code: 'NEED_HANDOVER',
|
||||
message: `${t.plateNo} 尚未交车(当前非自营运营态),请先「模拟交车」或完成运维交车后再办结`,
|
||||
};
|
||||
}
|
||||
|
||||
const dispatchDate = (confirmPayload && confirmPayload.dispatchDate) || t.planDate || todayDate();
|
||||
const quantity = Number(confirmPayload?.quantity ?? t.quantity) || 1;
|
||||
const unitPrice = Number(confirmPayload?.unitPrice ?? t.unitPrice) || 0;
|
||||
const salary = Number(confirmPayload?.salary ?? t.salary) || 0;
|
||||
const remark = String(confirmPayload?.remark ?? t.remark ?? '').trim();
|
||||
|
||||
const year = dispatchDate.slice(0, 4);
|
||||
const month = String(Number(dispatchDate.slice(5, 7)) || 1);
|
||||
const amount = computeAmount(unitPrice, quantity);
|
||||
const ledgerDraft = {
|
||||
id: `auto-${t.id}`,
|
||||
year,
|
||||
month,
|
||||
dispatchDate,
|
||||
businessName: t.businessName,
|
||||
driver: t.driver,
|
||||
phone: t.phone,
|
||||
plateNo: t.plateNo,
|
||||
systemVehicleType: brandModelOf(t.plateNo),
|
||||
unitPrice,
|
||||
quantity,
|
||||
amount,
|
||||
hydrogenFee: 0,
|
||||
etcFee: 0,
|
||||
salary,
|
||||
electricityFee: 0,
|
||||
manualReimbursement: 0,
|
||||
dailySocialSecurityFee: 0,
|
||||
dailyTrailerServiceFee: 0,
|
||||
dailyTrailerFee: 0,
|
||||
dailyParkingFee: 0,
|
||||
dailyTireFee: 0,
|
||||
vehicleFee: 0,
|
||||
totalCost: 0,
|
||||
profitLoss: 0,
|
||||
multiTrip: t.multiTrip || '否',
|
||||
routePricing: t.routePricing || '线路计价',
|
||||
remark: remark ? `[调度自动·${t.taskCode}] ${remark}` : `[调度自动·${t.taskCode}]`,
|
||||
rowSource: 'auto',
|
||||
dispatchTaskId: t.id,
|
||||
dispatchTaskCode: t.taskCode,
|
||||
contractCode: t.contractCode,
|
||||
};
|
||||
ledgerDraft.totalCost = computeTotalCost(ledgerDraft);
|
||||
ledgerDraft.profitLoss = roundMoney(amount - ledgerDraft.totalCost);
|
||||
|
||||
const autoRows = loadAutoLedgerRows();
|
||||
const withoutDup = autoRows.filter((r) => r.dispatchTaskId !== t.id && r.id !== ledgerDraft.id);
|
||||
withoutDup.unshift(ledgerDraft);
|
||||
saveAutoLedgerRows(withoutDup);
|
||||
|
||||
list[idx] = {
|
||||
...t,
|
||||
status: 'completed',
|
||||
quantity,
|
||||
unitPrice,
|
||||
salary,
|
||||
remark,
|
||||
completedAt: nowStamp(),
|
||||
ledgerRowId: ledgerDraft.id,
|
||||
};
|
||||
saveDispatchTasks(list);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
task: list[idx],
|
||||
ledgerRow: ledgerDraft,
|
||||
message: `已办结 ${t.taskCode},并自动生成物流业务明细 1 行`,
|
||||
};
|
||||
}
|
||||
|
||||
// —— 台账自动行 ——
|
||||
|
||||
export function loadAutoLedgerRows() {
|
||||
const stored = readJson(SO_LEDGER_AUTO_STORAGE_KEY, null);
|
||||
return Array.isArray(stored) ? stored : [];
|
||||
}
|
||||
|
||||
export function saveAutoLedgerRows(list) {
|
||||
writeJson(SO_LEDGER_AUTO_STORAGE_KEY, list || []);
|
||||
}
|
||||
|
||||
export function removeAutoLedgerRow(rowId) {
|
||||
const next = loadAutoLedgerRows().filter((r) => r.id !== rowId);
|
||||
saveAutoLedgerRows(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并种子/当前列表与自动生成行:自动行按 id 覆盖同 id,其余追加到顶部
|
||||
*/
|
||||
export function mergeLedgerWithAutoRows(baseRows) {
|
||||
const auto = loadAutoLedgerRows();
|
||||
const autoIds = new Set(auto.map((r) => r.id));
|
||||
const rest = (baseRows || []).filter((r) => !autoIds.has(r.id));
|
||||
return [...auto, ...rest];
|
||||
}
|
||||
|
||||
export function subscribeSelfOperatedBridge(handler) {
|
||||
const fn = (e) => handler(e?.detail);
|
||||
window.addEventListener('oneos-self-operated-bridge', fn);
|
||||
window.addEventListener('storage', fn);
|
||||
return () => {
|
||||
window.removeEventListener('oneos-self-operated-bridge', fn);
|
||||
window.removeEventListener('storage', fn);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# 业务部台账 · 产品需求说明(PRD)
|
||||
|
||||
> 原型路径:`/prototypes/business-dept-ledger`
|
||||
> 实现页:`src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx`
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块定位
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 目标用户 | 业务部业管 / 运营查看汇总 |
|
||||
| 核心任务 | 按业务条线汇总查看业绩、成本、盈亏,并支持租赁/氢费下钻 |
|
||||
| 内容来源 | 各业务明细模块按月汇总(原型本地种子,未接真实 API) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 各业务条线数据统计来源(摘要)
|
||||
|
||||
主表分组表头在业务名称后方标注最小颗粒度来源;悬停可查看业绩/成本/盈亏字段口径。
|
||||
|
||||
| 业务条线 | 最小颗粒度 | 业绩 | 成本 | 盈亏 |
|
||||
|---|---|---|---|---|
|
||||
| 物流业务 | 物流业务明细 | 「金额」列 | 「总成本」列 | 金额 − 总成本 |
|
||||
| 租赁业务 | 租赁业务明细 | 「实收金额」列 | 「车辆实际成本」列 | 实收金额 − 车辆实际成本 |
|
||||
| 氢费业务 | 车辆氢费明细 | 「加氢总价(元)」列 | 「成本总价(元)」列 | 加氢总价 − 成本总价 |
|
||||
| 电费业务 | 充电订单 | 「对客费用(元)」列 | 「成本费用(元)」列 | 对客费用 − 成本费用 |
|
||||
| ETC业务 | ETC业务 | 字段口径待补充 | 字段口径待补充 | 字段口径待补充 |
|
||||
|
||||
完整判定、前置条件与代码映射见 [sector-data-source.md](./sector-data-source.md)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 验收重点
|
||||
|
||||
- [ ] 主表五条业务线后方分别显示:`物流业务明细` / `租赁业务明细` / `车辆氢费明细` / `充电订单` / `ETC业务`(不再显示「预留明细」)
|
||||
- [ ] 悬停业务名称或来源标注,可看到该条线最小颗粒度与业绩/成本/盈亏口径
|
||||
- [ ] 悬停「业绩」「成本」「盈亏」列头,提示对应明细字段
|
||||
- [ ] 电费来源为「充电订单」;ETC 来源为「ETC业务」
|
||||
- [ ] 文档与页面口径一致:`.spec/sector-data-source.md`、本 PRD
|
||||
@@ -0,0 +1,60 @@
|
||||
# 业务部台账 · 各业务条线数据统计来源
|
||||
|
||||
> 实现:`src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx`
|
||||
> 常量:`SECTOR_DATA_SOURCE`、`SECTOR_METRIC_SOURCE`
|
||||
> UI:主表分组表头「业务名称 ← 明细来源」;业绩/成本/盈亏列悬停展示字段口径
|
||||
|
||||
## 对照数据源
|
||||
|
||||
| 业务条线 | 最小颗粒度数据来源 | 原型本地种子 | 真实 API |
|
||||
|---|---|---|---|
|
||||
| 物流业务 | 物流业务明细 | 是(汇总种子) | 未接 |
|
||||
| 租赁业务 | 租赁业务明细 | 是(汇总种子) | 未接 |
|
||||
| 氢费业务 | 车辆氢费明细 | 是(汇总种子) | 未接 |
|
||||
| 电费业务 | 充电订单 | 是(汇总种子) | 未接 |
|
||||
| ETC业务 | ETC业务 | 是(汇总种子) | 未接 |
|
||||
|
||||
主表当前展示的是**按月(或客户/事业部)汇总后的结果**;上表「最小颗粒度」指最终应回溯到的明细模块。
|
||||
|
||||
## 业绩 / 成本 / 盈亏口径(判定顺序)
|
||||
|
||||
按业务条线取数;同一条线内先取业绩、成本字段,再计算盈亏。
|
||||
|
||||
| 优先级 | 指标 | 规则 |
|
||||
|---|---|---|
|
||||
| 1 | 业绩 | 取对应明细模块指定金额列(见下表) |
|
||||
| 2 | 成本 | 取对应明细模块指定成本列(见下表) |
|
||||
| 3 | 盈亏 | 业绩 − 成本(见下表公式) |
|
||||
|
||||
### 分条线字段映射
|
||||
|
||||
| 业务条线 | 业绩来源列 | 成本来源列 | 盈亏公式 |
|
||||
|---|---|---|---|
|
||||
| 物流业务 | 物流业务明细「金额」 | 物流业务明细「总成本」 | 金额 − 总成本 |
|
||||
| 租赁业务 | 租赁业务明细「实收金额」 | 租赁业务明细「车辆实际成本」 | 实收金额 − 车辆实际成本 |
|
||||
| 氢费业务 | 车辆氢费明细「加氢总价(元)」 | 车辆氢费明细「成本总价(元)」 | 加氢总价(元)− 成本总价(元) |
|
||||
| 电费业务 | 充电订单「对客费用(元)」 | 充电订单「成本费用(元)」 | 对客费用(元)− 成本费用(元) |
|
||||
| ETC业务 | ETC业务(字段口径待补充) | ETC业务(字段口径待补充) | ETC业务(字段口径待补充) |
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 仅在业务部台账主表分组列展示来源标注;钻取子页「数据来源」仍使用 `SECTOR_DATA_SOURCE` 短名。
|
||||
- ETC 业务本期仅确认最小颗粒度模块为「ETC业务」,业绩/成本/盈亏字段列名待业务补充后更新本规格与代码常量。
|
||||
|
||||
## 用户可见结果
|
||||
|
||||
| 位置 | 展示 |
|
||||
|---|---|
|
||||
| 分组表头业务名称后方 | `←物流业务明细` / `←租赁业务明细` / `←车辆氢费明细` / `←充电订单` / `←ETC业务` |
|
||||
| 悬停业务名称或来源标注 | 最小颗粒度 + 业绩/成本/盈亏口径全文 |
|
||||
| 悬停「业绩」「成本」「盈亏」列头 | 该列对应字段口径 |
|
||||
|
||||
## 与代码映射
|
||||
|
||||
| 常量 / 函数 | 作用 |
|
||||
|---|---|
|
||||
| `SECTOR_DATA_SOURCE` | 表头后方短名;钻取页数据来源短名 |
|
||||
| `SECTOR_METRIC_SOURCE` | 业绩/成本/盈亏字段口径 |
|
||||
| `buildSectorSourceTooltip` | 分组表头悬停全文 |
|
||||
| `renderSectorMetricTitle` | 指标列头悬停 |
|
||||
| `renderSectorGroupTitle` | 渲染「业务名 ← 来源」 |
|
||||
69
src/prototypes/business-dept-ledger/annotation-source.json
Normal file
69
src/prototypes/business-dept-ledger/annotation-source.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"documentVersion": 1,
|
||||
"format": "axhub-annotation-source",
|
||||
"data": {
|
||||
"version": 2,
|
||||
"prototypeName": "business-dept-ledger",
|
||||
"pageId": "main",
|
||||
"updatedAt": 1783938000000,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "bdl-list-table",
|
||||
"index": 1,
|
||||
"title": "主表 · 业务条线数据来源",
|
||||
"pageId": "main",
|
||||
"locator": {
|
||||
"selectors": [
|
||||
"[data-annotation-id=\"bdl-list-table\"]"
|
||||
],
|
||||
"fingerprint": "bdl-list-table",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "业务部台账主表各业务条线业绩/成本/盈亏数据来源口径。",
|
||||
"annotationText": "## 各业务条线数据统计来源\n\n分组表头业务名称后方标注最小颗粒度来源;悬停查看业绩/成本/盈亏字段口径。\n\n| 业务条线 | 最小颗粒度 | 业绩 | 成本 | 盈亏 |\n|---|---|---|---|---|\n| 物流业务 | 物流业务明细 | 金额 | 总成本 | 金额 − 总成本 |\n| 租赁业务 | 租赁业务明细 | 实收金额 | 车辆实际成本 | 实收金额 − 车辆实际成本 |\n| 氢费业务 | 车辆氢费明细 | 加氢总价(元) | 成本总价(元) | 加氢总价 − 成本总价 |\n| 电费业务 | 充电订单 | 对客费用(元) | 成本费用(元) | 对客费用 − 成本费用 |\n| ETC业务 | ETC业务 | 待补充 | 待补充 | 待补充 |\n\n完整规格见 `.spec/sector-data-source.md`。原型本地种子,未接真实 API。",
|
||||
"hasMarkdown": true,
|
||||
"color": "#2563eb",
|
||||
"images": [],
|
||||
"createdAt": 1783938000000,
|
||||
"updatedAt": 1783938000000
|
||||
}
|
||||
],
|
||||
"notes": {
|
||||
"bdl-list-table": "## 各业务条线数据统计来源\n\n分组表头业务名称后方标注最小颗粒度来源;悬停查看业绩/成本/盈亏字段口径。\n\n| 业务条线 | 最小颗粒度 | 业绩 | 成本 | 盈亏 |\n|---|---|---|---|---|\n| 物流业务 | 物流业务明细 | 金额 | 总成本 | 金额 − 总成本 |\n| 租赁业务 | 租赁业务明细 | 实收金额 | 车辆实际成本 | 实收金额 − 车辆实际成本 |\n| 氢费业务 | 车辆氢费明细 | 加氢总价(元) | 成本总价(元) | 加氢总价 − 成本总价 |\n| 电费业务 | 充电订单 | 对客费用(元) | 成本费用(元) | 对客费用 − 成本费用 |\n| ETC业务 | ETC业务 | 待补充 | 待补充 | 待补充 |\n\n完整规格见 `.spec/sector-data-source.md`。原型本地种子,未接真实 API。",
|
||||
"bdl-doc-sector-data-source": "# 业务部台账 · 各业务条线数据统计来源\n\n见 `.spec/sector-data-source.md`。"
|
||||
},
|
||||
"directory": [
|
||||
{
|
||||
"type": "group",
|
||||
"id": "bdl-dir-logic",
|
||||
"title": "业务逻辑规格",
|
||||
"children": [
|
||||
{
|
||||
"type": "note",
|
||||
"id": "bdl-doc-sector-data-source",
|
||||
"title": "各业务条线数据统计来源",
|
||||
"markdownPath": ".spec/sector-data-source.md"
|
||||
},
|
||||
{
|
||||
"type": "note",
|
||||
"id": "bdl-doc-prd",
|
||||
"title": "产品需求说明(PRD)",
|
||||
"markdownPath": ".spec/requirements-prd.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"id": "bdl-dir-page",
|
||||
"title": "页面",
|
||||
"children": [
|
||||
{
|
||||
"type": "annotation",
|
||||
"id": "bdl-list-table",
|
||||
"title": "主表 · 业务条线数据来源"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
376
src/prototypes/insurance-procurement/.spec/requirements-prd.md
Normal file
376
src/prototypes/insurance-procurement/.spec/requirements-prd.md
Normal file
@@ -0,0 +1,376 @@
|
||||
# 保险采购 · 产品需求说明(全模块)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 模块名称 | 保险采购 |
|
||||
| 所属系统 | ONE-OS(PC) |
|
||||
| 交互原型 | `/prototypes/insurance-procurement` |
|
||||
| 业务条线 | 运维管理条线(证照 / 保险相关) |
|
||||
| 文档状态 | AutoPRD · 已对齐可运行原型 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话与目标
|
||||
|
||||
**一句话**
|
||||
在同一模块内管理车队**保单台账**与**采购前比价审批**两条业务线,保证续保不遗漏、变更可追溯,并为交车提供交强险 + 商业险合规校验。
|
||||
|
||||
**要解决的问题**
|
||||
- 保险信息散落在表格、邮件与线下批单中,台账不全、临期易漏
|
||||
- 停保 / 复驶 / 退保缺少统一留痕
|
||||
- 采购前多方比价难追溯,审批结果难跟踪
|
||||
- 交车时无法与车辆管理共用同一套「保险是否有效」口径
|
||||
|
||||
**本期目标**
|
||||
1. 一车一档维护五类险种台账;支持手工新增、批量识别、批量导入
|
||||
2. 支持停保 / 复驶 / 退保办理与操作历史
|
||||
3. 输出与车辆管理一致的车辆级保险状态,支撑交车拦截
|
||||
4. 比价单批次管理:多方报价、最晚付费预警、勾选提交采购审批(本页只读跟踪)
|
||||
|
||||
**非目标(本期不做)**
|
||||
- 比价单与保单台账**自动双向同步**;审批通过后**不自动写入**正式保单
|
||||
- 在本模块内办理审批(通过 / 驳回 / 撤回均在审批中心)
|
||||
- 维护行驶证、道路运输证等证照档案(见证照管理)
|
||||
- 事故赔付、保险上浮等事故链路台账(见事故管理)
|
||||
|
||||
---
|
||||
|
||||
## 2. 模块边界(最重要)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
IPC[保险采购]
|
||||
VM[车辆管理]
|
||||
OPS[交车管理]
|
||||
APPR[审批中心]
|
||||
CERT[证照管理]
|
||||
WB[工作台]
|
||||
IPC -->|输出保险状态口径| VM
|
||||
VM -->|交车前置校验| OPS
|
||||
CERT -.->|并列交车前置| OPS
|
||||
IPC -->|发起采购审批| APPR
|
||||
APPR -->|回写采购状态| IPC
|
||||
WB -->|临期 KPI / 快捷入口| IPC
|
||||
```
|
||||
|
||||
| 业务线 / 子域 | 做什么 | 不做什么 |
|
||||
|---------------|--------|----------|
|
||||
| 保单管理 | 一车一档、多通道录入、停保/复驶/退保、状态与交车口径 | 不替代证照管理;不办事故赔付 |
|
||||
| 比价单 | 批次比价、付费预警、提交采购、只读跟踪审批 | 不自动生成正式保单;不在本页审批 |
|
||||
| 与车辆 / 交车 | 提供交强 + 商业是否有效的统一口径 | 不在本页完成交车动作 |
|
||||
|
||||
**外部依赖(产品口径)**
|
||||
|
||||
| 依赖方 | 交互方式 | 产品要求 |
|
||||
|--------|----------|----------|
|
||||
| 车辆管理 | 读取保险状态 / 到期日 | 口径与本模块一致:异常则禁止交车 |
|
||||
| 交车管理 | 交付前校验交强 + 商业有效 | 不合规须可理解拦截原因 |
|
||||
| 审批中心 | 接收采购申请并回写状态 / 当前审批人 | 本页只读展示,不办理审批 |
|
||||
| 工作台 | KPI、快捷「发起保险采购」 | 跳转至本模块对应能力 |
|
||||
| 证照管理 | 并列交车前置 | 证照与保险各自独立维护 |
|
||||
|
||||
**关键约束**
|
||||
- **保单台账 ↔ 比价单互不自动同步**;审批通过后须运营在保单管理侧另行录入正式保单
|
||||
- **交车只看交强险 + 商业险**;超赔 / 货物 / 驾意仅参与临期预警,不参与交车判定
|
||||
- **最晚付费临期 / 超期**仅服务比价采购节奏,不写入保单台账
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户与角色
|
||||
|
||||
| 角色 | 主要目标 |
|
||||
|------|----------|
|
||||
| 运维部 | 维护保单台账、办理停保/复驶/退保;交车前确认保险有效 |
|
||||
| 安全部 | 从合规视角关注保险与证照并列的交车前置 |
|
||||
| 采购部 | 创建比价单、录入报价、提交采购申请;关注临期与付费节奏 |
|
||||
| 业务管理组(业管) | 在审批中心确认报价等节点(本页只读看到结果) |
|
||||
|
||||
> 部门名对齐业务条线说明;`module-role-mapping` 将本模块主责记为运维部、安全部,采购部为比价线主操作用户。
|
||||
|
||||
---
|
||||
|
||||
## 4. 用户故事与故事点(业务条线说明口径)
|
||||
|
||||
> 故事点供排期参考,可按团队基准调整。合计约 **28 SP**。
|
||||
|
||||
### Epic A · 保单台账与交车合规(约 15 SP)
|
||||
|
||||
#### A1 · 一车一档台账与检索(约 3 SP)
|
||||
- **角色**:运维部、采购部
|
||||
- **起点**:需要按车牌 / VIN / 运营状态 / 保险状态等快速定位车辆保险档案。
|
||||
- **怎么运作**:
|
||||
1. 在列表筛选或点击 KPI 卡片缩小范围。
|
||||
2. 查看五类险种到期日与车辆级保险状态。
|
||||
3. 进入「管理」打开车辆保险档案。
|
||||
- **关键结果**:`可按条件定位车辆` / `空态提示无可匹配台账`
|
||||
- **闭环**:台账可检索、可下钻,与 KPI 口径一致。
|
||||
- **排期(可选)**:US-IPC-01 · 规模 M · 作为运维/采购,我想快速筛出台账车辆,以便处理续保与异常。
|
||||
|
||||
#### A2 · 多通道录入正式保单(约 5 SP)
|
||||
- **角色**:运维部、采购部
|
||||
- **起点**:拿到新保或续保材料(纸质 / 电子保单、批量表格)。
|
||||
- **怎么运作**:
|
||||
1. 选择新增、保单批量识别或批量导入。
|
||||
2. 识别类须人工确认后再落库;导入仅处理新保/续保。
|
||||
3. 按车牌/VIN(或批单场景按保单号)匹配车辆并写入对应险种台账。
|
||||
- **关键结果**:`确认后落库` / `识别失败可理解提示` / `车牌不一致则拦截`
|
||||
- **闭环**:正式保单进入一车一档,列表与车辆管理可见最新到期与状态。
|
||||
- **排期(可选)**:US-IPC-02 · 规模 L · 作为运维/采购,我想用识别或导入补齐台账,以便续保不遗漏。
|
||||
|
||||
#### A3 · 停保 / 复驶 / 退保留痕(约 4 SP)
|
||||
- **角色**:运维部
|
||||
- **起点**:车辆需临时停保、恢复行驶或办理退保。
|
||||
- **怎么运作**:
|
||||
1. 在车辆保险档案对**当前有效**记录发起对应办理。
|
||||
2. 填写时间、金额等业务字段并上传批单附件。
|
||||
3. 写入操作历史;更新险种与车辆状态展示。
|
||||
- **关键结果**:`变更可追溯` / `归档记录只读`
|
||||
- **闭环**:停保/复驶/退保全程留痕,交车口径随之更新。
|
||||
- **排期(可选)**:US-IPC-03 · 规模 M。
|
||||
|
||||
#### A4 · 交强 + 商业决定是否可交车(约 3 SP)
|
||||
- **角色**:运维部、采购部
|
||||
- **起点**:采购/运维维护交强险与商业险台账,作为交车前置合规数据。
|
||||
- **怎么运作**:
|
||||
1. 台账维护一车一档核心险种。
|
||||
2. 交车时按「交强 + 商业均为正常或临期」判定是否放行。
|
||||
3. 异常(未购买 / 已到期 / 已退保视同无效)则拦截并提示原因。
|
||||
- **关键结果**:`允许交车` / `禁止交车 · 保险异常`
|
||||
- **闭环**:交车合规与台账一致、可追溯;异常车辆不能进入履约运营。
|
||||
- **排期(可选)**:US-17 · 规模 M · 作为采购/运维,我想维护交强+商业险并在交车时校验,以便不合规车辆无法交车。
|
||||
|
||||
### Epic B · 比价采购与审批跟踪(约 13 SP)
|
||||
|
||||
#### B1 · 比价单批次与付费预警(约 4 SP)
|
||||
- **角色**:采购部
|
||||
- **起点**:需要按批次组织待购车辆与险种,并盯住最晚付费日。
|
||||
- **怎么运作**:
|
||||
1. 在比价单管理新建或编辑批次;可从临期预警一键带入。
|
||||
2. 用全部 / 临期(≤3 天)/ 超期看板筛选批次。
|
||||
3. 保存时校验购买记录、备注与附件。
|
||||
- **关键结果**:`可按付费节奏跟进` / `未满足保存条件则拦截`
|
||||
- **闭环**:采购节奏可视、可预警;不与保单台账自动混写。
|
||||
- **排期(可选)**:US-IPC-04 · 规模 M。
|
||||
|
||||
#### B2 · 多方报价与提交采购(约 5 SP)
|
||||
- **角色**:采购部
|
||||
- **起点**:同一购买需求需比较多家保司报价后发起采购。
|
||||
- **怎么运作**:
|
||||
1. 为每行录入多家报价并指定唯一最终比价。
|
||||
2. 填写最晚付费日,勾选可提交行。
|
||||
3. 提交采购申请进入审批中心。
|
||||
- **关键结果**:`审批中/已通过行不可再提` / `撤回或驳回后可再提`
|
||||
- **闭环**:比价过程可追溯;正式保单仍须审批通过后在台账侧补录。
|
||||
- **排期(可选)**:US-IPC-05 · 规模 L。
|
||||
|
||||
#### B3 · 审批只读回显(约 4 SP)
|
||||
- **角色**:采购部、业务管理组
|
||||
- **起点**:采购申请已在审批中心流转。
|
||||
- **怎么运作**:
|
||||
1. 本页展示采购状态与当前审批人(可为空)。
|
||||
2. 业管等角色在审批中心办理节点。
|
||||
3. 结果回写本页;通过后运营去保单管理录入正式保单。
|
||||
- **关键结果**:`本页不可办理审批` / `状态与审批中心一致`
|
||||
- **闭环**:采购决策可跟踪;台账仍以人工录入为准。
|
||||
- **排期(可选)**:US-IPC-06 · 规模 M。
|
||||
|
||||
---
|
||||
|
||||
## 5. 功能模块说明(正向 / 逆向)
|
||||
|
||||
### 5.1 保险台账列表与 KPI
|
||||
|
||||
**正向**
|
||||
1. 按车牌、VIN、品牌型号、运营状态、保险状态、险种 + 到期区间筛选。
|
||||
2. 点击 KPI(台账车辆、状态正常、核心逾期、核心待购等)联动列表或打开预警。
|
||||
3. 行内「管理」打开车辆保险档案。
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| 无匹配数据 | 空态提示无可匹配台账车辆 |
|
||||
| 退出运营车辆 | 列表可查看但样式弱化 |
|
||||
| 险种到期筛选未选险种 | 须先选险种再筛到期区间 |
|
||||
| 车牌与多车牌条件互斥 | 按产品约定互斥,避免条件冲突 |
|
||||
|
||||
### 5.2 保单录入(新增 / 识别 / 导入)
|
||||
|
||||
**正向**
|
||||
1. 新增:完整字段手工录入后保存。
|
||||
2. 批量识别:选业务类型 → 上传 → 识别 → **人工确认** → 落库。
|
||||
3. 批量导入:下载模板填写 → 上传 → 仅新保/续保入库。
|
||||
4. 识别任务记录:查看进度,对待确认结果继续确认。
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| 识别失败 | 提示清晰度 / 格式 / 损坏等原因 |
|
||||
| 无成功结果可确认 | 提示暂无识别成功结果 |
|
||||
| 识别车牌与所选车不一致 | 拦截并提示两边车牌 |
|
||||
| 导入中的停保/复驶/退保行 | 自动跳过 |
|
||||
| OCR 确认页 | 不展示承保险种明细、分项保费(手工路径仍可填) |
|
||||
|
||||
### 5.3 车辆保险档案 · 停保 / 复驶 / 退保
|
||||
|
||||
**正向**
|
||||
1. 分险种查看历史;对当前有效记录办理停保、复驶或退保。
|
||||
2. 填写业务时间 / 金额等并上传批单附件。
|
||||
3. 全周期时间轴与操作历史可追溯。
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| 历史归档记录 | 只读,不可再办停保/退保 |
|
||||
| 已退保 | 仅允许复驶 |
|
||||
| 已停保 | 可复驶或退保 |
|
||||
| 新保覆盖已退保险种 | 旧记录归档后再写入新保单 |
|
||||
|
||||
### 5.4 比价单管理与编辑
|
||||
|
||||
**正向**
|
||||
1. 新建 / 编辑比价单,添加购买记录与多方报价。
|
||||
2. 设最终比价、最晚付费日;保存备注与附件。
|
||||
3. 勾选行提交采购;跟踪审批状态。
|
||||
4. 可从险种临期预警一键生成比价单(附件须补传)。
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| 保存校验不通过 | 拦截并提示缺记录 / 备注 / 附件等 |
|
||||
| 审批中或已通过行 | 勾选禁用,不可重复提交 |
|
||||
| 撤回 / 驳回 | 恢复可提交 |
|
||||
| 修改行内险种 | 清空该行报价与最终比价 |
|
||||
| 删除比价单 | 二次确认,删除后不可恢复 |
|
||||
| 一键生成时已有审批中/通过的同车同险 | 跳过该条 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键业务逻辑(必须对齐)
|
||||
|
||||
### 6.1 险种范围
|
||||
|
||||
| 险种 | 台账 | 临期预警 | 交车判定 |
|
||||
|------|------|----------|----------|
|
||||
| 交强险 | 是 | 是 | **是(核心)** |
|
||||
| 商业险 | 是 | 是 | **是(核心)** |
|
||||
| 超赔险 | 是 | 是 | 否 |
|
||||
| 驾意险 | 是 | 是 | 否 |
|
||||
| 货物险 | 是 | 是 | 否 |
|
||||
|
||||
### 6.2 单险种状态(按优先级命中即止)
|
||||
|
||||
1. 已退保
|
||||
2. 无保单号 → 未购买
|
||||
3. 停保标记 → 已停保
|
||||
4. 有号但无到期日 → 未购买
|
||||
5. 到期日 ≤ 今天 → 已到期
|
||||
6. 到期日 ≤ 今天 + 30 天 → 临期
|
||||
7. 否则 → 正常
|
||||
|
||||
列表到期日取同车同险种**最新一条**记录。
|
||||
|
||||
### 6.3 车辆级保险状态(交车联动)
|
||||
|
||||
| 条件 | 车辆保险状态 | 交车 |
|
||||
|------|--------------|------|
|
||||
| 交强与商业均为正常或临期(仍在有效期内) | 正常 / 临期 | **允许**(临期可提示尽快续保) |
|
||||
| 交强或商业任一未购买或已到期 | 异常 | **禁止** |
|
||||
| 已退保 | 视同无有效保障 | **禁止** |
|
||||
|
||||
### 6.4 新保 / 续保录入必填(业务口径)
|
||||
|
||||
- 车牌与车架号至少填一项
|
||||
- 必填:险种、保险公司、保单号、生效日、到期日、保险费合计
|
||||
- 承保险种明细、分项保费:非必填
|
||||
|
||||
### 6.5 批单匹配方式
|
||||
|
||||
| 业务 | 匹配方式 |
|
||||
|------|----------|
|
||||
| 保单录入 | 按车牌 / 车架号匹配车辆 |
|
||||
| 停保 / 复驶 / 退保 | 按**保单号**在全库匹配已有记录 |
|
||||
|
||||
### 6.6 比价保存与提交
|
||||
|
||||
**保存须同时满足**:至少 1 条购买记录;每行已选车辆;整单备注非空;整单至少 1 个附件。
|
||||
|
||||
**提交采购另须**:至少勾选 1 行;行内已有报价并设最终比价;已填最晚付费日;采购状态为未提交 / 撤回 / 驳回;未保存须先保存。
|
||||
|
||||
**报价**:保司 + 金额大于 0;同一保司不可对同行重复报价。
|
||||
|
||||
**最晚付费预警(仅比价)**:≤ 3 个自然日为临期;已过为超期。
|
||||
|
||||
### 6.7 采购状态(购买记录行级)
|
||||
|
||||
| 状态 | 可否再提交 | 说明 |
|
||||
|------|------------|------|
|
||||
| 未提交 | 可以 | 默认 |
|
||||
| 审批中 | 不可以 | 本页提交后写入;勾选禁用 |
|
||||
| 审批通过 | 不可以 | 审批中心回写;勾选禁用 |
|
||||
| 撤回 / 审批驳回 | 可以 | 审批中心回写后可再提 |
|
||||
|
||||
> 审批办理不在本页;当前审批人由工作流回写,可为空。
|
||||
|
||||
---
|
||||
|
||||
## 7. 总览流程图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
entry[保险采购入口]
|
||||
entry --> policy[保单管理]
|
||||
entry --> compare[比价单]
|
||||
|
||||
policy --> input[新增 / 识别 / 导入]
|
||||
input --> confirm[人工确认落库]
|
||||
confirm --> archive[车辆保险档案]
|
||||
archive --> change[停保 / 复驶 / 退保]
|
||||
archive --> status[更新车辆保险状态]
|
||||
status --> handover[交车校验交强+商业]
|
||||
|
||||
compare --> batch[新建或预警生成批次]
|
||||
batch --> quote[多方报价与最终比价]
|
||||
quote --> save[保存备注与附件]
|
||||
save --> submit[勾选提交采购]
|
||||
submit --> appr[审批中心流转]
|
||||
appr --> readonly[本页只读回显]
|
||||
readonly --> manual[审批通过后人工录入正式保单]
|
||||
manual --> policy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收清单(产品 / 测试)
|
||||
|
||||
**保单台账**
|
||||
- [ ] 筛选与 KPI 口径一致;空态文案正确
|
||||
- [ ] 五类险种独立台账;列表到期日取最新一条
|
||||
- [ ] 新增 / OCR 确认 / 导入(仅新保续保)均可落库
|
||||
- [ ] OCR 失败、车牌不一致有明确拦截
|
||||
- [ ] 停保 / 复驶 / 退保须附件并写入操作历史;归档只读
|
||||
|
||||
**交车合规**
|
||||
- [ ] 仅交强 + 商业决定车辆级状态
|
||||
- [ ] 异常禁止交车;正常/临期允许(临期可提示续保)
|
||||
- [ ] 超赔 / 货物 / 驾意不参与交车判定
|
||||
|
||||
**比价采购**
|
||||
- [ ] 保存 / 提交校验完整
|
||||
- [ ] 最晚付费临期 ≤3 天、超期看板正确
|
||||
- [ ] 审批中 / 通过行不可再提;撤回 / 驳回可再提
|
||||
- [ ] 审批通过**不**自动写入保单台账
|
||||
- [ ] 本页不可办理审批,状态只读回显
|
||||
|
||||
**边界**
|
||||
- [ ] 保单与比价互不自动同步
|
||||
- [ ] 一键生成跳过已审批中/通过的同车同险
|
||||
|
||||
---
|
||||
|
||||
## 9. 交付口径
|
||||
|
||||
> 「保险采购」在 OneOS PC 承载**保单台账**与**比价采购**两条独立业务线:运维部、采购部可一车一档维护交强 / 商业等五类险种并办理停保复驶退保,车辆管理与交车据此校验核心险种有效性;采购部可在比价单中完成多方报价、付费预警与采购审批跟踪,审批通过后须人工补录正式保单——台账与比价互不同步,审批不在本页办理。
|
||||
File diff suppressed because one or more lines are too long
68
src/prototypes/oneos-h5-h2-order/.spec/flows.md
Normal file
68
src/prototypes/oneos-h5-h2-order/.spec/flows.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 流程图
|
||||
|
||||
> 标注面板支持 Mermaid 渲染。下列流程与当前原型行为一致(本地 Store,未接真实 API)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 主路径:从打开到上报
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[打开加氢订单 H5] --> B[查看本站列表与 KPI]
|
||||
B --> C{前一日手工台账已上传?}
|
||||
C -->|否| D[提示:手工台账有缺失…]
|
||||
D --> E[进入手工台账 · 选中昨日]
|
||||
E --> F[拍照/相册上传并确认]
|
||||
F --> C
|
||||
C -->|是| G[点新增]
|
||||
G --> H[填写时间/车牌/面板/量价]
|
||||
H --> I{同站同车牌同时间已存在?}
|
||||
I -->|是| J[拦截提示]
|
||||
J --> H
|
||||
I -->|否| K[保存写入 Bridge]
|
||||
K --> B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 手工台账补传
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[手工台账 Tab] --> B[月历:绿已传 / 橙未传]
|
||||
B --> C[点选 ≤今日 且 ≥首笔日]
|
||||
C --> D[上传图片]
|
||||
D --> E[二次确认]
|
||||
E --> F[归档不可删 · 可追加]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 编辑 / 删除与锁定
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[打开详情] --> L{羚牛车辆?}
|
||||
L -->|否| D[可编辑 / 可删除]
|
||||
L -->|是| B{已核对或已对账?}
|
||||
B -->|是| C[只读 · 禁改删]
|
||||
B -->|否| D
|
||||
D --> E[写回 Bridge]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 与 Web / 台账联动(概念)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H5 as 加氢订单 H5
|
||||
participant BR as Bridge Store
|
||||
participant WEB as 加氢记录 Web
|
||||
participant LED as 车辆氢费明细
|
||||
participant SITE as 站点对账单
|
||||
H5->>BR: 新增/改删流水 · 上传台账
|
||||
WEB->>BR: 同 Store 读写 · 导出
|
||||
LED->>BR: 完成核对 → 已核对
|
||||
SITE->>BR: 提交对账单 → 已对账
|
||||
```
|
||||
52
src/prototypes/oneos-h5-h2-order/.spec/key-logic.md
Normal file
52
src/prototypes/oneos-h5-h2-order/.spec/key-logic.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 关键逻辑索引
|
||||
|
||||
本页汇总加氢订单(H5)复杂判定入口;细则见各规格全文。原型数据均为本地种子 / 内存 Store,**未接真实 API**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 判定主题一览
|
||||
|
||||
| 主题 | 一句话 | 全文 |
|
||||
|---|---|---|
|
||||
| 手工台账门禁 | **昨日**未传 → 今日禁新增;固定提示文案并直达台账 | [feature-manual-ledger.md](./feature-manual-ledger.md) |
|
||||
| 核对 ≠ 对账 | 能源部核对 / 站点对账单提交,触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md) |
|
||||
| 记录锁定 | **仅羚牛**:已核对或已对账 → 站端不可改删;非羚牛不锁 | [feature-detail.md](./feature-detail.md)、[reconcile-linkage.md](./reconcile-linkage.md)、[fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||
| 重复键 | 同站 + 同车牌 + 同加氢时间 | Bridge / [Web record-data-model](../oneos-web-h2-station/.spec/record-data-model.md) |
|
||||
| 总额计算 | 总额 = 单价 × 量(两位小数) | [feature-create.md](./feature-create.md) |
|
||||
| 羚牛车辆 | 本地系统车牌集合 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 手工台账门禁(摘要)
|
||||
|
||||
| 优先级 | 条件 | 结果 |
|
||||
|--------|------|------|
|
||||
| 1 | 编辑已有记录 | 不校验 |
|
||||
| 2 | 昨日早于本站首笔加氢日 | 不要求昨日台账,可新增 |
|
||||
| 3 | 昨日已有 ≥1 张台账图 | 可新增 |
|
||||
| 4 | 昨日未上传 | 提示并跳转手工台账(选中昨日) |
|
||||
|
||||
用户可见文案:`手工台账有缺失,作为对账重要依据,请先上传手工台账`
|
||||
|
||||
---
|
||||
|
||||
## 3. 核对 / 对账(摘要)
|
||||
|
||||
| 状态 | 谁触发 | 站端影响 |
|
||||
|---|---|---|
|
||||
| 已核对 | 车辆氢费明细「完成核对」 | **羚牛**不可改删 |
|
||||
| 已对账 | 站点信息对账单提交 | **羚牛**不可改删;回写对账时间 |
|
||||
| 非羚牛 | — | 不参与核对/对账展示与锁定 |
|
||||
|
||||
H5 列表主要展示核对状态(仅羚牛);对账细节见联动规格。
|
||||
|
||||
---
|
||||
|
||||
## 4. 代码路径
|
||||
|
||||
| 能力 | 路径 |
|
||||
|---|---|
|
||||
| Bridge | `src/common/h2VehicleLedgerBridge.js` |
|
||||
| 门禁工具 | `utils/manual-ledger.ts` → `isManualLedgerGateBlocked` |
|
||||
| 订单 CRUD | `utils/orders.ts` |
|
||||
| 页面 | `index.tsx` / `ManualLedgerPanel` / `CreateWizard` |
|
||||
64
src/prototypes/oneos-h5-h2-order/.spec/prototype-review.md
Normal file
64
src/prototypes/oneos-h5-h2-order/.spec/prototype-review.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Prototype Review
|
||||
|
||||
- 审查目标:`src/prototypes/oneos-h5-h2-order`(加氢站管理 → **加氢订单** H5;入口 `index.tsx`)
|
||||
- 用户资料/参考资料:
|
||||
- 本轮用户说明:对「加氢订单」执行 Prototype Review / 需求评审(未另附独立需求附件)
|
||||
- `.spec/`:`requirements-prd.md`(v1.7)、`roles.md`、`user-stories.md`、`key-logic.md`、`flows.md`、`feature-list.md`、`feature-create.md`、`feature-detail.md`、`feature-manual-ledger.md`、`fleet-plate-tag.md`、`reconcile-linkage.md`;早期 `2026-07-14-requirements-design.md`(已标注作废条目)
|
||||
- `src/resources/`:`oneos-web-legacy/…/车辆氢费明细-需求文档.md`(文首已注明以 verify-reconcile / reconcile-linkage 为准)
|
||||
- 关联规格:`oneos-web-h2-station/.spec/fleet-vehicle-verify.md`、`record-data-model.md`;`vehicle-h2-fee-ledger/.spec/verify-reconcile.md`
|
||||
- 源码:`index.tsx`、`components/*`、`utils/*`、`types.ts`、`annotation-source.json`;共享 Bridge / 品牌 Store
|
||||
- 生成时间:2026-07-18 20:14
|
||||
- 修复闭环:2026-07-18 20:32(按 Review 处理全部冲突与 P0–P3 项)
|
||||
- 资料冲突/待确认:**无**(已对齐)
|
||||
- 独立评估:`full`
|
||||
|
||||
## 总体点评
|
||||
|
||||
加氢订单 H5 主路径完整可演示。评审指出的冲突已闭环:**昨日门禁文案**、**列表顶栏橙条**、**锁定仅羚牛**(规格与实现一致)、**早期切站稿作废标注**、**删除确认「未锁定」**、历史 resources 权威顺序说明。剩余为原型边界内 Mock(OCR / 车机 / 无真实 API),不构成业务冲突。
|
||||
|
||||
## P0-P3 优先级问题
|
||||
|
||||
### P1 - 手工台账页文案把「今日上传」当成新增门禁 — **已修复**
|
||||
|
||||
- 处理:改为「今日不作为新增门禁,仅要求前一日已上传」;昨日缺失时台账页单独橙条并可选中昨日。
|
||||
|
||||
### P2 - 「记录锁定」规格未写明羚牛例外 — **已修复**
|
||||
|
||||
- 处理:`feature-detail.md` / `reconcile-linkage.md` / `key-logic.md` / `flows.md` / PRD v1.7 / US-04 统一为「锁定仅羚牛」;与 `isOrderLocked` 实现一致。
|
||||
|
||||
### P2 - 门禁提示缺列表顶栏橙条 — **已修复**
|
||||
|
||||
- 处理:订单列表顶部增加可点橙条(跳转台账并选中昨日);保留 FAB 旁提示 + Tab 红点。
|
||||
|
||||
### P3 - 早期设计文档仍写切站 — **已修复**
|
||||
|
||||
- 处理:`2026-07-14-requirements-design.md` 文首与表格标注作废/覆盖项;权威以 PRD 为准。台账规格区分 H5 固定本站 vs Web 多站。
|
||||
|
||||
### P3 - 删除确认文案偏「未对账」 — **已修复**
|
||||
|
||||
- 处理:确认语改为「确认删除车牌 … 的未锁定记录?」;台账二次确认与 Web 文案对齐。
|
||||
|
||||
## 完整性与项目对齐
|
||||
|
||||
| 维度 | 结论 |
|
||||
|------|------|
|
||||
| 核心用户 / 主流程 | 对齐 PRD v1.7 |
|
||||
| 范围边界 | 真登录 / 真 OCR / PLC / 原生 App / 后端联调仍不做 |
|
||||
| 资料冲突 | 已消除;resources 旧文档文首指向现行核对≠对账规格 |
|
||||
| 双端口径 | 门禁昨日、锁定羚牛、Bridge 共享 — 与 Web / 台账一致 |
|
||||
|
||||
## 业务逻辑连贯性
|
||||
|
||||
- 台账门禁:昨日未传 → 顶栏橙条 / Toast / 跳转台账选中昨日;今日状态 ≠ 门禁。
|
||||
- 锁定:先判羚牛,再判核对∨对账。
|
||||
- 总额偏差二次确认、重复键拦截:与 Web 共用规则。
|
||||
|
||||
## 状态、异常、边界与恢复
|
||||
|
||||
评审所列空态、门禁、必填、重复、脏返回、锁定、归档路径仍成立;本轮仅修正文案/提示位与规格一致性,未改变 Mock 边界。
|
||||
|
||||
## 证据与评估说明
|
||||
|
||||
- 用户资料优先级:无新附件;以 `.spec` PRD v1.7 为基线。
|
||||
- 读取范围:见文首;闭环改动见 `ManualLedgerPanel.tsx`、`index.tsx` 及上述规格文件。
|
||||
- 独立评估:`full`。
|
||||
38
src/prototypes/oneos-h5-h2-order/.spec/roles.md
Normal file
38
src/prototypes/oneos-h5-h2-order/.spec/roles.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 需求角色
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 适用原型 | 加氢订单(H5)`oneos-h5-h2-order` |
|
||||
| 关联 Web | 加氢记录 `oneos-web-h2-station`(同一业务、不同端) |
|
||||
| 数据 | 原型本地种子 + 共享 Bridge(未接真实登录 / API) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 角色一览
|
||||
|
||||
| 角色 | 端 | 核心诉求 | 本原型是否为主用户 |
|
||||
|---|---|---|---|
|
||||
| **加氢站操作人员** | H5(手机浏览器) | 在站场快速上报本站加氢流水;补传手工台账照片 | **是** |
|
||||
| **加氢站站长 / 多站管理员** | Web 为主,H5 可看本站 | 多站台账、导出、手工台账补传与督导 | 间接(H5 固定单站演示) |
|
||||
| **能源部核对人员** | 车辆氢费明细 | 完成核对,回写「已核对」 | 否(触发方在台账) |
|
||||
| **站点对账人员** | 站点信息 · 对账单 | 提交对账单,回写「已对账」 | 否(触发方在站点信息) |
|
||||
| **超级管理员** | Web | 查看全部站点流水与台账 | 否(Web 标注状态演示) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 本原型主角色:加氢站操作人员
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 场景 | 户外 / 站场,手机竖屏 |
|
||||
| 权限边界 | 仅本站数据;不可切站(原型固定演示站) |
|
||||
| 日常动作 | 看列表 KPI → 上传昨日手工台账(若缺失)→ 新增加氢记录 → 查看/编辑未锁定记录 |
|
||||
| 约束 | 前一日手工台账未上传则今日不可新增;已核对或已对账不可改删 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 角色边界(本期不做)
|
||||
|
||||
- 真登录与组织权限中心对接
|
||||
- 能源部 / 对账人员在 H5 内操作核对或对账
|
||||
- 跨站汇总与导出(属 Web「加氢记录」)
|
||||
84
src/prototypes/oneos-h5-h2-order/.spec/user-stories.md
Normal file
84
src/prototypes/oneos-h5-h2-order/.spec/user-stories.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 用户故事
|
||||
|
||||
> 以加氢站操作人员为主;验收口径见 [requirements-prd.md](./requirements-prd.md)。
|
||||
|
||||
---
|
||||
|
||||
## US-01 查看本站加氢订单
|
||||
|
||||
**作为** 加氢站操作人员,
|
||||
**我希望** 打开加氢订单即看到本站列表与 KPI,
|
||||
**以便** 快速了解今日/近期上报情况。
|
||||
|
||||
**验收要点**
|
||||
|
||||
- 默认已登录本站;无切站
|
||||
- KPI:订单数 / 加氢量 / 加氢金额随列表汇总
|
||||
- 卡片展示加氢时间、车牌、量、额、核对状态等
|
||||
|
||||
---
|
||||
|
||||
## US-02 补传缺失的手工台账后再新增
|
||||
|
||||
**作为** 加氢站操作人员,
|
||||
**我希望** 在前一日手工台账未上传时被明确拦截,并直达台账页补传,
|
||||
**以便** 保证纸质台账作为对账依据不缺失。
|
||||
|
||||
**验收要点**
|
||||
|
||||
- 顶栏橙条 + 底栏提示:「手工台账有缺失,作为对账重要依据,请先上传手工台账」
|
||||
- 点「新增」或顶栏橙条跳转手工台账并选中昨日
|
||||
- 补传昨日确认后可新增
|
||||
|
||||
---
|
||||
|
||||
## US-03 新增加氢记录
|
||||
|
||||
**作为** 加氢站操作人员,
|
||||
**我希望** 用车牌键盘 + 面板拍照/OCR 快速填单,
|
||||
**以便** 在站场少打字完成上报。
|
||||
|
||||
**验收要点**
|
||||
|
||||
- 加氢时间默认=点击新增时刻(到分)
|
||||
- 站名只读为本站
|
||||
- 总额 = 单价 × 加氢量(界面不展示公式)
|
||||
- 同站同车牌同时间重复保存拦截
|
||||
|
||||
---
|
||||
|
||||
## US-04 查看详情并编辑/删除未锁定记录
|
||||
|
||||
**作为** 加氢站操作人员,
|
||||
**我希望** 打开详情查看字段与识别原图,并对未核对未对账记录改删,
|
||||
**以便** 纠正录入错误。
|
||||
|
||||
**验收要点**
|
||||
|
||||
- 羚牛车辆已核对或已对账:不可编辑/删除,有锁定说明;非羚牛不锁定
|
||||
- 详情展示车牌/面板含水印原图
|
||||
|
||||
---
|
||||
|
||||
## US-05 按日历管理手工台账
|
||||
|
||||
**作为** 加氢站操作人员,
|
||||
**我希望** 在月历上看已传/未传并补传过往日,
|
||||
**以便** 多日未操作后仍能补齐缺口。
|
||||
|
||||
**验收要点**
|
||||
|
||||
- 自首笔加氢日起绿/橙标记;首笔前不要求
|
||||
- 禁选未来日;已归档图不可删,仅可追加
|
||||
|
||||
---
|
||||
|
||||
## US-06 识别羚牛车辆
|
||||
|
||||
**作为** 加氢站操作人员,
|
||||
**我希望** 在列表/详情看到是否羚牛车辆,
|
||||
**以便** 理解后续核对对账是否适用该车。
|
||||
|
||||
**验收要点**
|
||||
|
||||
- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定)
|
||||
@@ -16,17 +16,6 @@ import {
|
||||
import { PhoneShell } from './PhoneShell';
|
||||
import { PlateKeyboardSheet } from './PlateKeyboardSheet';
|
||||
|
||||
function listDispenserBrandHints(): string[] {
|
||||
const store = (window as unknown as { H2DispenserBrandStore?: { labels?: () => string[] } })
|
||||
.H2DispenserBrandStore;
|
||||
if (!store || typeof store.labels !== 'function') return [];
|
||||
try {
|
||||
return store.labels().slice(0, 4);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
mode: 'create' | 'edit';
|
||||
draft: CreateDraft;
|
||||
@@ -107,7 +96,6 @@ export function CreateWizard({
|
||||
const [plateOpen, setPlateOpen] = useState(false);
|
||||
|
||||
const vehicleTag = useMemo(() => getVehicleTag(draft.plateNo), [draft.plateNo]);
|
||||
const brandHints = listDispenserBrandHints();
|
||||
|
||||
function patchDraft(patch: Partial<CreateDraft>) {
|
||||
const nextPrice = patch.unitPrice !== undefined ? patch.unitPrice : draft.unitPrice;
|
||||
@@ -380,12 +368,6 @@ export function CreateWizard({
|
||||
<p>请拍摄清晰的加氢机照片</p>
|
||||
</div>
|
||||
</div>
|
||||
{brandHints.length ? (
|
||||
<p className="h5-brand-hint" data-annotation-id="h5-dispenser-brands">
|
||||
OCR 参考品牌型号(站点信息维护):{brandHints.join('、')}
|
||||
{brandHints.length >= 4 ? '…' : ''}
|
||||
</p>
|
||||
) : null}
|
||||
<div
|
||||
className={`h5-shot h5-shot--compact${draft.panelPreviewUrl ? ' h5-shot--filled' : ''}`}
|
||||
role="button"
|
||||
|
||||
38
src/prototypes/oneos-h5-vehicle-assets/.spec/biz-timeline.md
Normal file
38
src/prototypes/oneos-h5-vehicle-assets/.spec/biz-timeline.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 业务记录 · 纵向时间轴
|
||||
|
||||
详情中租赁 / 保险历史 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审等业务记录,统一为**近 → 远纵向时间轴**展示(与租赁记录同一视觉与交互骨架)。
|
||||
|
||||
## 结构
|
||||
|
||||
1. **时间筛选**(事故及右侧 Tab):快捷「全部 / 近30天 / 近90天 / 近1年 / 自定义起止」;按各 Tab 关键时间过滤;无匹配可重置
|
||||
- **自定义起止**:底部原生滚轮日期选择(年 / 月 / 日,精确到日);起日不得晚于止日
|
||||
- 本模块凡需选日的入口,统一用该原生日期选择器,不使用浏览器原生 `date` 控件
|
||||
2. **时间节点**:左侧圆点 + 连线;节点日期取该条业务关键时间(近 → 远)
|
||||
3. **记录卡片**:主标题(多为完整时间)+ 键值行 + 角标状态
|
||||
4. **未闭环强调**:进行中 / 未还车 / 未解决 / 未处理等节点用强调色圆点与日期
|
||||
|
||||
## 各 Tab 关键时间
|
||||
|
||||
| Tab | 节点日期 / 筛选字段 | 卡片要点 |
|
||||
|-----|---------------------|----------|
|
||||
| 租赁 | 交车/还车较近者 | 合同、项目、客户、交还车;未还车文案;客户/项目搜索 |
|
||||
| 保险历史 | 采购时间(无则到期) | 保单号、公司、状态、到期;附件预览/下载 |
|
||||
| 事故 | 事故时间 | 标题为事故时间;下侧:单号、位置、等级、类型、客户、状态 |
|
||||
| 故障 | 上报时间 | 标题为上报时间;下侧:编号、等级、类型、解决;故障说明用只读文本域 |
|
||||
| 违章 | 违章时间 | 标题为违章时间;下侧:行为、位置、缴费、处理、客户 |
|
||||
| 异动 | 开始日 | 标题为开始时间;下侧:类型、目的地、预计结束 |
|
||||
| 调拨 | 调拨日 | 标题为调拨日期;下侧:出发城市、到达城市、停车场、接收人、接收日 |
|
||||
| 年审 | 执行时间 | 标题为执行时间;下侧:有效期至、检测站、执行人 |
|
||||
|
||||
保险 Tab 仍保留总览 + 五险种索引 + 当前摘要;**采购/批单历史**改为时间轴。
|
||||
|
||||
## 数据源
|
||||
|
||||
种子优先;不足时 H5 演示兜底(见 [demo-biz-records.md](./demo-biz-records.md))。
|
||||
|
||||
## 代码路径
|
||||
|
||||
- 通用:`components/RecordTimeline.tsx`、`TimelineDateFilter.tsx`、`NativeDatePickerSheet.tsx`
|
||||
- 租赁:`LeaseRecordsPanel.tsx`
|
||||
- 保险:`InsuranceRecordsPanel.tsx`
|
||||
- 其余:`BizTimelinePanels.tsx`
|
||||
@@ -0,0 +1,25 @@
|
||||
# 业务记录 · 演示用例数据(H5)
|
||||
|
||||
详情中租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审等业务 Tab,在**台账种子无该车记录**时,由 H5 本地按车牌稳定生成演示记录;租赁不足 3 条时补足,便于查看完整时间轴。
|
||||
|
||||
## 判定优先级
|
||||
|
||||
| 顺序 | 条件 | 结果 |
|
||||
|------|------|------|
|
||||
| 1 | PC 同源种子已有该车牌记录且条数足够 | 使用种子(不覆盖) |
|
||||
| 2 | 租赁可由车辆台账交还车字段推导 | 使用推导记录,不足 3 条再补历史演示 |
|
||||
| 3 | 仍为空,或条数不足 3 | 生成 / 补足演示用例(租赁至少 3 条:1 条在租 + 2 条已还车历史) |
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 仅 H5「车辆资产」详情生效;不写入真实 API
|
||||
- 演示字段取自该车台账(客户、项目、城市、停车场、里程等)+ 车牌哈希,换车内容会变、同车刷新稳定
|
||||
|
||||
## 空状态
|
||||
|
||||
无任何可展示记录时(极少):插画空态 + 标题「暂无记录」+ 引导回 PC 录入。
|
||||
|
||||
## 代码路径
|
||||
|
||||
- 兜底:`utils/demo-biz-records.ts`
|
||||
- 空态:`components/RecordsEmpty.tsx`
|
||||
@@ -0,0 +1,31 @@
|
||||
# 检验有效期剩余天数 · 沪牌等级评定
|
||||
|
||||
## 检验有效期 · 剩余天数标签
|
||||
|
||||
| 项 | 口径 |
|
||||
|----|------|
|
||||
| 展示位置 | 基本信息「证照 / 保险概览」· 检验有效期;证照信息「行驶证」· 检验有效期 |
|
||||
| 有有效日期 | 日期后方标签:`剩余x天`;已过期则为 `已过期x天` |
|
||||
| 无日期 | 文案「未上传」,无标签 |
|
||||
| 临期着色 | 剩余 ≤ 30 天:警告色;已过期:危险色;其余:主色浅底 |
|
||||
| 计算 | 到期日相对今天的日历天数差(与 PC 一致) |
|
||||
|
||||
## 沪牌等级评定(仅车牌以「沪」开头)
|
||||
|
||||
| 字段 | 展示 |
|
||||
|------|------|
|
||||
| 等级评定时间 | 日期;空为「未上传」 |
|
||||
| 剩余天数 | `剩余x天` / `已过期x天`;无日期为「—」 |
|
||||
|
||||
- 证照信息 Tab 在行驶证内展示沪牌等级评定(与 PC 一致);基本信息档案区亦有等级评定字段
|
||||
- 非沪牌不展示
|
||||
|
||||
## 数据源
|
||||
|
||||
- 检验有效期:台账检验有效期 / 行驶证检验有效期
|
||||
- 等级评定:台账等级评定时间(空时证照 Tab 可回退行驶证下次评定日)
|
||||
- 原型本地计算剩余天数,未接实时服务端日历接口
|
||||
|
||||
## 代码路径
|
||||
|
||||
- `components/detailTabs.tsx` → `ExpireValue` / `formatRemainDaysLabel` / `isShanghaiPlate`
|
||||
@@ -0,0 +1,37 @@
|
||||
# 保险记录 · 与保险采购对齐
|
||||
|
||||
详情「保险记录」Tab 数据与险种口径对齐 **保险采购 / PC 车辆资产保险记录**。
|
||||
|
||||
## 结构
|
||||
|
||||
1. **车辆保险档案总览**:整体保险状态(正常 / 临期 / 异常)及说明(交强险 + 商业险判定,与保险采购联动口径一致)
|
||||
2. **五险种索引**(单行横滑密排):交强险、商业险、超赔险、驾意险、货物险
|
||||
- 展示险种名 + 简短状态(正常/临期/未购等)+ 历史条数;到期日在下方详情区
|
||||
3. **当前险种详情**
|
||||
- 摘要:保单号、保险公司、到期日期、保费
|
||||
- 历史:纵向时间轴(近→远);节点取采购时间;卡片含保单号、公司、状态、到期、保单附件(预览 / 下载);不展示导入时间与付款时间
|
||||
|
||||
手机端保单附件:
|
||||
- **预览**:壳内底部弹层展示文件名与演示影像(正式环境预览真实 PDF)
|
||||
- **下载**:触发本地下载演示文件并 Toast「已开始下载」
|
||||
- 无附件显示「无附件」
|
||||
|
||||
## 状态口径(摘要)
|
||||
|
||||
| 整体状态 | 条件 |
|
||||
|----------|------|
|
||||
| 正常 | 交强险、商业险均在有效期内(含临期、停保仍可交车语义与 PC 一致时按 PC) |
|
||||
| 临期 | 交强或商业有效但临期 |
|
||||
| 异常 | 交强或商业未购买 / 已到期 / 已退保等 |
|
||||
|
||||
手机端只读录入;保单附件支持预览与下载(原型演示)。某险种无历史时展示带插画的紧凑空态,引导回 PC「保险采购」录入。
|
||||
|
||||
## 数据源
|
||||
|
||||
- `resolveVehicleInsuranceOverview`(保险采购记录 + 车辆保险台账种子)
|
||||
- 原型本地种子,未接正式保险采购写接口
|
||||
|
||||
## 代码路径
|
||||
|
||||
- UI:`components/InsuranceRecordsPanel.tsx`
|
||||
- 数据:`vehicle-management/utils/vehicleInsuranceRecords.ts`
|
||||
@@ -0,0 +1,42 @@
|
||||
# 租赁记录 · 纵向时间轴
|
||||
|
||||
详情「租赁记录」以**时间为主轴**展示,由近到远;支持按客户名称 / 项目名称即时搜索。
|
||||
|
||||
## 结构
|
||||
|
||||
1. **搜索栏**(时间轴上方):提示「搜索客户名称 / 项目名称」;可一键清空
|
||||
2. **纵向时间轴**:左侧圆点 + 连线;节点日期取该条记录交车/还车中较近的日期
|
||||
3. **记录卡片**(节点右侧):
|
||||
- 客户合同编码
|
||||
- 项目名称
|
||||
- **客户**(交车上方,始终展示)
|
||||
- 交车时间
|
||||
- 还车时间;未还车时显示 **未还车**(强调色)
|
||||
- 业务类型角标(租赁 / 物流)
|
||||
|
||||
## 排序
|
||||
|
||||
按节点日期降序(近 → 远)。搜索只过滤可见项,不改变排序规则。
|
||||
|
||||
## 搜索匹配
|
||||
|
||||
| 顺序 | 条件 | 结果 |
|
||||
|------|------|------|
|
||||
| 1 | 关键词为空 | 展示该车全部租赁记录 |
|
||||
| 2 | 客户名称或项目名称包含关键词(不区分大小写) | 保留该条 |
|
||||
| 3 | 均不包含 | 过滤掉;全部无匹配时展示「暂无匹配记录」+ 清空搜索 |
|
||||
|
||||
**前置条件**
|
||||
|
||||
- 该车无任何租赁记录:只展示「暂无记录」,不展示搜索栏
|
||||
- 仅匹配客户名称、项目名称;合同编码等不参与搜索(本期)
|
||||
|
||||
## 数据源
|
||||
|
||||
- `resolveVehicleLeaseRecords`(租赁记录种子;无种子时由车辆台账交还车信息推导)
|
||||
- 原型本地种子,未接真实租赁合同 API
|
||||
|
||||
## 代码路径
|
||||
|
||||
- UI:`components/LeaseRecordsPanel.tsx`
|
||||
- 数据:`vehicle-management/utils/leaseRecords.ts`
|
||||
@@ -0,0 +1,33 @@
|
||||
# 证照信息 · 与证照管理对齐
|
||||
|
||||
详情「证照信息」Tab 证照种类、字段与状态口径对齐 **证照管理 / PC 车辆资产证照 Tab**(八类)。
|
||||
|
||||
## 八类证照
|
||||
|
||||
| 顺序 | 证照 | 展示内容 | 状态角标 |
|
||||
|------|------|----------|----------|
|
||||
| 1 | 行驶证 | 影像 + 注册/发证/强制报废/检验有效期至;沪牌另显下次等级评定时间与剩余天数 | 已上传 / 未上传 |
|
||||
| 2 | 道路运输证 | 影像 + 经营许可证号、核发时间、证件有效期、审验有效期 | 已上传 / 未上传 |
|
||||
| 3 | 登记证 | 影像 | 已上传 / 未上传 |
|
||||
| 4 | 特种设备使用登记证 | 影像 | 已上传 / 未上传 |
|
||||
| 5 | 特种设备使用标识 | 影像 + 下次检验日期 | 已上传 / 未上传 |
|
||||
| 6 | 加氢卡 | 卡号/余额/配发信息;未绑定文案「尚未绑定加氢卡」 | 已绑定 / 未绑定 |
|
||||
| 7 | 安全阀 | 影像 + 本次检验日期、下次检测日期 | 已上传 / 未上传 |
|
||||
| 8 | 压力表 | 影像 + 本次检验日期、下次检验日期 | 已上传 / 未上传 |
|
||||
|
||||
顶部提供八类名称**单行横向滚动**索引;点选后自动锚点滚动到对应证照区块,选中项高亮。
|
||||
|
||||
## 日期与剩余天数
|
||||
|
||||
有明确到期/检验日的字段,日期后方可展示「剩余 x 天 / 已过期 x 天」(与检验有效期规则一致)。
|
||||
|
||||
## 数据源
|
||||
|
||||
- 与 PC 车辆资产同一套证照包解析(原型种子 + 台账回填)
|
||||
- 影像为原型演示样例图;正式环境以证照管理上传为准
|
||||
- 手机端只读,不提供编辑 / 跳转证照管理
|
||||
|
||||
## 代码路径
|
||||
|
||||
- UI:`components/LicenseCertPanel.tsx`
|
||||
- 数据:`vehicle-management/utils/vehicleLicenses.ts` → `LICENSE_CERT_NAV` / `resolveVehicleLicenseBundle`
|
||||
77
src/prototypes/oneos-h5-vehicle-assets/.spec/mileage-task.md
Normal file
77
src/prototypes/oneos-h5-vehicle-assets/.spec/mileage-task.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 列表卡片 · 里程任务进度
|
||||
|
||||
> 原型本地演示种子:[`utils/mileage-task.ts`](../utils/mileage-task.ts)
|
||||
> **未接真实采购合同 / 任务工单 API**
|
||||
|
||||
## 业务背景
|
||||
|
||||
采购部发起车辆采购合同时,合同可关联对应批次车辆的**里程要求**与**时间起止**;里程任务通过**任务工单**下发到车辆。手机列表卡片展示该车当前里程任务完成情况,便于外勤扫一眼进度与是否赶得上截止。
|
||||
|
||||
## 展示位置
|
||||
|
||||
车辆卡片中,在「来源 / 客户名」等标签行**下方**,全宽分隔区:
|
||||
|
||||
1. 标题「里程任务」(有任务时右侧展示完成百分比)
|
||||
2. 有任务:进度条
|
||||
3. 有任务:剩余公里 + 按近 7 活跃日推算的预计完成天数
|
||||
4. 进行中任务:文案后方完成预判标签(对照任务截止剩余日历天)
|
||||
|
||||
## 判定 / 展示优先级
|
||||
|
||||
| 顺序 | 条件 | 结果 |
|
||||
|------|------|------|
|
||||
| 1 | 车辆未挂接有效里程任务工单 | 展示标题 +「暂无里程任务」(无进度条、无剩余/预计、无预判标签) |
|
||||
| 2 | 已完成里程 ≥ 目标里程(或进度 ≥ 100%) | 进度 100%、成功色进度条与百分比;文案「已完成」;**不**展示预判标签 |
|
||||
| 3 | 有任务且未完成,且近 7 个活跃日均里程可算 | 进度条 +「剩余 N km · 预计 D 天内完成」+ 预判标签;预判为「无法完成」时进度条/百分比为危险红;「有完成风险」为警告橙 |
|
||||
| 4 | 有任务且未完成,但无活跃日(近窗内日里程均 ≤ 0) | 进度条(警告橙)+「剩余 N km · 暂无法预计」+ 预判标签「剩余 x 天,预计有完成风险」 |
|
||||
|
||||
## 口径公式
|
||||
|
||||
| 项 | 口径 |
|
||||
|----|------|
|
||||
| 完成百分比 | `min(100, 保留 1 位小数(任务内已完成里程 / 合同目标里程 × 100))` |
|
||||
| 剩余公里 | `max(0, 目标里程 − 已完成里程)` |
|
||||
| 活跃日 | 自然日内该车行驶里程 **> 0 km** |
|
||||
| 近 7 活跃日均 | 取最近最多 **7** 个活跃日的日里程算术平均;不足 7 个则有几个算几个 |
|
||||
| 预计天数 D | `ceil(剩余公里 / 近 7 活跃日均)`;均里程为 0 或无活跃日 → 无法预计 |
|
||||
| 截止剩余日历天 x | 任务/合同时间窗截止日相对今天的剩余自然日(下限 0) |
|
||||
|
||||
## 完成预判标签(文案后方)
|
||||
|
||||
用预计天数 D(近 7 活跃日推算)对照截止剩余日历天 x:
|
||||
|
||||
| 顺序 | 条件 | 标签文案 | 色调 | 进度条 / 百分比 |
|
||||
|------|------|----------|------|----------------|
|
||||
| 1 | D 无法算出(无活跃日) | 剩余 x 天,预计有完成风险 | 警告橙 | 同警告橙 |
|
||||
| 2 | x ≤ 0 或 D > x | 剩余 x 天,预计无法完成 | 危险红 | 同危险红 |
|
||||
| 3 | D > x × 85%,或 (x − D) ≤ 7 | 剩余 x 天,预计有完成风险 | 警告橙 | 同警告橙 |
|
||||
| 4 | 其余 | 剩余 x 天,预计可完成 | 成功绿 | 默认主色蓝 |
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 仅列表卡片只读展示;不提供下发、编辑、跳转工单详情(本期不做)
|
||||
- 筛选 / KPI / 搜索不影响本区展示规则,只影响哪些车出现在列表
|
||||
|
||||
## 数据源(原型)
|
||||
|
||||
| 来源 | 说明 |
|
||||
|------|------|
|
||||
| 演示种子 | 按车牌稳定 hash:约 65% 挂接演示任务(目标里程、已完成、近窗活跃日里程序列、截止剩余日历天);其余为无任务 |
|
||||
| 台账导表 | 仅借用车牌标识做稳定种子;**不**把导表「当前总里程」直接当作任务进度真相 |
|
||||
|
||||
正式环境应由采购合同批次里程要求 + 时间起止 + 任务工单执行回写提供目标 / 已完成 / 日里程明细 / 截止日。
|
||||
|
||||
## 用户可见文案
|
||||
|
||||
| 场景 | 文案 |
|
||||
|------|------|
|
||||
| 无任务 | 暂无里程任务 |
|
||||
| 进行中 | 剩余 {N} km · 预计 {D} 天内完成 |
|
||||
| 进行中且无法预计 | 剩余 {N} km · 暂无法预计 |
|
||||
| 预判标签 | 剩余 {x} 天,预计无法完成 / 预计可完成 / 预计有完成风险 |
|
||||
| 已完成 | 已完成(无预判标签) |
|
||||
|
||||
## 代码路径
|
||||
|
||||
- 解析:`utils/mileage-task.ts` → `resolveMileageTaskCard` / `resolveMileageForecast`
|
||||
- 展示:`components/VehicleCard.tsx` + `styles/index.css`(`.h5-va-mileage-task*` / `.h5-va-mileage-forecast`)
|
||||
38
src/prototypes/oneos-h5-vehicle-assets/.spec/more-filters.md
Normal file
38
src/prototypes/oneos-h5-vehicle-assets/.spec/more-filters.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 更多筛选 · 判定规格
|
||||
|
||||
「更多筛选」底部面板内的条件与列表筛选项行(品牌车型 / 运营城市 / 车辆状态)叠加生效;任一非「全部」即点亮更多筛选角标。
|
||||
|
||||
## 面板项与匹配口径
|
||||
|
||||
| 优先级 | 筛选项 | 选项 | 匹配规则 | 数据源 |
|
||||
|--------|--------|------|----------|--------|
|
||||
| 1 | 证照状态 | 全部 / 正常 / 异常 | 异常 = 检验有效期早于今天;正常 = 非异常 | 台账证照有效期 |
|
||||
| 2 | 保险状态 | 全部 / 正常 / 异常 | 异常 = 保险状态为「异常」;正常 = 非异常 | 台账保险状态 |
|
||||
| 3 | 出库状态 | 全部 / 未出库 / 已出库 | 见下表(由车辆状态推导,无独立出库字段) | 台账车辆状态 |
|
||||
| 4 | 车辆来源 | 全部 / 自有 / 外租 / 挂靠 | 精确等于台账「车辆来源」 | 台账车辆来源 |
|
||||
| 5 | 在线状态 | 全部 / 在线 / 离线 | 精确等于台账「在线状态」 | 台账在线状态 |
|
||||
|
||||
以上条件与 KPI、搜索、品牌车型、运营城市、车辆状态为 **AND**。
|
||||
|
||||
## 出库状态推导(原型本地)
|
||||
|
||||
| 出库选项 | 车辆状态集合 |
|
||||
|----------|----------------|
|
||||
| 未出库 | 待验车、未备车、已备车、待交车 |
|
||||
| 已出库 | 有明确车辆状态,且不属于「未出库」集合(如已交车、待还车、异动中等) |
|
||||
|
||||
**前置条件**
|
||||
|
||||
- 车辆状态为空、`-`、`无`:不匹配「未出库」也不匹配「已出库」(选任一侧时该车被滤掉)
|
||||
- 正式环境若上线独立「出库」字段,以字段为准,本表仅作原型推导
|
||||
|
||||
## 交互与布局
|
||||
|
||||
- 面板挂在手机壳操作区内,宽度不超过壳层;内容区可滚动,底部「重置 / 完成」固定
|
||||
- 「重置」只清更多筛选五项,不清筛选项行与 KPI / 搜索
|
||||
- 「完成」关闭面板并保留当前选择;列表已随选择即时刷新
|
||||
|
||||
## 代码路径(实现对照)
|
||||
|
||||
- 匹配:`utils/filter.ts` → `matchOutboundFilter` / `matchVehicleSourceFilter` / `matchOnlineStatusFilter` 等
|
||||
- UI:`components/FilterSheet.tsx`
|
||||
276
src/prototypes/oneos-h5-vehicle-assets/.spec/requirements-prd.md
Normal file
276
src/prototypes/oneos-h5-vehicle-assets/.spec/requirements-prd.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# 车辆资产(H5)· 产品需求说明(全模块)
|
||||
|
||||
## 1. 一句话与目标
|
||||
|
||||
**一句话**
|
||||
在 App / 手机浏览器提供与 PC「车辆资产」口径一致的只读查车与异常浏览能力。
|
||||
|
||||
**要解决的问题**
|
||||
- 外勤与业务人员无法在手机上快速按车牌查车、看营运分类与证照/保险异常
|
||||
- PC 宽表页不适合塞进 WebView,需要独立手机信息架构
|
||||
|
||||
**本期目标**
|
||||
1. 列表:车牌搜索、KPI 分类横滑、证照/保险异常筛选、车辆卡片(含里程任务进度)
|
||||
2. 详情:多 Tab 对齐 PC,全程只读浏览
|
||||
3. 全角色同一套界面;手机端不做编辑
|
||||
|
||||
**非目标(本期不做)**
|
||||
- 任意字段编辑(含 Admin)
|
||||
- 导入 / 导出、催办、跳转证照管理或保险采购等业务处理页
|
||||
- 里程任务工单下发 / 编辑、从卡片跳转工单详情、按截止日逾期标红
|
||||
- 在租车型占比等 PC 尾部分析入口
|
||||
- 将 PC 页做响应式自适应
|
||||
|
||||
## 2. 模块边界
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
App[App WebView] --> H5[车辆资产 H5]
|
||||
H5 -.口径一致.-> PC[车辆资产 PC]
|
||||
H5 -.只读.-> Data[车辆台账与关联记录]
|
||||
```
|
||||
|
||||
| 业务线 / 子域 | 做什么 | 不做什么 |
|
||||
|---------------|--------|----------|
|
||||
| 查车列表 | 搜索、KPI、异常筛选、卡片进详情、卡片里程任务进度 | 批量操作、写操作、下发/编辑工单 |
|
||||
| 车辆详情 | 11 个 Tab 只读浏览 | 行内编辑、跳转处理 |
|
||||
|
||||
**外部依赖(产品口径)**
|
||||
|
||||
| 依赖方 | 交互方式 | 产品要求 |
|
||||
|--------|----------|----------|
|
||||
| 车辆资产 PC | 同一套台账与统计口径 | KPI / 证照异常 / 保险异常定义一致;台账数据同源(业务导表) |
|
||||
| 采购合同 / 任务工单 | 合同关联批次里程要求与起止;工单下发后回写进度 | 列表卡片只读展示完成百分比、剩余公里与预计天数;无工单时仍展示「暂无里程任务」 |
|
||||
| App 壳 | 内嵌打开本 H5 | 固定打开手机页,不打开 PC 宽表地址 |
|
||||
|
||||
**关键约束**
|
||||
- 手机端明确提示:修改请在 PC 完成
|
||||
- 筛选与 KPI 叠加:先按 KPI,再按证照/保险条件
|
||||
- 列表车辆数据与 PC「车辆资产」共用业务导表台账(含真实车牌、客户、运营状态分布)
|
||||
|
||||
## 3. 用户与角色
|
||||
|
||||
| 角色 | 主要目标 |
|
||||
|------|----------|
|
||||
| 运维 / 业务 / 管理等全角色 | 同一套界面查车、看异常与档案摘要 |
|
||||
| Admin | 与普通用户相同;手机端仍无编辑(改数回 PC) |
|
||||
|
||||
## 4. 用户故事与故事点(业务条线说明口径)
|
||||
|
||||
合计约 **15 SP**。
|
||||
|
||||
### Epic A · 列表查车(约 10 SP)
|
||||
|
||||
#### A1 · 车牌搜索与卡片列表
|
||||
- **角色**:全体手机用户
|
||||
- **起点**:从 App 进入车辆资产 H5
|
||||
- **怎么运作**:
|
||||
1. 顶栏左对齐「车辆资产」;搜索栏提示「搜索车牌 / VIN / 停车场 / 客户名称」,支持按这些关键词过滤;右侧扫码为占位提示
|
||||
2. KPI 横滑摘要卡(每屏 3 张、共 2 页;图标 + 计数 + 占比);下方固定筛选项行:品牌车型 / 运营城市(省→市,可只选省)/ 车辆状态 +「更多筛选」(证照 / 保险 / 出库 / 来源 / 在线);前三项为 App 原生底部选择器,选项来自业务导表;更多筛选面板限制在手机操作区内并可滚动
|
||||
3. 列表横卡含车牌、车型、来源标签、按运营状态变化的次要标签(租赁/自营→客户名省略;库存→未备车警告/已备车正常;非运营→非运营车辆;退出→三方退租/销售/报废)、营运状态圆点、里程与异常角标(不含车辆照片);标签下方展示「里程任务」分隔区(有任务:百分比 + 进度条 + 剩余公里/预计天数 + 完成预判标签;无任务:「暂无里程任务」);列表不展示总车辆数,不提供翻页按钮,滚动触底异步加载更多
|
||||
4. 底部 App Tab 为单条悬浮胶囊壳层预览:车辆 / 业务 / 工作台(中间圆形仅图标)/ 消息 / 我的;非车辆入口仅提示;点卡片进详情
|
||||
- **闭环**:找到目标车并进入详情
|
||||
- **排期**:US-01 · M
|
||||
|
||||
#### A2 · KPI 分类与异常筛选
|
||||
- **角色**:运维 / 业务
|
||||
- **起点**:需要按营运分类或证照/保险异常缩小范围
|
||||
- **怎么运作**:
|
||||
1. 横滑点选 KPI 摘要卡(全部营运/租赁/物流/库存/非运营/退出;每屏完整显示 3 张,翻页看其余 3 张;显示计数与相对占比)
|
||||
2. 筛选项行:品牌车型(品牌→车型)、运营城市(省→市,可只选省)、车辆状态(与 PC 字典一致,含调拨中 / 替换中等;无数据时计数为 0 仍可选);点「更多筛选」可再调证照状态、保险状态、出库状态、车辆来源、在线状态
|
||||
3. 列表随条件刷新并从顶部重新加载;不展示列表总辆数
|
||||
- **闭环**:得到与 PC 同口径的筛选结果
|
||||
- **排期**:US-02 · M
|
||||
|
||||
#### A3 · 卡片里程任务进度
|
||||
- **角色**:运维 / 业务 / 管理
|
||||
- **起点**:采购合同已为批次车辆关联里程要求与起止时间,并通过任务工单下发
|
||||
- **怎么运作**:
|
||||
1. 列表每张卡片固定展示「里程任务」区
|
||||
2. 有工单:展示完成百分比、进度条、剩余公里;预计完成天数按近 7 个活跃日(日里程 > 0)平均日里程推算;文案后方展示完成预判标签(对照任务截止剩余日历天:预计无法完成 / 可完成 / 有完成风险)
|
||||
3. 无工单:仍保留区块,文案「暂无里程任务」
|
||||
- **闭环**:不进详情也能判断该车里程任务是否在轨、还差多少
|
||||
- **排期**:US-04 · S
|
||||
|
||||
### Epic B · 详情只读(约 5 SP)
|
||||
|
||||
#### B1 · 多 Tab 档案浏览
|
||||
- **角色**:全体手机用户
|
||||
- **起点**:从列表进入某车详情
|
||||
- **怎么运作**:
|
||||
1. 详情与列表共用一体浅蓝渐变底;顶栏透明,仅返回 + 车牌;不挂底部悬浮 Tab
|
||||
2. 摘要为浮起白卡:车牌、品牌型号、营运状态(与列表同文案/色点)、来源·城市·项目等标签、里程、证照/保险异常角标(无车辆实景大图)
|
||||
3. 横滑切换与 PC 一致的 11 个 Tab;选中态为浅底主色字(对齐列表 Chip)
|
||||
4. 各 Tab 仅展示信息(白卡 Section / 记录列表),无编辑按钮;检验有效期后方展示「剩余 x 天」标签;沪牌车在档案信息额外展示等级评定时间与剩余天数;证照信息 Tab 对齐证照管理八类证照;保险记录 Tab 对齐保险采购五险种与采购历史;租赁记录为纵向时间轴(近→远,未还车标注),并支持按客户名称 / 项目名称搜索;事故等业务 Tab 无种子时展示演示用例;无数据时为空态插画
|
||||
- **闭环**:不回 PC 也能看完档案与业务记录摘要
|
||||
- **排期**:US-03 · M
|
||||
|
||||
## 5. 功能模块说明(正向 / 逆向)
|
||||
|
||||
### 5.1 列表
|
||||
|
||||
**正向**
|
||||
1. 默认展示「全部营运」KPI 下的卡片列表(首屏一批,触底异步加载更多;不含退出运营、不含非运营车辆)
|
||||
2. 搜索、切 KPI、改筛选后列表刷新并从顶部重新加载;不展示总车辆数、无翻页按钮
|
||||
3. 证照/保险异常在卡片上可见
|
||||
4. 每张卡片标签下方展示「里程任务」:有工单则百分比 + 进度条 + 剩余/预计 + 完成预判标签;无工单则「暂无里程任务」
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| 无匹配 | 展示「暂无匹配车辆」+ 引导文案;可清空搜索或重置筛选 |
|
||||
| 仅筛选异常 | 只保留对应异常车辆 |
|
||||
| 更多筛选多项叠加 | 与 KPI / 搜索 / 筛选项行条件同时生效(AND) |
|
||||
| 已加载全部 | 底部提示「没有更多了」 |
|
||||
| 里程任务已完成 | 进度 100%,文案「已完成」,无预判标签 |
|
||||
| 有任务但近窗无活跃日 | 仍显示剩余公里,预计侧「暂无法预计」,预判标签为「剩余 x 天,预计有完成风险」 |
|
||||
| 按活跃日推算赶不上截止 | 预判标签「剩余 x 天,预计无法完成」 |
|
||||
|
||||
### 5.2 详情
|
||||
|
||||
**正向**
|
||||
1. 进入详情后背景与列表一体连续;摘要卡可见状态、标签、里程与异常
|
||||
2. 11 个 Tab 均可切换;内容区白卡展示字段或记录
|
||||
3. 检验有效期后方展示剩余/已过期天数标签;沪牌车额外展示等级评定时间与剩余天数
|
||||
4. 证照信息含八类证照(行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表),字段与证照管理对齐;顶部索引单行横滑点选锚点
|
||||
5. 保险记录对齐保险采购:五险种卡片 + 当前保单摘要 + 采购/批单历史(纵向时间轴,只读)
|
||||
6. 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审均为纵向时间轴(近→远);事故及右侧 Tab 支持时间快捷/自定义筛选(自定义为手机端原生滚轮选日);调拨卡片展示出发城市、到达城市、停车场、接收人、接收日(不展示路线与调出人);租赁可搜客户/项目,未还车显示「未还车」
|
||||
7. 业务 Tab 无种子时展示演示用例;租赁不足 3 条时补足
|
||||
8. 无记录时展示美化空态;无底部 App Tab,避免挡内容
|
||||
|
||||
**逆向 / 边界**
|
||||
|
||||
| 情况 | 系统表现 |
|
||||
|------|----------|
|
||||
| 任意角色 | 均无编辑、删除、导入入口 |
|
||||
| 返回 | 顶栏返回或系统返回回到列表;列表底栏样式不受详情影响 |
|
||||
| 检验有效期为空 | 显示「未上传」,无剩余天数标签 |
|
||||
| 非沪牌 | 不展示等级评定时间 / 剩余天数 |
|
||||
| 未绑定加氢卡 | 显示「尚未绑定加氢卡」 |
|
||||
| 某险种无采购记录 | 显示「暂无记录」,引导回 PC 保险采购 |
|
||||
| 租赁未还车 | 还车字段显示「未还车」 |
|
||||
| 租赁搜索无匹配 | 展示「暂无匹配记录」,可清空搜索 |
|
||||
| 业务 Tab 无种子 | 展示稳定演示用例(非真实业务单据) |
|
||||
|
||||
## 6. 关键业务逻辑
|
||||
|
||||
### 6.1 KPI 口径(与 PC 一致)
|
||||
|
||||
| KPI | 口径 |
|
||||
|-----|------|
|
||||
| 全部营运(原「所有营运车辆」) | 非「非运营车辆」,且运营状态不为「退出运营」 |
|
||||
| 租赁 / 物流 | 运营状态等于对应值 |
|
||||
| 库存 | 可运营或待运营 |
|
||||
| 非运营 | 台账类型为非运营车辆 |
|
||||
| 退出运营 | 运营状态为退出运营 |
|
||||
|
||||
### 6.2 异常口径
|
||||
|
||||
| 异常 | 口径 |
|
||||
|------|------|
|
||||
| 证照异常 | 检验有效期早于今天 |
|
||||
| 保险异常 | 保险状态为「异常」 |
|
||||
|
||||
### 6.3 更多筛选口径
|
||||
|
||||
| 筛选项 | 口径 |
|
||||
|--------|------|
|
||||
| 证照 / 保险 | 同 §6.2 |
|
||||
| 出库状态 | 由车辆状态推导:未出库 = 待验车/未备车/已备车/待交车;已出库 = 其余有状态车辆(原型无独立出库字段) |
|
||||
| 车辆来源 | 自有 / 外租 / 挂靠,与台账一致 |
|
||||
| 在线状态 | 在线 / 离线,与台账一致 |
|
||||
|
||||
规格全文见 [more-filters.md](./more-filters.md)。
|
||||
|
||||
### 6.4 里程任务进度(列表卡片)
|
||||
|
||||
| 项 | 口径 |
|
||||
|----|------|
|
||||
| 来源 | 采购合同批次里程要求 + 时间起止 + 任务工单下发(原型为本地演示种子,未接真实 API) |
|
||||
| 完成百分比 | 任务内已完成里程 / 目标里程,封顶 100%,展示保留 1 位小数 |
|
||||
| 剩余公里 | 目标 − 已完成,下限 0 |
|
||||
| 活跃日 | 当日行驶里程 > 0 km |
|
||||
| 预计天数 D | 剩余 ÷ 近最多 7 个活跃日平均日里程,向上取整;无活跃日则「暂无法预计」 |
|
||||
| 截止剩余日历天 x | 任务时间窗相对今天的剩余自然日 |
|
||||
| 预判标签 | 用 D 对照 x:无法完成 / 有完成风险 / 可完成(见规格全文);无法完成时进度条与百分比为危险红,有完成风险为警告橙 |
|
||||
| 无工单 | 仍展示区块,文案「暂无里程任务」 |
|
||||
|
||||
规格全文见 [mileage-task.md](./mileage-task.md)。
|
||||
|
||||
### 6.5 检验有效期剩余天数 · 沪牌等级评定
|
||||
|
||||
| 项 | 口径 |
|
||||
|----|------|
|
||||
| 检验有效期 | 日期后方标签:剩余 x 天 / 已过期 x 天;空为「未上传」 |
|
||||
| 沪牌判定 | 车牌以「沪」开头 |
|
||||
| 等级评定时间 | 仅沪牌展示;空为「未上传」 |
|
||||
| 剩余天数 | 仅沪牌展示;相对等级评定日计算 |
|
||||
|
||||
规格全文见 [expire-remain-shanghai.md](./expire-remain-shanghai.md)。
|
||||
|
||||
### 6.6 证照信息(八类)
|
||||
|
||||
与证照管理一致:行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表。规格全文见 [license-certs.md](./license-certs.md)。
|
||||
|
||||
### 6.7 保险记录(对齐保险采购)
|
||||
|
||||
五险种(交强 / 商业 / 超赔 / 驾意 / 货物)+ 当前保单摘要 + 采购历史(纵向时间轴)。规格全文见 [insurance-records.md](./insurance-records.md)。
|
||||
|
||||
### 6.8 业务记录纵向时间轴
|
||||
|
||||
租赁 / 保险历史 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审统一近→远时间轴。
|
||||
|
||||
| 能力 | 口径 |
|
||||
|------|------|
|
||||
| 时间筛选 | 事故及右侧 Tab:全部 / 近30天 / 近90天 / 近1年 / 自定义;按各 Tab 关键时间过滤;无匹配可重置 |
|
||||
| 自定义选日 | 手机端底部滚轮(年 / 月 / 日,精确到日);起日不得晚于止日;本模块选日入口统一该交互 |
|
||||
| 调拨卡片 | 出发城市、到达城市、停车场、接收人、接收日;不展示「路线」合并行与「调出」人 |
|
||||
|
||||
规格全文见 [biz-timeline.md](./biz-timeline.md)、[lease-records.md](./lease-records.md)。
|
||||
|
||||
### 6.9 业务记录演示用例与空态
|
||||
|
||||
无种子时 H5 本地生成演示用例;真正无数据时展示美化空态。规格全文见 [demo-biz-records.md](./demo-biz-records.md)。
|
||||
|
||||
## 7. 总览流程图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
entry[App 打开车辆资产 H5] --> list[列表:搜索 / KPI / 筛选]
|
||||
list --> card[点车辆卡片]
|
||||
card --> detail[详情摘要]
|
||||
detail --> tabs[切换只读 Tab]
|
||||
tabs --> back[返回列表]
|
||||
```
|
||||
|
||||
## 8. 验收清单(产品 / 测试)
|
||||
|
||||
- [ ] 独立原型可打开,桌面 PhoneShell / 窄屏可用
|
||||
- [ ] KPI 六类计数与同数据 PC 一致
|
||||
- [ ] 证照异常、保险异常筛选结果与 PC 规则一致
|
||||
- [ ] 「更多筛选」含证照 / 保险 / 出库 / 来源 / 在线;面板不超出手机操作区,内容可滚动;启用后角标点亮
|
||||
- [ ] 出库:未出库对应待验车/未备车/已备车/待交车;来源与在线与台账字段一致
|
||||
- [ ] 车辆状态选项与 PC 字典一致,含「调拨中」「替换中」
|
||||
- [ ] 搜索支持车牌 / VIN / 停车场 / 客户名称过滤列表;可一键清空搜索
|
||||
- [ ] 无匹配时有引导操作(清空搜索 / 重置筛选)
|
||||
- [ ] 列表无总辆数、无翻页按钮;滚动触底可异步加载更多,到底提示「没有更多了」
|
||||
- [ ] 列表进详情:一体渐变底连续,无白顶栏色带;摘要为浮起白卡(状态/标签/里程/异常与列表同语义);详情无底栏
|
||||
- [ ] 详情检验有效期后方有「剩余 x 天」或「已过期 x 天」标签;空为「未上传」
|
||||
- [ ] 沪牌详情可见「等级评定时间」「剩余天数」;非沪牌不出现
|
||||
- [ ] 证照信息 Tab 可见八类证照,与证照管理种类一致
|
||||
- [ ] 保险记录可见五险种卡片、当前保单摘要与历史时间轴;整体状态口径与保险采购一致
|
||||
- [ ] 保单附件可预览与下载(原型演示)
|
||||
- [ ] 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审为近→远时间轴;租赁未还车显示「未还车」
|
||||
- [ ] 调拨卡片可见出发城市、到达城市、停车场、接收人、接收日;无「路线」「调出」
|
||||
- [ ] 事故及右侧业务 Tab 可按时间快捷或自定义区间筛选;自定义起止为底部滚轮选日(精确到日),无匹配可重置
|
||||
- [ ] 租赁记录可按客户名称 / 项目名称搜索;无匹配可清空搜索
|
||||
- [ ] 任意车辆打开上述业务 Tab 可见列表(种子或演示用例);空态有插画与说明
|
||||
- [ ] 详情 11 个 Tab 可进入且只读,无编辑按钮
|
||||
- [ ] 列表卡片标签下有「里程任务」区:有任务可见百分比、进度条、剩余公里与预计天数;无任务显示「暂无里程任务」
|
||||
- [ ] 预计天数按近 7 活跃日均里程推算;无活跃日时显示「暂无法预计」
|
||||
- [ ] 进行中任务文案后方有完成预判标签(剩余 x 天 + 预计无法完成 / 可完成 / 有完成风险)
|
||||
- [ ] 页脚说明「修改请在 PC」可见
|
||||
|
||||
**交付口径**
|
||||
本期交付可嵌入 App 的车辆资产只读 H5:能查、能筛异常、能看详情全 Tab,并在列表卡片扫读里程任务进度;写操作与异常处理仍回 PC。
|
||||
293
src/prototypes/oneos-h5-vehicle-assets/annotation-source.json
Normal file
293
src/prototypes/oneos-h5-vehicle-assets/annotation-source.json
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,37 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
type ToastState = { id: number; message: string } | null;
|
||||
|
||||
let pushToast: ((message: string) => void) | null = null;
|
||||
|
||||
export function showAppToast(message: string) {
|
||||
pushToast?.(message);
|
||||
}
|
||||
|
||||
export function AppToastHost() {
|
||||
const [toast, setToast] = useState<ToastState>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timer = 0;
|
||||
pushToast = (message: string) => {
|
||||
const id = Date.now();
|
||||
setToast({ id, message });
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => {
|
||||
setToast((current) => (current?.id === id ? null : current));
|
||||
}, 1800);
|
||||
};
|
||||
return () => {
|
||||
pushToast = null;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!toast) return null;
|
||||
|
||||
return (
|
||||
<div className="h5-va-toast" role="status" aria-live="polite">
|
||||
{toast.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { VehicleRecord } from '../utils/filter';
|
||||
import { resolveVehicleAccidentRecords } from '../../vehicle-management/utils/accidentRecords';
|
||||
import { resolveVehicleFaultRecords } from '../../vehicle-management/utils/faultRecords';
|
||||
import { resolveVehicleViolationRecords } from '../../vehicle-management/utils/violationRecords';
|
||||
import { resolveVehicleMovementRecords } from '../../vehicle-management/utils/movementRecords';
|
||||
import { resolveVehicleTransferRecords } from '../../vehicle-management/utils/transferRecords';
|
||||
import { resolveVehicleAnnualReviewRecords } from '../../vehicle-management/utils/annualReviewRecords';
|
||||
import {
|
||||
ensureAccidentRecords,
|
||||
ensureAnnualReviewRecords,
|
||||
ensureFaultRecords,
|
||||
ensureMovementRecords,
|
||||
ensureTransferRecords,
|
||||
ensureViolationRecords,
|
||||
} from '../utils/demo-biz-records';
|
||||
import { RecordsEmpty } from './RecordsEmpty';
|
||||
import {
|
||||
RecordTimeline,
|
||||
sortByDateDesc,
|
||||
timelineDatePart,
|
||||
type TimelineItem,
|
||||
} from './RecordTimeline';
|
||||
import {
|
||||
matchTimelineDate,
|
||||
useTimelineDateFilter,
|
||||
} from './TimelineDateFilter';
|
||||
|
||||
function PanelShell({
|
||||
label,
|
||||
emptyDesc,
|
||||
items,
|
||||
dateFieldLabel,
|
||||
}: {
|
||||
label: string;
|
||||
emptyDesc: string;
|
||||
items: TimelineItem[];
|
||||
dateFieldLabel: string;
|
||||
}) {
|
||||
const { range, filterActive, filterNode, reset } = useTimelineDateFilter(dateFieldLabel);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => items.filter((item) => matchTimelineDate(item.date, range)),
|
||||
[items, range],
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
return <RecordsEmpty title="暂无记录" desc={emptyDesc} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h5-va-tl-panel h5-va-section--records" aria-label={label}>
|
||||
{filterNode}
|
||||
{!filtered.length ? (
|
||||
<div className="h5-va-empty h5-va-empty--inset" role="status">
|
||||
<p className="h5-va-empty-title">暂无匹配记录</p>
|
||||
<p className="h5-va-empty-desc">调整时间范围,或重置筛选查看全部</p>
|
||||
{filterActive ? (
|
||||
<div className="h5-va-empty-actions">
|
||||
<button type="button" className="h5-btn h5-btn--primary" onClick={reset}>
|
||||
重置时间筛选
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<RecordTimeline items={filtered} label={`${label}时间轴`} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccidentRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const items = useMemo(() => {
|
||||
const rows = sortByDateDesc(
|
||||
ensureAccidentRecords(record, resolveVehicleAccidentRecords(record)),
|
||||
(row) => timelineDatePart(row.accidentTime),
|
||||
);
|
||||
return rows.map((row): TimelineItem => ({
|
||||
id: row.id,
|
||||
date: timelineDatePart(row.accidentTime) || '—',
|
||||
accent: row.status === '处理中',
|
||||
badge: row.status,
|
||||
title: row.accidentTime || '—',
|
||||
meta: [
|
||||
{ label: '单号', value: row.accidentCode || '—' },
|
||||
{ label: '位置', value: row.accidentLocation || '—' },
|
||||
{ label: '等级', value: row.accidentLevel || '—' },
|
||||
{ label: '类型', value: row.accidentType || '—' },
|
||||
...(row.customerName ? [{ label: '客户', value: row.customerName }] : []),
|
||||
],
|
||||
}));
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
label="事故记录"
|
||||
emptyDesc="该车辆暂无事故记录,可回 PC 录入后同步查看"
|
||||
dateFieldLabel="事故时间"
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FaultRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const items = useMemo(() => {
|
||||
const rows = sortByDateDesc(
|
||||
ensureFaultRecords(record, resolveVehicleFaultRecords(record)),
|
||||
(row) => timelineDatePart(row.reportedAt),
|
||||
);
|
||||
return rows.map((row): TimelineItem => {
|
||||
const open = row.resolutionStatus !== '已解决';
|
||||
const description = (row.faultDescription || '').trim() || '—';
|
||||
return {
|
||||
id: row.id,
|
||||
date: timelineDatePart(row.reportedAt) || '—',
|
||||
accent: open,
|
||||
badge: row.resolutionStatus || '未解决',
|
||||
title: row.reportedAt || '—',
|
||||
meta: [
|
||||
{ label: '编号', value: row.faultNo || '—' },
|
||||
{ label: '等级', value: row.faultLevel || '—' },
|
||||
{ label: '类型', value: row.faultType || '—' },
|
||||
...(row.resolvedAt
|
||||
? [{ label: '解决', value: row.resolvedAt }]
|
||||
: [{ label: '解决', value: '未解决', pending: true }]),
|
||||
],
|
||||
footer: (
|
||||
<div className="h5-va-tl-fieldblock">
|
||||
<div className="h5-va-tl-fieldblock-label">故障说明</div>
|
||||
<textarea
|
||||
className="h5-va-tl-textarea"
|
||||
readOnly
|
||||
rows={3}
|
||||
value={description}
|
||||
aria-label="故障说明"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
label="故障记录"
|
||||
emptyDesc="该车辆暂无故障记录,可回 PC 录入后同步查看"
|
||||
dateFieldLabel="上报时间"
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ViolationRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const items = useMemo(() => {
|
||||
const rows = sortByDateDesc(
|
||||
ensureViolationRecords(record, resolveVehicleViolationRecords(record)),
|
||||
(row) => timelineDatePart(row.violationTime),
|
||||
);
|
||||
return rows.map((row): TimelineItem => ({
|
||||
id: row.id,
|
||||
date: timelineDatePart(row.violationTime) || '—',
|
||||
accent: row.processed === '未处理' || row.paymentStatus === '未缴费',
|
||||
badge: row.processed,
|
||||
title: row.violationTime || '—',
|
||||
meta: [
|
||||
{ label: '行为', value: row.violationBehavior || '—' },
|
||||
{ label: '位置', value: row.violationLocation || '—' },
|
||||
{ label: '缴费', value: row.paymentStatus || '—', pending: row.paymentStatus === '未缴费' },
|
||||
{ label: '处理', value: row.processed || '—', pending: row.processed === '未处理' },
|
||||
...(row.customerName ? [{ label: '客户', value: row.customerName }] : []),
|
||||
],
|
||||
}));
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
label="违章记录"
|
||||
emptyDesc="该车辆暂无违章记录,可回 PC 录入后同步查看"
|
||||
dateFieldLabel="违章时间"
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MovementRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const items = useMemo(() => {
|
||||
const rows = sortByDateDesc(
|
||||
ensureMovementRecords(record, resolveVehicleMovementRecords(record)),
|
||||
(row) => timelineDatePart(row.startDate),
|
||||
);
|
||||
return rows.map((row): TimelineItem => ({
|
||||
id: row.id,
|
||||
date: timelineDatePart(row.startDate) || '—',
|
||||
accent: row.status === '进行中',
|
||||
badge: row.status,
|
||||
title: row.startDate || '—',
|
||||
meta: [
|
||||
{ label: '类型', value: row.movementType || '—' },
|
||||
{
|
||||
label: '目的地',
|
||||
value: `${row.destinationType || ''} ${row.destinationName || ''}`.trim() || '—',
|
||||
},
|
||||
{
|
||||
label: '预计结束',
|
||||
value: row.estimatedEndDate || '进行中',
|
||||
pending: !row.estimatedEndDate,
|
||||
},
|
||||
],
|
||||
}));
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
label="异动记录"
|
||||
emptyDesc="该车辆暂无异动记录,可回 PC 录入后同步查看"
|
||||
dateFieldLabel="开始时间"
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TransferRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const items = useMemo(() => {
|
||||
const rows = sortByDateDesc(
|
||||
ensureTransferRecords(record, resolveVehicleTransferRecords(record)),
|
||||
(row) => timelineDatePart(row.transferDate),
|
||||
);
|
||||
return rows.map((row): TimelineItem => ({
|
||||
id: row.id,
|
||||
date: timelineDatePart(row.transferDate) || '—',
|
||||
badge: row.transferMethod || undefined,
|
||||
title: row.transferDate || '—',
|
||||
meta: [
|
||||
{ label: '出发城市', value: row.departureArea || '—' },
|
||||
{ label: '到达城市', value: row.receivingArea || '—' },
|
||||
{
|
||||
label: '停车场',
|
||||
value: `${row.departureParking || '—'} → ${row.receivingParking || '—'}`,
|
||||
},
|
||||
{ label: '接收人', value: row.recipientPerson || '—' },
|
||||
{ label: '接收日', value: row.receiveDate || '—' },
|
||||
],
|
||||
}));
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
label="调拨记录"
|
||||
emptyDesc="该车辆暂无调拨记录,可回 PC 录入后同步查看"
|
||||
dateFieldLabel="调拨日期"
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnnualReviewRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const items = useMemo(() => {
|
||||
const rows = sortByDateDesc(
|
||||
ensureAnnualReviewRecords(record, resolveVehicleAnnualReviewRecords(record)),
|
||||
(row) => timelineDatePart(row.executeTime),
|
||||
);
|
||||
return rows.map((row): TimelineItem => ({
|
||||
id: row.id,
|
||||
date: timelineDatePart(row.executeTime) || timelineDatePart(row.inspectionValidUntil) || '—',
|
||||
title: row.executeTime || '—',
|
||||
meta: [
|
||||
{ label: '有效期至', value: row.inspectionValidUntil || '—' },
|
||||
{ label: '检测站', value: row.inspectionStation || '—' },
|
||||
{ label: '执行人', value: row.executor || '—' },
|
||||
],
|
||||
}));
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
label="年审记录"
|
||||
emptyDesc="该车辆暂无年审记录,可回 PC 录入后同步查看"
|
||||
dateFieldLabel="执行时间"
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { useEffect, useId, useRef } from 'react';
|
||||
import type {
|
||||
InsuranceFilter,
|
||||
LicenseFilter,
|
||||
OnlineFilter,
|
||||
OutboundFilter,
|
||||
} from '../utils/filter';
|
||||
import { IconX } from './Icons';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
licenseStatus: LicenseFilter;
|
||||
insuranceStatus: InsuranceFilter;
|
||||
outboundStatus: OutboundFilter;
|
||||
vehicleSource: string;
|
||||
onlineStatus: OnlineFilter;
|
||||
onLicenseChange: (value: LicenseFilter) => void;
|
||||
onInsuranceChange: (value: InsuranceFilter) => void;
|
||||
onOutboundChange: (value: OutboundFilter) => void;
|
||||
onVehicleSourceChange: (value: string) => void;
|
||||
onOnlineChange: (value: OnlineFilter) => void;
|
||||
onReset: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const LICENSE_OPTIONS: Array<{ value: LicenseFilter; label: string }> = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '正常', label: '正常' },
|
||||
{ value: '异常', label: '异常' },
|
||||
];
|
||||
|
||||
const INSURANCE_OPTIONS: Array<{ value: InsuranceFilter; label: string }> = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '正常', label: '正常' },
|
||||
{ value: '异常', label: '异常' },
|
||||
];
|
||||
|
||||
const OUTBOUND_OPTIONS: Array<{ value: OutboundFilter; label: string }> = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '未出库', label: '未出库' },
|
||||
{ value: '已出库', label: '已出库' },
|
||||
];
|
||||
|
||||
const SOURCE_OPTIONS: Array<{ value: string; label: string }> = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '自有', label: '自有' },
|
||||
{ value: '外租', label: '外租' },
|
||||
{ value: '挂靠', label: '挂靠' },
|
||||
];
|
||||
|
||||
const ONLINE_OPTIONS: Array<{ value: OnlineFilter; label: string }> = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '在线', label: '在线' },
|
||||
{ value: '离线', label: '离线' },
|
||||
];
|
||||
|
||||
type ChipGroupProps<T extends string> = {
|
||||
labelId: string;
|
||||
label: string;
|
||||
value: T;
|
||||
options: Array<{ value: T; label: string }>;
|
||||
onChange: (value: T) => void;
|
||||
};
|
||||
|
||||
function ChipGroup<T extends string>({ labelId, label, value, options, onChange }: ChipGroupProps<T>) {
|
||||
return (
|
||||
<div className="h5-va-sheet-section">
|
||||
<div className="h5-va-sheet-label" id={labelId}>{label}</div>
|
||||
<div className="h5-va-chip-row" role="group" aria-labelledby={labelId}>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={`${labelId}-${option.label}`}
|
||||
type="button"
|
||||
className={`h5-va-chip${value === option.value ? ' is-active' : ''}`}
|
||||
aria-pressed={value === option.value}
|
||||
onClick={() => onChange(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterSheet({
|
||||
open,
|
||||
licenseStatus,
|
||||
insuranceStatus,
|
||||
outboundStatus,
|
||||
vehicleSource,
|
||||
onlineStatus,
|
||||
onLicenseChange,
|
||||
onInsuranceChange,
|
||||
onOutboundChange,
|
||||
onVehicleSourceChange,
|
||||
onOnlineChange,
|
||||
onReset,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const titleId = useId();
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
closeRef.current?.focus();
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onCloseRef.current();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h5-va-sheet-mask"
|
||||
role="presentation"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="h5-va-sheet h5-va-sheet--filters"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
data-annotation-id="h5-va-filter"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="h5-va-sheet-handle" aria-hidden />
|
||||
<div className="h5-va-sheet-head">
|
||||
<h2 id={titleId}>更多筛选</h2>
|
||||
<button
|
||||
ref={closeRef}
|
||||
type="button"
|
||||
className="h5-nav-btn"
|
||||
onClick={onClose}
|
||||
aria-label="关闭更多筛选"
|
||||
>
|
||||
<IconX />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h5-va-sheet-body">
|
||||
<ChipGroup
|
||||
labelId="h5-va-license-label"
|
||||
label="证照状态"
|
||||
value={licenseStatus}
|
||||
options={LICENSE_OPTIONS}
|
||||
onChange={onLicenseChange}
|
||||
/>
|
||||
<ChipGroup
|
||||
labelId="h5-va-insurance-label"
|
||||
label="保险状态"
|
||||
value={insuranceStatus}
|
||||
options={INSURANCE_OPTIONS}
|
||||
onChange={onInsuranceChange}
|
||||
/>
|
||||
<ChipGroup
|
||||
labelId="h5-va-outbound-label"
|
||||
label="出库状态"
|
||||
value={outboundStatus}
|
||||
options={OUTBOUND_OPTIONS}
|
||||
onChange={onOutboundChange}
|
||||
/>
|
||||
<ChipGroup
|
||||
labelId="h5-va-source-label"
|
||||
label="车辆来源"
|
||||
value={vehicleSource}
|
||||
options={SOURCE_OPTIONS}
|
||||
onChange={onVehicleSourceChange}
|
||||
/>
|
||||
<ChipGroup
|
||||
labelId="h5-va-online-label"
|
||||
label="在线状态"
|
||||
value={onlineStatus}
|
||||
options={ONLINE_OPTIONS}
|
||||
onChange={onOnlineChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h5-va-sheet-actions">
|
||||
<button type="button" className="h5-btn h5-btn--ghost" onClick={onReset}>
|
||||
重置
|
||||
</button>
|
||||
<button type="button" className="h5-btn h5-btn--primary" onClick={onClose}>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
512
src/prototypes/oneos-h5-vehicle-assets/components/Icons.tsx
Normal file
512
src/prototypes/oneos-h5-vehicle-assets/components/Icons.tsx
Normal file
@@ -0,0 +1,512 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function IconEmptyFleet({ size = 64, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 80 80" fill="none" aria-hidden>
|
||||
<circle cx="40" cy="40" r="36" fill="url(#h5-va-empty-glow)" />
|
||||
<rect x="16" y="28" width="48" height="28" rx="8" fill="#fff" stroke="currentColor" strokeWidth="1.6" />
|
||||
<path
|
||||
d="M22 40h10.5l3-6h14l3.5 6H58"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="28" cy="50" r="3.2" fill="currentColor" />
|
||||
<circle cx="52" cy="50" r="3.2" fill="currentColor" />
|
||||
<path d="M54 22l8 8M62 22l-8 8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" opacity="0.55" />
|
||||
<defs>
|
||||
<radialGradient id="h5-va-empty-glow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 40) rotate(90) scale(36)">
|
||||
<stop stopColor="currentColor" stopOpacity="0.16" />
|
||||
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 业务记录空态 — 剪贴板 / 单据 */
|
||||
export function IconEmptyRecords({ size = 64, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 80 80" fill="none" aria-hidden>
|
||||
<circle cx="40" cy="40" r="36" fill="url(#h5-va-empty-rec-glow)" />
|
||||
<rect x="24" y="18" width="32" height="44" rx="8" fill="#fff" stroke="currentColor" strokeWidth="1.6" />
|
||||
<rect x="30" y="14" width="20" height="10" rx="4" fill="#fff" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M32 36h16M32 44h16M32 52h10" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" opacity="0.55" />
|
||||
<circle cx="54" cy="54" r="10" fill="#fff" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M50.5 54h7M54 50.5v7" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" opacity="0.45" />
|
||||
<defs>
|
||||
<radialGradient id="h5-va-empty-rec-glow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 40) rotate(90) scale(36)">
|
||||
<stop stopColor="currentColor" stopOpacity="0.16" />
|
||||
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconCheck({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M5 12.5l5 5L19 7" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 列表到底 — 勾选圆点 */
|
||||
export function IconListEnd({ size = 16, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6" opacity="0.35" />
|
||||
<circle cx="12" cy="12" r="9" fill="currentColor" opacity="0.08" />
|
||||
<path
|
||||
d="M8 12.2l2.6 2.6L16.4 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.9"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconChevronLeft({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M15 18l-6-6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconChevronRight({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M9 18l6-6-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconChevronDown({ size = 16, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSearch({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M20 20l-3.5-3.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSliders({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<circle cx="4" cy="14" r="2.5" stroke="currentColor" strokeWidth="2" />
|
||||
<circle cx="12" cy="10" r="2.5" stroke="currentColor" strokeWidth="2" />
|
||||
<circle cx="20" cy="16" r="2.5" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconFilter({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M4 5h16l-6.5 7.5V19l-3 1.5v-8L4 5z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconX({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconCircleX({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconCar({ size = 24, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M5 16l1.2-4.2A2 2 0 018.1 10h7.8a2 2 0 011.9 1.8L19 16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 16h17M7 16v1.5a1.5 1.5 0 003 0V16m4 0v1.5a1.5 1.5 0 003 0V16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 10l1.2-2.4A1.5 1.5 0 019.99 7h4.02a1.5 1.5 0 011.29.6L16.5 10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconBell({ size = 22, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
{/* soft body fill — depth without heavy chrome */}
|
||||
<path
|
||||
d="M12 3.2c.48 0 .88.3 1.02.74A5.85 5.85 0 0118.1 9.6c0 2.4.72 4.05 1.4 5.15.18.3-.04.7-.4.7H4.9c-.36 0-.58-.4-.4-.7.68-1.1 1.4-2.75 1.4-5.15a5.85 5.85 0 015.08-5.66C11.12 3.5 11.52 3.2 12 3.2z"
|
||||
fill="currentColor"
|
||||
opacity="0.1"
|
||||
/>
|
||||
{/* bell body */}
|
||||
<path
|
||||
d="M12 3.2c.48 0 .88.3 1.02.74A5.85 5.85 0 0118.1 9.6c0 2.4.72 4.05 1.4 5.15.18.3-.04.7-.4.7H4.9c-.36 0-.58-.4-.4-.7.68-1.1 1.4-2.75 1.4-5.15a5.85 5.85 0 015.08-5.66C11.12 3.5 11.52 3.2 12 3.2z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.7"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
{/* hanger */}
|
||||
<path
|
||||
d="M12 3.15V2.15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.7"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="12" cy="1.85" r="0.95" fill="currentColor" />
|
||||
{/* clapper */}
|
||||
<path
|
||||
d="M9.6 18.35a2.55 2.55 0 004.8 0"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.7"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconPlus({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconScan({ size = 18, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M4 8V5a1 1 0 011-1h3M16 4h3a1 1 0 011 1v3M20 16v3a1 1 0 01-1 1h-3M8 20H5a1 1 0 01-1-1v-3" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<path d="M5 12h14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconPlay({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M8 5.5v13l11-6.5L8 5.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconPause({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconAlert({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M12 3l10 18H2L12 3z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
|
||||
<path d="M12 10v4M12 17h.01" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconWallet({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<rect x="3" y="6" width="18" height="13" rx="2.5" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M3 10h18" stroke="currentColor" strokeWidth="1.8" />
|
||||
<circle cx="16.5" cy="14.5" r="1.2" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 业务 Tab:公文包 */
|
||||
export function IconBriefcase({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<rect x="3" y="7" width="18" height="13" rx="2.5" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M9 7V5.8A1.8 1.8 0 0110.8 4h2.4A1.8 1.8 0 0115 5.8V7" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M3 12h18" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M10 12v1.5a2 2 0 004 0V12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconChart({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M4 19V5M4 19h16" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<path d="M8 15v-4M12 15V8M16 15v-6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 工作台中心凸起按钮用的饼图图标 */
|
||||
export function IconPie({ size = 22, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M12 3a9 9 0 109 9h-9V3z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M13.5 3.2A9 9 0 0120.8 10.5H13.5V3.2z" fill="currentColor" opacity="0.35" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconMessage({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M5 6.5A2.5 2.5 0 017.5 4h9A2.5 2.5 0 0119 6.5v7A2.5 2.5 0 0116.5 16H10l-4 3.5V6.5z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconUser({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="8" r="3.5" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M5 19.5c1.5-3.2 4-4.5 7-4.5s5.5 1.3 7 4.5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconHome({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M4 10.5L12 4l8 6.5V20a1 1 0 01-1 1h-5v-6H10v6H5a1 1 0 01-1-1v-9.5z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconTasks({ size = 20, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M9 6h11M9 12h11M9 18h11" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<path d="M5 6.2l1.2 1.2L8.2 5.4M5 12.2l1.2 1.2L8.2 11.4M5 18.2l1.2 1.2L8.2 17.4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconTruck({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M3 7h11v10H3V7zM14 10h4l3 3v4h-7v-7z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
|
||||
<circle cx="7" cy="18" r="1.6" fill="currentColor" />
|
||||
<circle cx="17" cy="18" r="1.6" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconBox({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M4 8l8-4 8 4v9l-8 4-8-4V8z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
|
||||
<path d="M4 8l8 4 8-4M12 12v9" stroke="currentColor" strokeWidth="1.6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSort({ size = 16, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M8 6v12M8 6l-3 3M8 6l3 3" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M16 18V6M16 18l-3-3M16 18l3-3" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconBrand({ size = 16, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M4 16.5l1.4-4.6A2 2 0 017.3 10h9.4a2 2 0 011.9 1.9L20 16.5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M6.5 16.5h11M8 16.5v1.2a1.2 1.2 0 002.4 0v-1.2m3.2 0v1.2a1.2 1.2 0 002.4 0v-1.2" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
|
||||
<path d="M8 10l1.1-2.2A1.2 1.2 0 0110.2 7h3.6a1.2 1.2 0 011.1.8L16 10" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconStatus({ size = 16, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="12" r="8" stroke="currentColor" strokeWidth="1.7" />
|
||||
<circle cx="12" cy="12" r="3.2" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconBuilding({ size = 16, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M5 20V6.5A1.5 1.5 0 016.5 5h7A1.5 1.5 0 0115 6.5V20" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round" />
|
||||
<path d="M15 10h3.5A1.5 1.5 0 0120 11.5V20" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round" />
|
||||
<path d="M8 8.5h3M8 12h3M8 15.5h3M5 20h15" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** KPI:全部车辆 — 车队轮廓 */
|
||||
export function IconKpiFleet({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M4.5 15.5h15M7 15.5v1.4a1.1 1.1 0 002.2 0v-1.4m5.6 0v1.4a1.1 1.1 0 002.2 0v-1.4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.2 15.5l1.1-3.8A1.8 1.8 0 018.05 10h7.9a1.8 1.8 0 011.75 1.7l1.1 3.8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 10l1-2.1A1.2 1.2 0 0110.1 7h3.8a1.2 1.2 0 011.1.9L16 10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="8.2" cy="13.2" r="0.85" fill="currentColor" />
|
||||
<circle cx="15.8" cy="13.2" r="0.85" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** KPI:租赁 — 钥匙卡片 */
|
||||
export function IconKpiLease({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="9" cy="10" r="3.2" stroke="currentColor" strokeWidth="1.6" />
|
||||
<path
|
||||
d="M11.4 12.2L18 18.8M15.2 16l1.6 1.6M17 17.8l1.5 1.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="9" cy="10" r="1.1" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** KPI:物流 — 厢式货车 */
|
||||
export function IconKpiLogistics({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M3.5 7.5h10.5v9H3.5v-9z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14 10.5h3.2L20 13.6v3H14v-6.1z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M3.5 12.2h10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<circle cx="7" cy="17.6" r="1.55" stroke="currentColor" strokeWidth="1.5" />
|
||||
<circle cx="17" cy="17.6" r="1.55" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** KPI:库存 — 立体货箱 */
|
||||
export function IconKpiStock({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M4.5 9.2L12 5.5l7.5 3.7v7.1L12 20l-7.5-4V9.2z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M4.5 9.2L12 13l7.5-3.8M12 13v7" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** KPI:非运营 — 暂停运营 */
|
||||
export function IconKpiIdle({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="12" r="8" stroke="currentColor" strokeWidth="1.6" />
|
||||
<path d="M10 9.2v5.6M14 9.2v5.6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** KPI:退出运营 — 离场箭头 */
|
||||
export function IconKpiExit({ size = 14, className }: Props) {
|
||||
return (
|
||||
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M10 5.5H7.2A2.2 2.2 0 005 7.7v8.6A2.2 2.2 0 007.2 18.5H10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M13 12h7.5M17.2 8.8L20.5 12l-3.3 3.2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { VehicleRecord } from '../utils/filter';
|
||||
import { daysUntilExpire } from '../../vehicle-management/utils/vehicle';
|
||||
import {
|
||||
insuranceStatusTone,
|
||||
resolveVehicleInsuranceOverview,
|
||||
type InsuranceHistoryRow,
|
||||
type InsuranceTypeCoverage,
|
||||
type VehicleInsuranceTypeKey,
|
||||
} from '../../vehicle-management/utils/vehicleInsuranceRecords';
|
||||
import { showAppToast } from './AppToast';
|
||||
import { IconX } from './Icons';
|
||||
import { RecordsEmpty } from './RecordsEmpty';
|
||||
import {
|
||||
RecordTimeline,
|
||||
sortByDateDesc,
|
||||
timelineDatePart,
|
||||
type TimelineItem,
|
||||
} from './RecordTimeline';
|
||||
|
||||
function ExpireInline({ date }: { date: string }) {
|
||||
if (!date) return <span>—</span>;
|
||||
const days = daysUntilExpire(date);
|
||||
let tag = '';
|
||||
let tone: 'ok' | 'warn' | 'danger' = 'ok';
|
||||
if (days !== null) {
|
||||
if (days < 0) {
|
||||
tag = `已过期${Math.abs(days)}天`;
|
||||
tone = 'danger';
|
||||
} else {
|
||||
tag = `剩余${days}天`;
|
||||
if (days <= 30) tone = 'warn';
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span className="h5-va-expire">
|
||||
<span className="h5-va-expire-date">{date.slice(0, 10)}</span>
|
||||
{tag ? <span className={`h5-va-expire-tag h5-va-expire-tag--${tone}`}>{tag}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusPill({
|
||||
label,
|
||||
tone,
|
||||
}: {
|
||||
label: string;
|
||||
tone: 'green' | 'amber' | 'red' | 'gray' | 'blue';
|
||||
}) {
|
||||
return <span className={`h5-va-ins-pill h5-va-ins-pill--${tone}`}>{label}</span>;
|
||||
}
|
||||
|
||||
function shortStatusLabel(status: InsuranceTypeCoverage['status']): string {
|
||||
switch (status) {
|
||||
case 'not_purchased':
|
||||
return '未购';
|
||||
case 'valid':
|
||||
return '正常';
|
||||
case 'warning':
|
||||
return '临期';
|
||||
case 'expired':
|
||||
return '到期';
|
||||
case 'suspended':
|
||||
return '停保';
|
||||
case 'cancelled':
|
||||
return '退保';
|
||||
default:
|
||||
return '—';
|
||||
}
|
||||
}
|
||||
|
||||
function TypeCard({
|
||||
coverage,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
coverage: InsuranceTypeCoverage;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const tone = insuranceStatusTone(coverage.status);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`h5-va-ins-type h5-va-ins-type--${tone}${active ? ' is-active' : ''}`}
|
||||
aria-pressed={active}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="h5-va-ins-type-title">{coverage.label}</span>
|
||||
<span className={`h5-va-ins-type-dot h5-va-ins-type-dot--${tone}`} aria-hidden />
|
||||
<span className="h5-va-ins-type-state">{shortStatusLabel(coverage.status)}</span>
|
||||
{coverage.history.length > 0 ? (
|
||||
<span className="h5-va-ins-type-count">{coverage.history.length}</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function buildPolicyPreviewUrl(fileName: string): string {
|
||||
const seed = fileName.replace(/[^a-zA-Z0-9]/g, '').slice(0, 24) || 'policy';
|
||||
return `https://picsum.photos/seed/${seed}/720/960`;
|
||||
}
|
||||
|
||||
function downloadPolicyFile(fileName: string) {
|
||||
const content = [
|
||||
'OneOS 保单附件(原型演示)',
|
||||
`文件名:${fileName}`,
|
||||
'说明:正式环境将下载真实保单 PDF;本文件仅用于演示下载流程。',
|
||||
'',
|
||||
].join('\n');
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
const safeName = fileName.replace(/[\\/:*?"<>|]/g, '_');
|
||||
link.href = url;
|
||||
link.download = /\.pdf$/i.test(safeName) ? safeName.replace(/\.pdf$/i, '.txt') : `${safeName}.txt`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
showAppToast(`已开始下载:${fileName}`);
|
||||
}
|
||||
|
||||
function PolicyPreviewSheet({
|
||||
fileName,
|
||||
onClose,
|
||||
onDownload,
|
||||
}: {
|
||||
fileName: string;
|
||||
onClose: () => void;
|
||||
onDownload: () => void;
|
||||
}) {
|
||||
const titleId = useId();
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
const host = typeof document !== 'undefined'
|
||||
? (document.querySelector('.h5-phone') as HTMLElement | null)
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
closeRef.current?.focus();
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const previewUrl = buildPolicyPreviewUrl(fileName);
|
||||
const sheet = (
|
||||
<div className="h5-va-sheet-mask h5-va-policy-mask" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
className="h5-va-sheet h5-va-sheet--filters h5-va-policy-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="h5-va-sheet-handle" aria-hidden />
|
||||
<div className="h5-va-sheet-head">
|
||||
<h2 id={titleId}>保单预览</h2>
|
||||
<button
|
||||
ref={closeRef}
|
||||
type="button"
|
||||
className="h5-nav-btn"
|
||||
onClick={onClose}
|
||||
aria-label="关闭保单预览"
|
||||
>
|
||||
<IconX />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h5-va-sheet-body h5-va-policy-body">
|
||||
<div className="h5-va-policy-meta">
|
||||
<div className="h5-va-policy-meta-label">文件名</div>
|
||||
<div className="h5-va-policy-meta-name">{fileName}</div>
|
||||
<div className="h5-va-policy-meta-hint">原型演示影像,正式环境预览真实保单 PDF</div>
|
||||
</div>
|
||||
<div className="h5-va-policy-frame">
|
||||
<img src={previewUrl} alt={`保单预览 ${fileName}`} loading="lazy" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h5-va-sheet-actions">
|
||||
<button type="button" className="h5-btn h5-btn--ghost" onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
<button type="button" className="h5-btn h5-btn--primary" onClick={onDownload}>
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (host) return createPortal(sheet, host);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
function HistoryTimeline({
|
||||
rows,
|
||||
onPreview,
|
||||
onDownload,
|
||||
}: {
|
||||
rows: InsuranceHistoryRow[];
|
||||
onPreview: (fileName: string) => void;
|
||||
onDownload: (fileName: string) => void;
|
||||
}) {
|
||||
const items = useMemo((): TimelineItem[] => {
|
||||
const sorted = sortByDateDesc(rows, (row) => {
|
||||
const purchase = timelineDatePart(row.purchaseTime);
|
||||
const expire = timelineDatePart(row.expireDate);
|
||||
return purchase || expire;
|
||||
});
|
||||
return sorted.map((row) => {
|
||||
const open = row.policyStatus === '正常';
|
||||
return {
|
||||
id: row.id,
|
||||
date: timelineDatePart(row.purchaseTime) || timelineDatePart(row.expireDate) || '—',
|
||||
accent: open && Boolean(row.remainText && !row.remainText.includes('过期')),
|
||||
badge: row.purchaseType,
|
||||
title: row.policyNo || '—',
|
||||
subtitle: row.company || undefined,
|
||||
meta: [
|
||||
{ label: '状态', value: row.policyStatus || '—' },
|
||||
{
|
||||
label: '到期',
|
||||
value: row.expireDate ? <ExpireInline date={row.expireDate} /> : '—',
|
||||
},
|
||||
],
|
||||
footer: (
|
||||
<div className="h5-va-ins-history-file">
|
||||
{row.policyFileName ? (
|
||||
<div className="h5-va-ins-file-row">
|
||||
<div className="h5-va-ins-file-name" title={row.policyFileName}>
|
||||
{row.policyFileName}
|
||||
</div>
|
||||
<div className="h5-va-ins-file-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="h5-va-ins-file-btn"
|
||||
onClick={() => onPreview(row.policyFileName!)}
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h5-va-ins-file-btn h5-va-ins-file-btn--primary"
|
||||
onClick={() => onDownload(row.policyFileName!)}
|
||||
>
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="h5-va-ins-file-empty">无附件</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [rows, onPreview, onDownload]);
|
||||
|
||||
return <RecordTimeline items={items} label="采购批单历史时间轴" />;
|
||||
}
|
||||
|
||||
export function InsuranceRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const overview = useMemo(() => resolveVehicleInsuranceOverview(record), [record]);
|
||||
const [activeType, setActiveType] = useState<VehicleInsuranceTypeKey>('compulsory');
|
||||
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||
const active = overview.types.find((item) => item.key === activeType) ?? overview.types[0];
|
||||
|
||||
function handleDownload(fileName: string) {
|
||||
downloadPolicyFile(fileName);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h5-va-ins-panel">
|
||||
<section className="h5-va-ins-hero" aria-label="保险总览">
|
||||
<div className="h5-va-ins-hero-row">
|
||||
<span className="h5-va-ins-hero-title">车辆保险档案</span>
|
||||
<StatusPill
|
||||
label={overview.overallLabel}
|
||||
tone={overview.overallTone}
|
||||
/>
|
||||
</div>
|
||||
{overview.overallTip ? (
|
||||
<p className="h5-va-ins-hero-tip">{overview.overallTip}</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<div className="h5-va-ins-type-grid" role="tablist" aria-label="险种">
|
||||
{overview.types.map((coverage) => (
|
||||
<TypeCard
|
||||
key={coverage.key}
|
||||
coverage={coverage}
|
||||
active={active.key === coverage.key}
|
||||
onClick={() => setActiveType(coverage.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="h5-va-section" role="tabpanel" aria-label={active.label}>
|
||||
<div className="h5-va-ins-panel-head">
|
||||
<h3 className="h5-va-section-title">{active.label}</h3>
|
||||
<StatusPill label={active.statusLabel} tone={insuranceStatusTone(active.status)} />
|
||||
</div>
|
||||
|
||||
{active.history.length === 0 ? (
|
||||
<RecordsEmpty
|
||||
compact
|
||||
title={`${active.label}暂无记录`}
|
||||
desc="请在 PC「保险采购」录入或导入保单后,手机端即可同步查看"
|
||||
tip="切换上方险种可查看其他保险档案"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="h5-va-field-grid h5-va-ins-summary">
|
||||
<div className="h5-va-field">
|
||||
<div className="h5-va-field-label">保单号</div>
|
||||
<div className="h5-va-field-value h5-va-field-value--code">{active.policyNo || '—'}</div>
|
||||
</div>
|
||||
<div className="h5-va-field">
|
||||
<div className="h5-va-field-label">保险公司</div>
|
||||
<div className="h5-va-field-value">{active.company || '—'}</div>
|
||||
</div>
|
||||
<div className="h5-va-field">
|
||||
<div className="h5-va-field-label">到期日期</div>
|
||||
<div className="h5-va-field-value">
|
||||
{active.expireDate ? <ExpireInline date={active.expireDate} /> : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h5-va-field">
|
||||
<div className="h5-va-field-label">保费(元)</div>
|
||||
<div className="h5-va-field-value">{active.premium || '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h5-va-ins-history-list">
|
||||
<div className="h5-va-ins-history-caption">采购 / 批单历史</div>
|
||||
<HistoryTimeline
|
||||
rows={active.history}
|
||||
onPreview={setPreviewFile}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{previewFile ? (
|
||||
<PolicyPreviewSheet
|
||||
fileName={previewFile}
|
||||
onClose={() => setPreviewFile(null)}
|
||||
onDownload={() => handleDownload(previewFile)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { VehicleRecord } from '../utils/filter';
|
||||
import {
|
||||
resolveVehicleLeaseRecords,
|
||||
type LeaseRecordRow,
|
||||
} from '../../vehicle-management/utils/leaseRecords';
|
||||
import { ensureLeaseRecords } from '../utils/demo-biz-records';
|
||||
import { IconCircleX, IconSearch } from './Icons';
|
||||
import { RecordsEmpty } from './RecordsEmpty';
|
||||
import { RecordTimeline, type TimelineItem } from './RecordTimeline';
|
||||
|
||||
function hasReturnDate(row: LeaseRecordRow): boolean {
|
||||
const value = (row.returnDate || '').trim();
|
||||
return Boolean(value) && value !== '-' && value !== '—';
|
||||
}
|
||||
|
||||
/** 时间轴排序键:取交车/还车中较近的日期,近→远 */
|
||||
function timelineSortKey(row: LeaseRecordRow): string {
|
||||
const delivery = (row.deliveryDate || '').trim();
|
||||
const ret = hasReturnDate(row) ? row.returnDate.trim() : '';
|
||||
if (delivery && ret) return delivery > ret ? delivery : ret;
|
||||
return ret || delivery || '';
|
||||
}
|
||||
|
||||
function formatReturnLabel(row: LeaseRecordRow): string {
|
||||
return hasReturnDate(row) ? row.returnDate : '未还车';
|
||||
}
|
||||
|
||||
function matchesLeaseSearch(row: LeaseRecordRow, query: string): boolean {
|
||||
const needle = query.trim().toLowerCase();
|
||||
if (!needle) return true;
|
||||
const customer = (row.customerName || '').toLowerCase();
|
||||
const project = (row.projectName || '').toLowerCase();
|
||||
return customer.includes(needle) || project.includes(needle);
|
||||
}
|
||||
|
||||
export function LeaseRecordsPanel({ record }: { record: VehicleRecord }) {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const allRows = useMemo(() => {
|
||||
const list = [...ensureLeaseRecords(record, resolveVehicleLeaseRecords(record))];
|
||||
return list.sort((a, b) => timelineSortKey(b).localeCompare(timelineSortKey(a)));
|
||||
}, [record]);
|
||||
|
||||
const rows = useMemo(
|
||||
() => allRows.filter((row) => matchesLeaseSearch(row, query)),
|
||||
[allRows, query],
|
||||
);
|
||||
|
||||
const items = useMemo(
|
||||
(): TimelineItem[] =>
|
||||
rows.map((row) => {
|
||||
const returned = hasReturnDate(row);
|
||||
return {
|
||||
id: row.id,
|
||||
date: timelineSortKey(row) || '—',
|
||||
accent: !returned,
|
||||
badge: row.businessType || undefined,
|
||||
title: row.contractNo || '—',
|
||||
subtitle: row.projectName || undefined,
|
||||
meta: [
|
||||
{ label: '客户', value: row.customerName || '—' },
|
||||
{ label: '交车', value: row.deliveryDate || '—' },
|
||||
{
|
||||
label: '还车',
|
||||
value: formatReturnLabel(row),
|
||||
pending: !returned,
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
[rows],
|
||||
);
|
||||
|
||||
const hasQuery = Boolean(query.trim());
|
||||
|
||||
if (!allRows.length) {
|
||||
return (
|
||||
<RecordsEmpty
|
||||
title="暂无记录"
|
||||
desc="该车辆暂无租赁交还车记录,可回 PC 录入后同步查看"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h5-va-tl-panel h5-va-section--records" aria-label="租赁记录">
|
||||
<div className="h5-va-tl-search h5-va-search">
|
||||
<IconSearch size={16} />
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
placeholder="搜索客户名称 / 项目名称"
|
||||
enterKeyHint="search"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
inputMode="search"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
aria-label="搜索客户名称或项目名称"
|
||||
/>
|
||||
{hasQuery ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h5-va-clear-btn"
|
||||
onClick={() => setQuery('')}
|
||||
aria-label="清空搜索"
|
||||
>
|
||||
<IconCircleX size={16} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!items.length ? (
|
||||
<div className="h5-va-empty h5-va-empty--inset" role="status">
|
||||
<p className="h5-va-empty-title">暂无匹配记录</p>
|
||||
<p className="h5-va-empty-desc">换个关键词,或清空搜索查看全部</p>
|
||||
<div className="h5-va-empty-actions">
|
||||
<button type="button" className="h5-btn h5-btn--primary" onClick={() => setQuery('')}>
|
||||
清空搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<RecordTimeline items={items} label="租赁记录时间轴" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { VehicleRecord } from '../utils/filter';
|
||||
import {
|
||||
daysUntilExpire,
|
||||
formatDateYmd,
|
||||
isEmpty,
|
||||
isShanghaiPlate,
|
||||
} from '../../vehicle-management/utils/vehicle';
|
||||
import {
|
||||
certPhotoStatus,
|
||||
formatLicenseBalance,
|
||||
formatLicenseDisplay,
|
||||
LICENSE_CERT_NAV,
|
||||
resolveVehicleLicenseBundle,
|
||||
type VehicleLicenseBundle,
|
||||
} from '../../vehicle-management/utils/vehicleLicenses';
|
||||
|
||||
type CertKey = keyof VehicleLicenseBundle;
|
||||
|
||||
function ExpireValue({ date, emptyLabel = '未上传' }: { date: string; emptyLabel?: string }) {
|
||||
if (isEmpty(date)) return <>{emptyLabel}</>;
|
||||
const text = formatDateYmd(date) || String(date).slice(0, 10);
|
||||
const days = daysUntilExpire(date);
|
||||
let tag: string | null = null;
|
||||
let tone: 'ok' | 'warn' | 'danger' = 'ok';
|
||||
if (days !== null) {
|
||||
if (days < 0) {
|
||||
tag = `已过期${Math.abs(days)}天`;
|
||||
tone = 'danger';
|
||||
} else {
|
||||
tag = `剩余${days}天`;
|
||||
if (days <= 30) tone = 'warn';
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span className="h5-va-expire">
|
||||
<span className="h5-va-expire-date">{text}</span>
|
||||
{tag ? <span className={`h5-va-expire-tag h5-va-expire-tag--${tone}`}>{tag}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function formatRemainDaysLabel(date: string): string {
|
||||
if (isEmpty(date)) return '—';
|
||||
const days = daysUntilExpire(date);
|
||||
if (days === null) return '—';
|
||||
return days < 0 ? `已过期${Math.abs(days)}天` : `剩余${days}天`;
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="h5-va-field">
|
||||
<div className="h5-va-field-label">{label}</div>
|
||||
<div className="h5-va-field-value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CertStatusBadge({ status }: { status: 'uploaded' | 'empty' | 'bound' | 'unbound' }) {
|
||||
const label =
|
||||
status === 'uploaded' ? '已上传'
|
||||
: status === 'empty' ? '未上传'
|
||||
: status === 'bound' ? '已绑定'
|
||||
: '未绑定';
|
||||
return (
|
||||
<span className={`h5-va-cert-badge h5-va-cert-badge--${status === 'bound' || status === 'uploaded' ? 'ok' : 'empty'}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function LicensePhotoRow({ photos, alt }: { photos: string[]; alt: string }) {
|
||||
if (!photos.length) {
|
||||
return <div className="h5-va-cert-photos-empty">未上传影像</div>;
|
||||
}
|
||||
return (
|
||||
<div className="h5-va-cert-photos" aria-label={`${alt}影像`}>
|
||||
{photos.map((url, index) => (
|
||||
<div key={`${url}-${index}`} className="h5-va-cert-photo">
|
||||
<img src={url} alt={`${alt}${index + 1}`} loading="lazy" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CertSection({
|
||||
id,
|
||||
title,
|
||||
status,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'uploaded' | 'empty' | 'bound' | 'unbound';
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="h5-va-section h5-va-cert-section" id={id}>
|
||||
<div className="h5-va-cert-head">
|
||||
<h3 className="h5-va-section-title">{title}</h3>
|
||||
<CertStatusBadge status={status} />
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function certAnchorId(key: CertKey): string {
|
||||
return `h5-va-cert-${key}`;
|
||||
}
|
||||
|
||||
/** 在手机壳 .h5-body 内滚动到证照区块(scrollIntoView 会滚错外层容器) */
|
||||
function scrollCertSectionIntoBody(key: CertKey) {
|
||||
const body = document.querySelector('.h5-body') as HTMLElement | null;
|
||||
const target = document.getElementById(certAnchorId(key));
|
||||
if (!body || !target) return;
|
||||
|
||||
const tabs = body.querySelector('.h5-va-tabs') as HTMLElement | null;
|
||||
const indexBar = body.querySelector('.h5-va-cert-index') as HTMLElement | null;
|
||||
const stickyOffset = (tabs?.offsetHeight || 0) + (indexBar?.offsetHeight || 0) + 6;
|
||||
|
||||
const bodyRect = body.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const nextTop = body.scrollTop + (targetRect.top - bodyRect.top) - stickyOffset;
|
||||
body.scrollTo({ top: Math.max(0, nextTop), behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function scrollChipIntoIndex(chip: HTMLButtonElement | null | undefined) {
|
||||
if (!chip) return;
|
||||
const indexBar = chip.closest('.h5-va-cert-index') as HTMLElement | null;
|
||||
if (!indexBar) return;
|
||||
const nextLeft = chip.offsetLeft - (indexBar.clientWidth - chip.offsetWidth) / 2;
|
||||
indexBar.scrollTo({ left: Math.max(0, nextLeft), behavior: 'smooth' });
|
||||
}
|
||||
|
||||
export function LicenseCertPanel({ record }: { record: VehicleRecord }) {
|
||||
const bundle = resolveVehicleLicenseBundle(record);
|
||||
const shanghaiPlate = isShanghaiPlate(record.plateNo);
|
||||
const shEval = record.ratingTime || bundle.driverLicense.shNextEvaluation || '';
|
||||
const hasHydrogen = Boolean(bundle.hydrogenCard.cardNo);
|
||||
const [activeKey, setActiveKey] = useState<CertKey>('driverLicense');
|
||||
const chipRefs = useRef<Partial<Record<CertKey, HTMLButtonElement | null>>>({});
|
||||
|
||||
useEffect(() => {
|
||||
scrollChipIntoIndex(chipRefs.current[activeKey]);
|
||||
}, [activeKey]);
|
||||
|
||||
function selectCert(key: CertKey) {
|
||||
setActiveKey(key);
|
||||
// 等选中态与 sticky 高度稳定后再滚
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollChipIntoIndex(chipRefs.current[key]);
|
||||
scrollCertSectionIntoBody(key);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h5-va-cert-panel">
|
||||
<div className="h5-va-cert-index" role="tablist" aria-label="证照分类">
|
||||
{LICENSE_CERT_NAV.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
ref={(node) => {
|
||||
chipRefs.current[item.key] = node;
|
||||
}}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeKey === item.key}
|
||||
className={`h5-va-cert-index-chip${activeKey === item.key ? ' is-active' : ''}`}
|
||||
onClick={() => selectCert(item.key)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('driverLicense')}
|
||||
title="行驶证"
|
||||
status={certPhotoStatus(bundle.driverLicense)}
|
||||
>
|
||||
<LicensePhotoRow photos={bundle.driverLicense.photos} alt="行驶证" />
|
||||
<div className="h5-va-field-grid">
|
||||
<Field label="注册日期" value={formatLicenseDisplay(bundle.driverLicense.regDate)} />
|
||||
<Field label="发证日期" value={formatLicenseDisplay(bundle.driverLicense.issueDate)} />
|
||||
<Field label="强制报废日期" value={formatLicenseDisplay(bundle.driverLicense.scrapDate)} />
|
||||
<Field
|
||||
label="检验有效期至"
|
||||
value={<ExpireValue date={bundle.driverLicense.expireDate || record.inspectExpire} />}
|
||||
/>
|
||||
</div>
|
||||
{shanghaiPlate ? (
|
||||
<div className="h5-va-cert-sh">
|
||||
<div className="h5-va-cert-sh-badge">沪牌车辆等级评定</div>
|
||||
<div className="h5-va-field-grid">
|
||||
<Field
|
||||
label="下次等级评定时间"
|
||||
value={isEmpty(shEval) ? '未上传' : (formatDateYmd(shEval) || shEval.slice(0, 10))}
|
||||
/>
|
||||
<Field label="剩余天数" value={formatRemainDaysLabel(shEval)} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</CertSection>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('transportLicense')}
|
||||
title="道路运输证"
|
||||
status={certPhotoStatus(bundle.transportLicense)}
|
||||
>
|
||||
<LicensePhotoRow photos={bundle.transportLicense.photos} alt="道路运输证" />
|
||||
<div className="h5-va-field-grid">
|
||||
<Field label="经营许可证号" value={formatLicenseDisplay(bundle.transportLicense.licenseNo)} />
|
||||
<Field label="核发时间" value={formatLicenseDisplay(bundle.transportLicense.issueDate)} />
|
||||
<Field label="证件有效期" value={<ExpireValue date={bundle.transportLicense.expireDate} />} />
|
||||
<Field label="审验有效期" value={<ExpireValue date={bundle.transportLicense.inspectValidUntil} />} />
|
||||
</div>
|
||||
</CertSection>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('registrationCert')}
|
||||
title="登记证"
|
||||
status={certPhotoStatus(bundle.registrationCert)}
|
||||
>
|
||||
<LicensePhotoRow photos={bundle.registrationCert.photos} alt="登记证" />
|
||||
</CertSection>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('specialEquipCert')}
|
||||
title="特种设备使用登记证"
|
||||
status={certPhotoStatus(bundle.specialEquipCert)}
|
||||
>
|
||||
<LicensePhotoRow photos={bundle.specialEquipCert.photos} alt="特种设备使用登记证" />
|
||||
</CertSection>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('specialEquipDecal')}
|
||||
title="特种设备使用标识"
|
||||
status={certPhotoStatus(bundle.specialEquipDecal)}
|
||||
>
|
||||
<LicensePhotoRow photos={bundle.specialEquipDecal.photos} alt="特种设备使用标识" />
|
||||
<div className="h5-va-field-grid">
|
||||
<Field
|
||||
label="下次检验日期"
|
||||
value={<ExpireValue date={bundle.specialEquipDecal.nextInspectDate} />}
|
||||
/>
|
||||
</div>
|
||||
</CertSection>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('hydrogenCard')}
|
||||
title="加氢卡"
|
||||
status={hasHydrogen ? 'bound' : 'unbound'}
|
||||
>
|
||||
{hasHydrogen ? (
|
||||
<div className="h5-va-field-grid">
|
||||
<Field label="加氢卡号" value={bundle.hydrogenCard.cardNo} />
|
||||
<Field label="实时余额" value={`¥ ${formatLicenseBalance(bundle.hydrogenCard.balance)}`} />
|
||||
<Field label="配发时间" value={formatLicenseDisplay(bundle.hydrogenCard.issueDate)} />
|
||||
<Field label="配发经办人" value={formatLicenseDisplay(bundle.hydrogenCard.issueUser)} />
|
||||
<Field label="卡类型" value={formatLicenseDisplay(bundle.hydrogenCard.cardType)} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="h5-va-cert-empty-note">该车辆尚未绑定加氢卡</div>
|
||||
)}
|
||||
</CertSection>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('safetyValve')}
|
||||
title="安全阀"
|
||||
status={certPhotoStatus(bundle.safetyValve)}
|
||||
>
|
||||
<LicensePhotoRow photos={bundle.safetyValve.photos} alt="安全阀" />
|
||||
<div className="h5-va-field-grid">
|
||||
<Field label="本次检验日期" value={formatLicenseDisplay(bundle.safetyValve.inspectDate)} />
|
||||
<Field
|
||||
label="下次检测日期"
|
||||
value={<ExpireValue date={bundle.safetyValve.nextInspectDate} />}
|
||||
/>
|
||||
</div>
|
||||
</CertSection>
|
||||
|
||||
<CertSection
|
||||
id={certAnchorId('pressureGauge')}
|
||||
title="压力表"
|
||||
status={certPhotoStatus(bundle.pressureGauge)}
|
||||
>
|
||||
<LicensePhotoRow photos={bundle.pressureGauge.photos} alt="压力表" />
|
||||
<div className="h5-va-field-grid">
|
||||
<Field label="本次检验日期" value={formatLicenseDisplay(bundle.pressureGauge.inspectDate)} />
|
||||
<Field
|
||||
label="下次检验日期"
|
||||
value={<ExpireValue date={bundle.pressureGauge.nextInspectDate} />}
|
||||
/>
|
||||
</div>
|
||||
</CertSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const ITEM_H = 36;
|
||||
const VISIBLE = 5;
|
||||
const PAD = Math.floor(VISIBLE / 2);
|
||||
|
||||
function pad2(n: number) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
function daysInMonth(year: number, month: number) {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
function parseYmd(value?: string): { y: number; m: number; d: number } {
|
||||
const now = new Date();
|
||||
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate() };
|
||||
}
|
||||
const [ys, ms, ds] = value.split('-');
|
||||
const y = Number(ys);
|
||||
const m = Number(ms);
|
||||
const d = Number(ds);
|
||||
if (!y || !m || !d) return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate() };
|
||||
return { y, m, d: Math.min(d, daysInMonth(y, m)) };
|
||||
}
|
||||
|
||||
function formatYmd(y: number, m: number, d: number) {
|
||||
return `${y}-${pad2(m)}-${pad2(d)}`;
|
||||
}
|
||||
|
||||
function formatDisplay(value?: string) {
|
||||
if (!value) return '';
|
||||
const { y, m, d } = parseYmd(value);
|
||||
return `${y}年${pad2(m)}月${pad2(d)}日`;
|
||||
}
|
||||
|
||||
type WheelProps = {
|
||||
options: { value: number; label: string }[];
|
||||
value: number;
|
||||
onChange: (v: number) => void;
|
||||
'aria-label': string;
|
||||
};
|
||||
|
||||
function WheelColumn({ options, value, onChange, 'aria-label': ariaLabel }: WheelProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lock = useRef(false);
|
||||
const timer = useRef(0);
|
||||
const idx = Math.max(
|
||||
0,
|
||||
options.findIndex((o) => o.value === value),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
lock.current = true;
|
||||
el.scrollTop = idx * ITEM_H;
|
||||
requestAnimationFrame(() => {
|
||||
lock.current = false;
|
||||
});
|
||||
}, [idx, options.length]);
|
||||
|
||||
const settle = () => {
|
||||
const el = ref.current;
|
||||
if (!el || lock.current) return;
|
||||
const next = Math.round(el.scrollTop / ITEM_H);
|
||||
const clamped = Math.max(0, Math.min(options.length - 1, next));
|
||||
el.scrollTop = clamped * ITEM_H;
|
||||
const opt = options[clamped];
|
||||
if (opt && opt.value !== value) onChange(opt.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h5-va-date-wheel-col" aria-label={ariaLabel}>
|
||||
<div
|
||||
ref={ref}
|
||||
className="h5-va-date-wheel-scroll"
|
||||
onScroll={() => {
|
||||
window.clearTimeout(timer.current);
|
||||
timer.current = window.setTimeout(settle, 90);
|
||||
}}
|
||||
onTouchEnd={settle}
|
||||
onMouseUp={settle}
|
||||
>
|
||||
<div style={{ height: PAD * ITEM_H }} aria-hidden />
|
||||
{options.map((o) => (
|
||||
<div
|
||||
key={o.value}
|
||||
className={`h5-va-date-wheel-item${o.value === value ? ' is-active' : ''}`}
|
||||
style={{ height: ITEM_H }}
|
||||
>
|
||||
{o.label}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ height: PAD * ITEM_H }} aria-hidden />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SheetProps = {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
value?: string;
|
||||
minYear?: number;
|
||||
maxYear?: number;
|
||||
onConfirm: (ymd: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/** iOS 风格底部滚轮日期选择(精确到日) */
|
||||
export function NativeDatePickerSheet({
|
||||
open,
|
||||
title = '选择日期',
|
||||
value,
|
||||
minYear = 2015,
|
||||
maxYear,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: SheetProps) {
|
||||
const parsed = parseYmd(value);
|
||||
const [y, setY] = useState(parsed.y);
|
||||
const [m, setM] = useState(parsed.m);
|
||||
const [d, setD] = useState(parsed.d);
|
||||
const [host, setHost] = useState<HTMLElement | null>(null);
|
||||
const yearMax = maxYear ?? new Date().getFullYear() + 1;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) return;
|
||||
setHost(document.querySelector('.h5-phone') as HTMLElement | null);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const next = parseYmd(value);
|
||||
setY(next.y);
|
||||
setM(next.m);
|
||||
setD(next.d);
|
||||
}, [open, value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
const maxDay = daysInMonth(y, m);
|
||||
useEffect(() => {
|
||||
if (d > maxDay) setD(maxDay);
|
||||
}, [d, maxDay]);
|
||||
|
||||
const years = useMemo(
|
||||
() => Array.from({ length: yearMax - minYear + 1 }, (_, i) => minYear + i).map((n) => ({ value: n, label: `${n}年` })),
|
||||
[minYear, yearMax],
|
||||
);
|
||||
const months = useMemo(
|
||||
() => Array.from({ length: 12 }, (_, i) => i + 1).map((n) => ({ value: n, label: `${n}月` })),
|
||||
[],
|
||||
);
|
||||
const days = useMemo(
|
||||
() => Array.from({ length: maxDay }, (_, i) => i + 1).map((n) => ({ value: n, label: `${n}日` })),
|
||||
[maxDay],
|
||||
);
|
||||
|
||||
if (!open || !host) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className="h5-va-sheet-mask h5-va-native-mask" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
className="h5-va-sheet h5-va-native-sheet h5-va-date-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="h5-va-sheet-handle" aria-hidden />
|
||||
<div className="h5-va-native-head">
|
||||
<button type="button" className="h5-va-native-head-btn h5-va-native-head-btn--text" onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<h2 className="h5-va-native-title">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="h5-va-native-head-btn h5-va-native-head-btn--text h5-va-native-head-btn--done"
|
||||
onClick={() => {
|
||||
onConfirm(formatYmd(y, m, Math.min(d, daysInMonth(y, m))));
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
<div className="h5-va-date-wheels" aria-label="年 月 日">
|
||||
<div className="h5-va-date-wheel-highlight" aria-hidden />
|
||||
<WheelColumn aria-label="年" options={years} value={y} onChange={setY} />
|
||||
<WheelColumn aria-label="月" options={months} value={m} onChange={setM} />
|
||||
<WheelColumn aria-label="日" options={days} value={Math.min(d, maxDay)} onChange={setD} />
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
host,
|
||||
);
|
||||
}
|
||||
|
||||
type FieldProps = {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
onChange: (ymd: string) => void;
|
||||
className?: string;
|
||||
'aria-label'?: string;
|
||||
};
|
||||
|
||||
/** 触发器 + 原生滚轮日期选择(精确到日) */
|
||||
export function NativeDateField({
|
||||
value,
|
||||
placeholder = '请选择',
|
||||
title = '选择日期',
|
||||
disabled,
|
||||
onChange,
|
||||
className,
|
||||
'aria-label': ariaLabel,
|
||||
}: FieldProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={`h5-va-native-date-field${className ? ` ${className}` : ''}${!value ? ' is-empty' : ''}`}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || title}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<span>{value ? formatDisplay(value) : placeholder}</span>
|
||||
</button>
|
||||
<NativeDatePickerSheet
|
||||
open={open}
|
||||
title={title}
|
||||
value={value}
|
||||
onConfirm={onChange}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { IconCheck, IconChevronLeft, IconSearch, IconX } from './Icons';
|
||||
|
||||
export type PickerOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
meta?: string;
|
||||
};
|
||||
|
||||
type BaseProps = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
options: PickerOption[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onClose: () => void;
|
||||
searchable?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
emptyText?: string;
|
||||
/** Show leading back instead of close */
|
||||
onBack?: () => void;
|
||||
annotationId?: string;
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
|
||||
function useSheetLock(open: boolean, onClose: () => void) {
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onCloseRef.current();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
}
|
||||
|
||||
/** App 原生风格底部列表选择器 */
|
||||
export function NativePickerSheet({
|
||||
open,
|
||||
title,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
onClose,
|
||||
searchable = false,
|
||||
searchPlaceholder = '搜索',
|
||||
emptyText = '暂无可选项',
|
||||
onBack,
|
||||
annotationId,
|
||||
closeOnSelect = true,
|
||||
}: BaseProps) {
|
||||
const titleId = useId();
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
const [query, setQuery] = useState('');
|
||||
useSheetLock(open, onClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setQuery('');
|
||||
return;
|
||||
}
|
||||
closeRef.current?.focus();
|
||||
}, [open]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return options;
|
||||
return options.filter((item) => (
|
||||
item.label.toLowerCase().includes(q)
|
||||
|| item.value.toLowerCase().includes(q)
|
||||
|| (item.meta || '').toLowerCase().includes(q)
|
||||
));
|
||||
}, [options, query]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="h5-va-sheet-mask h5-va-native-mask" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
className="h5-va-sheet h5-va-native-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
data-annotation-id={annotationId}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="h5-va-sheet-handle" aria-hidden />
|
||||
<div className="h5-va-native-head">
|
||||
{onBack ? (
|
||||
<button
|
||||
ref={closeRef}
|
||||
type="button"
|
||||
className="h5-va-native-head-btn"
|
||||
onClick={onBack}
|
||||
aria-label="返回"
|
||||
>
|
||||
<IconChevronLeft size={20} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
ref={closeRef}
|
||||
type="button"
|
||||
className="h5-va-native-head-btn h5-va-native-head-btn--text"
|
||||
onClick={onClose}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
)}
|
||||
<h2 id={titleId} className="h5-va-native-title">{title}</h2>
|
||||
<button type="button" className="h5-va-native-head-btn" onClick={onClose} aria-label="关闭">
|
||||
<IconX size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{searchable ? (
|
||||
<div className="h5-va-native-search">
|
||||
<IconSearch size={16} />
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
aria-label={searchPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="h5-va-native-list" role="listbox" aria-label={title}>
|
||||
{filtered.length === 0 ? (
|
||||
<div className="h5-va-native-empty">{emptyText}</div>
|
||||
) : (
|
||||
filtered.map((option) => {
|
||||
const selected = option.value === value;
|
||||
return (
|
||||
<button
|
||||
key={option.value || '__all__'}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
className={`h5-va-native-option${selected ? ' is-selected' : ''}`}
|
||||
onClick={() => {
|
||||
onChange(option.value);
|
||||
if (closeOnSelect) onClose();
|
||||
}}
|
||||
>
|
||||
<span className="h5-va-native-option-main">
|
||||
<span className="h5-va-native-option-label">{option.label}</span>
|
||||
{option.meta ? <span className="h5-va-native-option-meta">{option.meta}</span> : null}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="h5-va-native-option-check" aria-hidden>
|
||||
<IconCheck size={18} />
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type BrandModelProps = {
|
||||
open: boolean;
|
||||
brand: string;
|
||||
model: string;
|
||||
brandOptions: Array<{ value: string; count: number }>;
|
||||
getModelOptions: (brand: string) => Array<{ value: string; count: number }>;
|
||||
onConfirm: (next: { brand: string; model: string }) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/** 品牌 → 车型 两级原生选择 */
|
||||
export function BrandModelPickerSheet({
|
||||
open,
|
||||
brand,
|
||||
model,
|
||||
brandOptions,
|
||||
getModelOptions,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: BrandModelProps) {
|
||||
const [step, setStep] = useState<'brand' | 'model'>('brand');
|
||||
const [draftBrand, setDraftBrand] = useState(brand);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setStep('brand');
|
||||
setDraftBrand(brand);
|
||||
}, [open, brand]);
|
||||
|
||||
const brandPickerOptions = useMemo<PickerOption[]>(() => ([
|
||||
{ value: '', label: '全部品牌', meta: '不限' },
|
||||
...brandOptions.map((item) => ({
|
||||
value: item.value,
|
||||
label: item.value,
|
||||
meta: `${item.count} 辆`,
|
||||
})),
|
||||
]), [brandOptions]);
|
||||
|
||||
const modelPickerOptions = useMemo<PickerOption[]>(() => {
|
||||
const models = getModelOptions(draftBrand);
|
||||
return [
|
||||
{ value: '', label: draftBrand ? `全部「${draftBrand}」` : '全部车型', meta: '不限型号' },
|
||||
...models.map((item) => ({
|
||||
value: item.value,
|
||||
label: item.value,
|
||||
meta: `${item.count} 辆`,
|
||||
})),
|
||||
];
|
||||
}, [draftBrand, getModelOptions]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
if (step === 'brand') {
|
||||
return (
|
||||
<NativePickerSheet
|
||||
open
|
||||
title="选择品牌"
|
||||
options={brandPickerOptions}
|
||||
value={brand}
|
||||
searchable
|
||||
searchPlaceholder="搜索品牌"
|
||||
annotationId="h5-va-brand-picker"
|
||||
closeOnSelect={false}
|
||||
onClose={onClose}
|
||||
onChange={(nextBrand) => {
|
||||
if (!nextBrand) {
|
||||
onConfirm({ brand: '', model: '' });
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
setDraftBrand(nextBrand);
|
||||
setStep('model');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NativePickerSheet
|
||||
open
|
||||
title="选择车型"
|
||||
options={modelPickerOptions}
|
||||
value={draftBrand === brand ? model : ''}
|
||||
searchable
|
||||
searchPlaceholder="搜索车型"
|
||||
annotationId="h5-va-model-picker"
|
||||
onBack={() => setStep('brand')}
|
||||
onClose={onClose}
|
||||
onChange={(nextModel) => {
|
||||
onConfirm({ brand: draftBrand, model: nextModel });
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type OperateCityProps = {
|
||||
open: boolean;
|
||||
/** 筛选值:空 | 省 | 省-市 */
|
||||
value: string;
|
||||
provinceOptions: Array<{ value: string; count: number }>;
|
||||
getCityOptions: (province: string) => Array<{ value: string; count: number }>;
|
||||
onConfirm: (next: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/** 运营城市:省 → 市;支持只选省 */
|
||||
export function OperateCityPickerSheet({
|
||||
open,
|
||||
value,
|
||||
provinceOptions,
|
||||
getCityOptions,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: OperateCityProps) {
|
||||
const [step, setStep] = useState<'province' | 'city'>('province');
|
||||
const [draftProvince, setDraftProvince] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setStep('province');
|
||||
const raw = String(value || '').trim().replace(/-+/g, '-');
|
||||
if (!raw) {
|
||||
setDraftProvince('');
|
||||
return;
|
||||
}
|
||||
if (!raw.includes('-')) {
|
||||
setDraftProvince(raw);
|
||||
return;
|
||||
}
|
||||
setDraftProvince(raw.slice(0, raw.indexOf('-')));
|
||||
}, [open, value]);
|
||||
|
||||
const provincePickerOptions = useMemo<PickerOption[]>(() => ([
|
||||
{ value: '', label: '全部省份', meta: '不限' },
|
||||
...provinceOptions.map((item) => ({
|
||||
value: item.value,
|
||||
label: item.value,
|
||||
meta: `${item.count} 辆`,
|
||||
})),
|
||||
]), [provinceOptions]);
|
||||
|
||||
const cityPickerOptions = useMemo<PickerOption[]>(() => {
|
||||
const cities = getCityOptions(draftProvince);
|
||||
return [
|
||||
{
|
||||
value: '',
|
||||
label: draftProvince ? `全部「${draftProvince}」` : '全部城市',
|
||||
meta: '仅按省筛选',
|
||||
},
|
||||
...cities.map((item) => ({
|
||||
value: item.value,
|
||||
label: item.value,
|
||||
meta: `${item.count} 辆`,
|
||||
})),
|
||||
];
|
||||
}, [draftProvince, getCityOptions]);
|
||||
|
||||
const selectedCity = useMemo(() => {
|
||||
const raw = String(value || '').trim().replace(/-+/g, '-');
|
||||
if (!raw.includes('-')) return '';
|
||||
const province = raw.slice(0, raw.indexOf('-'));
|
||||
const city = raw.slice(raw.indexOf('-') + 1);
|
||||
return province === draftProvince ? city : '';
|
||||
}, [value, draftProvince]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
if (step === 'province') {
|
||||
return (
|
||||
<NativePickerSheet
|
||||
open
|
||||
title="选择省份"
|
||||
options={provincePickerOptions}
|
||||
value={value ? draftProvince : ''}
|
||||
searchable
|
||||
searchPlaceholder="搜索省份"
|
||||
annotationId="h5-va-city-picker"
|
||||
closeOnSelect={false}
|
||||
onClose={onClose}
|
||||
onChange={(nextProvince) => {
|
||||
if (!nextProvince) {
|
||||
onConfirm('');
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
setDraftProvince(nextProvince);
|
||||
setStep('city');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NativePickerSheet
|
||||
open
|
||||
title="选择城市"
|
||||
options={cityPickerOptions}
|
||||
value={selectedCity}
|
||||
searchable
|
||||
searchPlaceholder="搜索城市"
|
||||
annotationId="h5-va-city-picker"
|
||||
onBack={() => setStep('province')}
|
||||
onClose={onClose}
|
||||
onChange={(nextCity) => {
|
||||
if (!nextCity) {
|
||||
onConfirm(draftProvince);
|
||||
} else {
|
||||
onConfirm(`${draftProvince}-${nextCity}`);
|
||||
}
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
126
src/prototypes/oneos-h5-vehicle-assets/components/PhoneShell.tsx
Normal file
126
src/prototypes/oneos-h5-vehicle-assets/components/PhoneShell.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
IconBell,
|
||||
IconBriefcase,
|
||||
IconCar,
|
||||
IconMessage,
|
||||
IconPie,
|
||||
IconUser,
|
||||
} from './Icons';
|
||||
import { showAppToast } from './AppToast';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
/** 弹层等覆盖层,挂在手机壳内,避免超出操作区 */
|
||||
overlays?: React.ReactNode;
|
||||
bodyClassName?: string;
|
||||
showBottomNav?: boolean;
|
||||
left?: React.ReactNode;
|
||||
right?: React.ReactNode;
|
||||
headerMode?: 'app' | 'detail';
|
||||
};
|
||||
|
||||
export function PhoneShell({
|
||||
title = '车辆资产',
|
||||
children,
|
||||
overlays,
|
||||
bodyClassName,
|
||||
showBottomNav = false,
|
||||
left,
|
||||
right,
|
||||
headerMode = 'app',
|
||||
}: Props) {
|
||||
const now = new Date();
|
||||
const timeLabel = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
const appHeader = (
|
||||
<header className="h5-nav h5-nav--app">
|
||||
<h1 className="h5-nav-brand">{title}</h1>
|
||||
<div className="h5-nav-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="h5-nav-icon-btn"
|
||||
aria-label="通知"
|
||||
onClick={() => showAppToast('通知中心开发中,本期仅支持查车')}
|
||||
>
|
||||
<IconBell />
|
||||
<span className="h5-nav-badge" aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
const detailHeader = (
|
||||
<header className="h5-nav h5-nav--detail">
|
||||
<div className="h5-nav-side">{left || <span className="h5-nav-side-spacer" aria-hidden />}</div>
|
||||
<h1 className="h5-nav-title">{title}</h1>
|
||||
<div className="h5-nav-side h5-nav-side--end">{right || <span className="h5-nav-side-spacer" aria-hidden />}</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'h5-phone',
|
||||
showBottomNav ? 'h5-phone--tabs' : '',
|
||||
headerMode === 'app' || headerMode === 'detail' ? 'h5-phone--immersive' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<div className="h5-status-bar" aria-hidden>
|
||||
<span>{timeLabel}</span>
|
||||
<span className="h5-island" />
|
||||
<span>5G · 86%</span>
|
||||
</div>
|
||||
{headerMode === 'app' ? appHeader : detailHeader}
|
||||
<main className={`h5-body${bodyClassName ? ` ${bodyClassName}` : ''}`}>{children}</main>
|
||||
{overlays}
|
||||
{showBottomNav ? (
|
||||
<nav className="h5-tabbar" aria-label="App 底部导航">
|
||||
<div className="h5-tabbar-dock">
|
||||
<button type="button" className="h5-tab is-active" aria-current="page">
|
||||
<IconCar size={22} />
|
||||
<span>车辆</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h5-tab"
|
||||
onClick={() => showAppToast('业务模块未接入本页')}
|
||||
>
|
||||
<IconBriefcase size={22} />
|
||||
<span>业务</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h5-tab-center"
|
||||
onClick={() => showAppToast('工作台未接入本页')}
|
||||
aria-label="工作台"
|
||||
title="工作台"
|
||||
>
|
||||
<IconPie size={20} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h5-tab"
|
||||
onClick={() => showAppToast('消息模块未接入本页')}
|
||||
>
|
||||
<span className="h5-tab-icon-wrap">
|
||||
<IconMessage size={22} />
|
||||
<span className="h5-tab-count">12</span>
|
||||
</span>
|
||||
<span>消息</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h5-tab"
|
||||
onClick={() => showAppToast('我的模块未接入本页')}
|
||||
>
|
||||
<IconUser size={22} />
|
||||
<span>我的</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
export type TimelineMetaRow = {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
pending?: boolean;
|
||||
};
|
||||
|
||||
export type TimelineItem = {
|
||||
id: string;
|
||||
date: string;
|
||||
/** 进行中 / 未闭环节点高亮 */
|
||||
accent?: boolean;
|
||||
badge?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
meta?: TimelineMetaRow[];
|
||||
footer?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function RecordTimeline({
|
||||
items,
|
||||
label,
|
||||
}: {
|
||||
items: TimelineItem[];
|
||||
label: string;
|
||||
}) {
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<div className="h5-va-tl" aria-label={label}>
|
||||
{items.map((item, index) => (
|
||||
<article
|
||||
key={item.id}
|
||||
className={`h5-va-tl-item${item.accent ? ' is-open' : ''}`}
|
||||
>
|
||||
<div className="h5-va-tl-axis" aria-hidden>
|
||||
<div className="h5-va-tl-dot" />
|
||||
{index < items.length - 1 ? <div className="h5-va-tl-line" /> : null}
|
||||
</div>
|
||||
<div className="h5-va-tl-main">
|
||||
<div className="h5-va-tl-date">{item.date || '—'}</div>
|
||||
<div className="h5-va-tl-card">
|
||||
<div className="h5-va-tl-title">{item.title || '—'}</div>
|
||||
{item.subtitle ? <div className="h5-va-tl-subtitle">{item.subtitle}</div> : null}
|
||||
{item.meta?.length ? (
|
||||
<div className="h5-va-tl-meta">
|
||||
{item.meta.map((row) => (
|
||||
<div key={row.label} className="h5-va-tl-meta-row">
|
||||
<span className="h5-va-tl-meta-label">{row.label}</span>
|
||||
<span
|
||||
className={`h5-va-tl-meta-value${row.pending ? ' is-pending' : ''}`}
|
||||
>
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{item.footer}
|
||||
{item.badge ? <span className="h5-va-tl-badge">{item.badge}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 取日期时间字符串的排序 / 展示用日期部分 */
|
||||
export function timelineDatePart(value: string): string {
|
||||
const text = (value || '').trim();
|
||||
if (!text || text === '-' || text === '—') return '';
|
||||
return text.slice(0, 10);
|
||||
}
|
||||
|
||||
export function sortByDateDesc<T>(rows: T[], getDate: (row: T) => string): T[] {
|
||||
return [...rows].sort((a, b) => getDate(b).localeCompare(getDate(a)));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { IconEmptyRecords } from './Icons';
|
||||
|
||||
export function RecordsEmpty({
|
||||
title = '暂无记录',
|
||||
desc = '该车辆暂无此类业务记录,可回 PC 录入后同步查看',
|
||||
tip = '演示环境已为多数车辆填充用例数据,刷新后仍为空可换车查看',
|
||||
compact = false,
|
||||
}: {
|
||||
title?: string;
|
||||
desc?: string;
|
||||
tip?: string;
|
||||
/** 嵌套在 Tab 区块内时使用更紧凑的空态 */
|
||||
compact?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`h5-va-empty h5-va-empty--inset${compact ? ' h5-va-empty--compact' : ''} h5-va-section--records`}
|
||||
role="status"
|
||||
>
|
||||
<div className="h5-va-empty-visual" aria-hidden>
|
||||
<span className="h5-va-empty-orb" />
|
||||
<span className="h5-va-empty-ring" />
|
||||
<IconEmptyRecords size={compact ? 56 : 68} className="h5-va-empty-icon" />
|
||||
</div>
|
||||
<p className="h5-va-empty-title">{title}</p>
|
||||
{desc ? <p className="h5-va-empty-desc">{desc}</p> : null}
|
||||
{tip ? <p className="h5-va-empty-tip">{tip}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user