Compare commits
2 Commits
84a25c42df
...
a01d2ab708
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a01d2ab708 | ||
|
|
e39df1c7c8 |
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]
|
||||||
|
```
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"server": {
|
"server": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"allowLAN": true,
|
"allowLAN": true,
|
||||||
|
"lanHost": "192.168.0.102",
|
||||||
"enableCommandAPI": false
|
"enableCommandAPI": false
|
||||||
},
|
},
|
||||||
"projectDefaults": {
|
"projectDefaults": {
|
||||||
|
|||||||
@@ -1,78 +1,23 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-07-20T05:34:45.927Z",
|
"updatedAt": "2026-07-21T08:13:15.367Z",
|
||||||
"prototypes": [
|
"prototypes": [
|
||||||
{
|
|
||||||
"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",
|
"id": "folder-1782874576229-r8atbu",
|
||||||
"kind": "folder",
|
"kind": "folder",
|
||||||
"title": "OneOS",
|
"title": "OneOS",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "item:prototypes:oneos-web-workbench",
|
"id": "item:prototypes:oneos-prototype-demo",
|
||||||
|
"kind": "item",
|
||||||
|
"title": "原型演示",
|
||||||
|
"itemKey": "prototypes/oneos-prototype-demo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "item:prototypes:oneos-web-workbench-new",
|
||||||
"kind": "item",
|
"kind": "item",
|
||||||
"title": "工作台",
|
"title": "工作台",
|
||||||
"itemKey": "prototypes/oneos-web-workbench"
|
"itemKey": "prototypes/oneos-web-workbench-new"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "folder-oneos-approval",
|
"id": "folder-oneos-approval",
|
||||||
@@ -141,6 +86,12 @@
|
|||||||
"title": "租赁合同",
|
"title": "租赁合同",
|
||||||
"itemKey": "prototypes/lease-contract-management"
|
"itemKey": "prototypes/lease-contract-management"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "item:prototypes:self-operated-contract",
|
||||||
|
"kind": "item",
|
||||||
|
"title": "自营合同",
|
||||||
|
"itemKey": "prototypes/self-operated-contract"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "item:prototypes:insurance-procurement",
|
"id": "item:prototypes:insurance-procurement",
|
||||||
"kind": "item",
|
"kind": "item",
|
||||||
@@ -173,12 +124,6 @@
|
|||||||
"title": "站点信息",
|
"title": "站点信息",
|
||||||
"itemKey": "prototypes/oneos-web-h2-station-site"
|
"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",
|
"id": "item:prototypes:oneos-web-h2-station",
|
||||||
"kind": "item",
|
"kind": "item",
|
||||||
@@ -192,9 +137,9 @@
|
|||||||
"itemKey": "prototypes/oneos-web-h2-station-weekly"
|
"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",
|
"kind": "item",
|
||||||
"title": "oneos web h2 station analysis",
|
"title": "加氢站分析",
|
||||||
"itemKey": "prototypes/oneos-web-h2-station-analysis"
|
"itemKey": "prototypes/oneos-web-h2-station-analysis"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -296,6 +241,12 @@
|
|||||||
"kind": "item",
|
"kind": "item",
|
||||||
"title": "任务工单",
|
"title": "任务工单",
|
||||||
"itemKey": "prototypes/task-work-order"
|
"itemKey": "prototypes/task-work-order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "item:prototypes:self-operated-dispatch-task",
|
||||||
|
"kind": "item",
|
||||||
|
"title": "调度任务",
|
||||||
|
"itemKey": "prototypes/self-operated-dispatch-task"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -303,22 +254,101 @@
|
|||||||
"id": "folder-1784341576489-spa7ms",
|
"id": "folder-1784341576489-spa7ms",
|
||||||
"kind": "folder",
|
"kind": "folder",
|
||||||
"title": "审批中心",
|
"title": "审批中心",
|
||||||
"children": [
|
"children": []
|
||||||
{
|
|
||||||
"id": "item:prototypes:oneos-web-approval",
|
|
||||||
"kind": "item",
|
|
||||||
"title": "审批中心",
|
|
||||||
"itemKey": "prototypes/oneos-web-approval"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "item:prototypes:lease-business-line-overview",
|
"id": "item:prototypes:lease-business-line-overview",
|
||||||
"kind": "item",
|
"kind": "item",
|
||||||
"title": "业务条线说明",
|
"title": "业务条线说明",
|
||||||
"itemKey": "prototypes/lease-business-line-overview"
|
"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": [],
|
"docs": [],
|
||||||
@@ -1105,4 +1135,4 @@
|
|||||||
"templates": [],
|
"templates": [],
|
||||||
"components": [],
|
"components": [],
|
||||||
"canvas": []
|
"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())
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 项目信息
|
||||||
|
|
||||||
|
- 项目名称:OneOS1.2
|
||||||
|
|
||||||
# Agent 工作流程
|
# Agent 工作流程
|
||||||
|
|
||||||
## 🧭 核心顺序
|
## 🧭 核心顺序
|
||||||
@@ -21,6 +25,7 @@
|
|||||||
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
|
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
|
||||||
| 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw`、`canvas-assets/` | 原型画布和画布素材 |
|
| 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw`、`canvas-assets/` | 原型画布和画布素材 |
|
||||||
| 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` |
|
| 业务逻辑规格 | `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` |
|
| 对象存储发布 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` |
|
| 原型标注布局 | `src/common/prototype-annotation-host.tsx` | `rules/prototype-annotation-layout-guide.md` |
|
||||||
|
|
||||||
|
|||||||
53
CLAUDE.md
53
CLAUDE.md
@@ -1,13 +1,20 @@
|
|||||||
# Agents 工作流程说明
|
## 项目信息
|
||||||
|
|
||||||
## 🧭 工作流程
|
- 项目名称:OneOS1.2
|
||||||
|
|
||||||
| 步骤 | 说明 | 参考文档 |
|
# Agent 工作流程
|
||||||
|------|------|----------|
|
|
||||||
| ① 读取上下文 | 系统规则、用户资料、相关规范、已有原型与资源目录 | — |
|
## 🧭 核心顺序
|
||||||
| ② 产品需求对齐 | 新建原型、明显重构或需求模糊时,先收敛目标用户、核心任务、范围、功能清单、内容来源和验收重点 | `rules/requirements-alignment-guide.md` |
|
|
||||||
| ③ 设计方案对齐 | 产品需求确认后,先让用户从 3-4 个匹配的 `DESIGN.md` 候选中确认设计基底,再把布局、交互、视觉和内容呈现收敛为设计决策 | `rules/requirements-alignment-guide.md` |
|
```text
|
||||||
| ④ 原型开发与验收 | 根据已确认方案实现原型;遇到问题按错误信息定位修复,并完成预览验收 | `rules/prototype-development-guide.md` |
|
产品需求确认 -> 设计方案确认 -> 原型实现
|
||||||
|
```
|
||||||
|
|
||||||
|
| 阶段 | 继续前必须确认 | 参考文档 |
|
||||||
|
|------|----------------|----------|
|
||||||
|
| 产品需求 | 目标用户、核心任务、范围、功能清单、内容来源和验收重点 | `rules/requirements-alignment-guide.md` |
|
||||||
|
| 设计方案 | `DESIGN.md` 设计基底、信息架构、交互路径、关键组件取舍和视觉方向 | `rules/requirements-alignment-guide.md` |
|
||||||
|
| 原型实现 | 根据已确认的需求和设计方案实现原型 | `rules/prototype-development-guide.md` |
|
||||||
|
|
||||||
## 额外产物
|
## 额外产物
|
||||||
|
|
||||||
@@ -16,39 +23,35 @@
|
|||||||
| vm-page 列表分页 | `src/common/TablePagination.tsx` + `vm-pagination.css` | `src/prototypes/vm-shared/DESIGN.md` |
|
| vm-page 列表分页 | `src/common/TablePagination.tsx` + `vm-pagination.css` | `src/prototypes/vm-shared/DESIGN.md` |
|
||||||
| 主题 | `src/themes/<theme-key>/` | `rules/theme-guide.md` |
|
| 主题 | `src/themes/<theme-key>/` | `rules/theme-guide.md` |
|
||||||
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
|
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
|
||||||
| UI Review 结论 | `src/prototypes/<prototype-name>/.spec/ui-review.md` | `rules/ui-review-guide.md` |
|
| 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw`、`canvas-assets/` | 原型画布和画布素材 |
|
||||||
| 原型 Review 结论 | `src/prototypes/<prototype-name>/.spec/prototype-review.md` | `rules/prototype-review-guide.md` |
|
|
||||||
| 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` |
|
| 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` |
|
||||||
| 对象存储发布 URL | `.axhub/make/axhub.config.json` → `cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` |
|
| 对象存储发布 URL | `.axhub/make/axhub.config.json` → `cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` |
|
||||||
| ACP 对话缓存 | `src/prototypes/<prototype-name>/.spec/acp/` | 本地私有运行数据,不提交、不导出、不发布 |
|
| 原型标注布局 | `src/common/prototype-annotation-host.tsx` | `rules/prototype-annotation-layout-guide.md` |
|
||||||
|
|
||||||
## ⚠️ 重要原则
|
## ⚠️ 重要原则
|
||||||
|
|
||||||
1. **产品需求和设计方案分阶段对齐**
|
1. **需求与设计是继续工作的门禁**
|
||||||
- 先确认做什么,再确认怎么表达;读取资料、规格/计划确认和开发验收过程中,发现影响方向的问题都要回到相应阶段继续对齐
|
- 产品需求未确认时,禁止继续找视觉参考、拆页面结构、整理 `DESIGN.md` 候选、写设计方案或开发实现;先用简短摘要让用户确认目标用户、核心任务、范围/功能清单、内容来源和验收重点
|
||||||
|
- 设计方案未确认时,禁止继续写规格/计划或开发实现;先确认设计基底、信息架构、交互路径、关键组件取舍和视觉方向
|
||||||
|
- 发现目标、范围、内容来源、验收重点、信息架构、交互路径、视觉方向或设计基底存在多种合理选择时,立即停在对应门禁补充对齐
|
||||||
|
|
||||||
| 阶段 | 需要对齐的情况 |
|
2. **设计要判断何时收敛、何时发散**
|
||||||
|------|----------------|
|
|
||||||
| 读取资料 | 目标、边界、素材、参考或约束不清 |
|
|
||||||
| 产品需求 | 出现不同目标用户、功能范围、内容来源或验收标准 |
|
|
||||||
| 设计方案 | 出现不同信息架构、交互路径、视觉方向或设计基底 |
|
|
||||||
| 开发验收 | 实现结果、体验取舍或验收标准发生变化 |
|
|
||||||
|
|
||||||
2. **优先创建和维护 task/todo**
|
|
||||||
- 多步骤、高风险、需求对齐、方案确认或跨文件任务,优先用 task/todo 记录当前步骤、状态和下一步
|
|
||||||
- 简单局部修改可以保持轻量,但要清楚说明当前正在处理什么、完成后如何验收
|
|
||||||
3. **设计要判断何时收敛、何时发散**
|
|
||||||
- AI 应自行判断当前需要收拢需求还是探索解法:需求不清先收敛;需要改善体验或创新表达时再发散。发散是为了帮助用户选择最终方向
|
- AI 应自行判断当前需要收拢需求还是探索解法:需求不清先收敛;需要改善体验或创新表达时再发散。发散是为了帮助用户选择最终方向
|
||||||
|
3. **原型按生产级界面处理**
|
||||||
|
- 本项目中的「原型」默认是可运行、接近正式产品的前端页面,不是黑白灰线框图或低保真草稿;只有用户明确要求时才使用低保真、wireframe、placeholder 等表达
|
||||||
4. **不要把截图当唯一真相**
|
4. **不要把截图当唯一真相**
|
||||||
- 截图用于视觉参考;有代码、组件、设计系统、业务资料或用户说明时,要结合上下文判断
|
- 截图用于视觉参考;有代码、组件、设计系统、业务资料或用户说明时,要结合上下文判断
|
||||||
5. **早展示,早反馈**
|
5. **早展示,早反馈**
|
||||||
- 产品需求、设计方案或原型应尽早交给用户确认,不要等到全部完成后才暴露方向问题
|
- 产品需求、设计方案或原型应尽早交给用户确认,不要等到全部完成后才暴露方向问题
|
||||||
|
- 涉及页面意图、组件取舍或多方案比稿时,优先用低成本、快速的 Markdown ASCII Wireframe/Diagram 或 Mermaid 展示方案,先对齐需求
|
||||||
6. **讲人话,用户不懂技术**
|
6. **讲人话,用户不懂技术**
|
||||||
- 用用户能理解的方式说明取舍、风险和结果;用户无法执行 CLI 命令,不得省略验收流程
|
- 用用户能理解的方式说明取舍、风险和结果;用户无法执行 CLI 命令,不得省略验收流程
|
||||||
|
- 向用户请求反馈或验收时,提醒用户尽量提供截图、预览链接、页面路径或具体问题位置,便于准确定位和复现
|
||||||
7. **复杂业务逻辑必须文档化**
|
7. **复杂业务逻辑必须文档化**
|
||||||
- 多步判定、跨模块校验、状态/公式推导、导入规则等,除代码外必须写入 `.spec/*.md` 与 `requirements-prd.md`,并同步标注目录(见 `rules/business-logic-documentation-guide.md`)
|
- 多步判定、跨模块校验、状态/公式推导、导入规则等,除代码外必须写入 `.spec/*.md` 与 `requirements-prd.md`,并同步标注目录(见 `rules/business-logic-documentation-guide.md`)
|
||||||
|
- 不得默认「先实现、后补文档」;逻辑变更与 PRD/标注更新同一轮交付
|
||||||
8. **对象存储发布 URL 与 Make 工具一致**
|
8. **对象存储发布 URL 与 Make 工具一致**
|
||||||
- `{baseUrl}/{prototype-id}/index.html`;禁止擅自改路径后缀(见 `rules/cloud-publish-url-guide.md`)
|
- 发布或写链接时格式为 `{baseUrl}/{prototype-id}/index.html`;禁止加 `prototypes/` 前缀或去掉 `index.html`(见 `rules/cloud-publish-url-guide.md`)
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
|
|||||||
736
DESIGN.md
736
DESIGN.md
@@ -1,736 +0,0 @@
|
|||||||
---
|
|
||||||
version: alpha
|
|
||||||
name: Vercel-design-analysis
|
|
||||||
description: An inspired interpretation of Vercel's design language — a developer-platform brand whose surface is a stark black-and-ink duet on near-white canvas, broken at hero scale by a multi-color mesh gradient (cyan / blue / magenta / amber) that acts as the entire decorative system, paired with a custom geometric sans for headlines and a monospaced caption face for technical labels.
|
|
||||||
|
|
||||||
colors:
|
|
||||||
primary: "#171717"
|
|
||||||
on-primary: "#ffffff"
|
|
||||||
ink: "#171717"
|
|
||||||
body: "#4d4d4d"
|
|
||||||
mute: "#888888"
|
|
||||||
hairline: "#ebebeb"
|
|
||||||
hairline-strong: "#a1a1a1"
|
|
||||||
canvas: "#ffffff"
|
|
||||||
canvas-soft: "#fafafa"
|
|
||||||
canvas-soft-2: "#f5f5f5"
|
|
||||||
link: "#0070f3"
|
|
||||||
link-deep: "#0761d1"
|
|
||||||
link-bg-soft: "#d3e5ff"
|
|
||||||
success: "#0070f3"
|
|
||||||
error: "#ee0000"
|
|
||||||
error-soft: "#f7d4d6"
|
|
||||||
error-deep: "#c50000"
|
|
||||||
warning: "#f5a623"
|
|
||||||
warning-soft: "#ffefcf"
|
|
||||||
warning-deep: "#ab570a"
|
|
||||||
violet: "#7928ca"
|
|
||||||
violet-soft: "#d8ccf1"
|
|
||||||
violet-deep: "#4c2889"
|
|
||||||
cyan: "#50e3c2"
|
|
||||||
cyan-soft: "#aaffec"
|
|
||||||
cyan-deep: "#29bc9b"
|
|
||||||
highlight-pink: "#ff0080"
|
|
||||||
highlight-magenta: "#eb367f"
|
|
||||||
gradient-develop-start: "#007cf0"
|
|
||||||
gradient-develop-end: "#00dfd8"
|
|
||||||
gradient-preview-start: "#7928ca"
|
|
||||||
gradient-preview-end: "#ff0080"
|
|
||||||
gradient-ship-start: "#ff4d4d"
|
|
||||||
gradient-ship-end: "#f9cb28"
|
|
||||||
selection-bg: "#171717"
|
|
||||||
selection-fg: "#f2f2f2"
|
|
||||||
|
|
||||||
typography:
|
|
||||||
display-xl:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 48px
|
|
||||||
fontWeight: 600
|
|
||||||
lineHeight: 48px
|
|
||||||
letterSpacing: -2.4px
|
|
||||||
display-lg:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 32px
|
|
||||||
fontWeight: 600
|
|
||||||
lineHeight: 40px
|
|
||||||
letterSpacing: -1.28px
|
|
||||||
display-md:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 24px
|
|
||||||
fontWeight: 600
|
|
||||||
lineHeight: 32px
|
|
||||||
letterSpacing: -0.96px
|
|
||||||
display-sm:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 20px
|
|
||||||
fontWeight: 600
|
|
||||||
lineHeight: 28px
|
|
||||||
letterSpacing: -0.6px
|
|
||||||
body-lg:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 18px
|
|
||||||
fontWeight: 400
|
|
||||||
lineHeight: 28px
|
|
||||||
letterSpacing: 0px
|
|
||||||
body-md:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 16px
|
|
||||||
fontWeight: 400
|
|
||||||
lineHeight: 24px
|
|
||||||
body-md-strong:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 16px
|
|
||||||
fontWeight: 500
|
|
||||||
lineHeight: 24px
|
|
||||||
body-sm:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 14px
|
|
||||||
fontWeight: 400
|
|
||||||
lineHeight: 20px
|
|
||||||
letterSpacing: -0.28px
|
|
||||||
body-sm-strong:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 14px
|
|
||||||
fontWeight: 500
|
|
||||||
lineHeight: 20px
|
|
||||||
letterSpacing: -0.28px
|
|
||||||
caption:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 12px
|
|
||||||
fontWeight: 400
|
|
||||||
lineHeight: 16px
|
|
||||||
caption-mono:
|
|
||||||
fontFamily: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, monospace
|
|
||||||
fontSize: 12px
|
|
||||||
fontWeight: 400
|
|
||||||
lineHeight: 16px
|
|
||||||
code:
|
|
||||||
fontFamily: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, monospace
|
|
||||||
fontSize: 13px
|
|
||||||
fontWeight: 400
|
|
||||||
lineHeight: 20px
|
|
||||||
button-md:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 14px
|
|
||||||
fontWeight: 500
|
|
||||||
lineHeight: 20px
|
|
||||||
button-lg:
|
|
||||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
|
||||||
fontSize: 16px
|
|
||||||
fontWeight: 500
|
|
||||||
lineHeight: 24px
|
|
||||||
|
|
||||||
rounded:
|
|
||||||
none: 0px
|
|
||||||
xs: 4px
|
|
||||||
sm: 6px
|
|
||||||
md: 8px
|
|
||||||
lg: 12px
|
|
||||||
xl: 16px
|
|
||||||
pill-sm: 64px
|
|
||||||
pill: 100px
|
|
||||||
full: 9999px
|
|
||||||
|
|
||||||
spacing:
|
|
||||||
xxs: 4px
|
|
||||||
xs: 8px
|
|
||||||
sm: 12px
|
|
||||||
md: 16px
|
|
||||||
lg: 24px
|
|
||||||
xl: 32px
|
|
||||||
2xl: 40px
|
|
||||||
3xl: 48px
|
|
||||||
4xl: 64px
|
|
||||||
5xl: 96px
|
|
||||||
6xl: 128px
|
|
||||||
section: 192px
|
|
||||||
|
|
||||||
components:
|
|
||||||
nav-bar:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
height: 64px
|
|
||||||
padding: "{spacing.sm} {spacing.lg}"
|
|
||||||
nav-link:
|
|
||||||
textColor: "{colors.body}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
rounded: "{rounded.full}"
|
|
||||||
padding: "{spacing.xs} {spacing.sm}"
|
|
||||||
nav-cta-signup:
|
|
||||||
backgroundColor: "{colors.primary}"
|
|
||||||
textColor: "{colors.on-primary}"
|
|
||||||
typography: "{typography.body-sm-strong}"
|
|
||||||
rounded: "{rounded.sm}"
|
|
||||||
padding: "0px {spacing.xs}"
|
|
||||||
height: 28px
|
|
||||||
nav-cta-login:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-sm-strong}"
|
|
||||||
rounded: "{rounded.sm}"
|
|
||||||
padding: "0px {spacing.xs}"
|
|
||||||
height: 28px
|
|
||||||
nav-cta-ask-ai:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
borderColor: "{colors.hairline}"
|
|
||||||
typography: "{typography.body-sm-strong}"
|
|
||||||
rounded: "{rounded.sm}"
|
|
||||||
padding: "0px {spacing.xs}"
|
|
||||||
height: 28px
|
|
||||||
button-primary:
|
|
||||||
backgroundColor: "{colors.primary}"
|
|
||||||
textColor: "{colors.on-primary}"
|
|
||||||
typography: "{typography.button-lg}"
|
|
||||||
rounded: "{rounded.pill}"
|
|
||||||
padding: "0px {spacing.sm}"
|
|
||||||
button-secondary:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.button-lg}"
|
|
||||||
rounded: "{rounded.pill}"
|
|
||||||
padding: "0px {spacing.sm}"
|
|
||||||
button-primary-sm:
|
|
||||||
backgroundColor: "{colors.primary}"
|
|
||||||
textColor: "{colors.on-primary}"
|
|
||||||
typography: "{typography.button-md}"
|
|
||||||
rounded: "{rounded.pill}"
|
|
||||||
padding: "0px {spacing.xs}"
|
|
||||||
button-secondary-sm:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.button-md}"
|
|
||||||
rounded: "{rounded.pill}"
|
|
||||||
padding: "0px {spacing.xs}"
|
|
||||||
tab-ghost:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
rounded: "{rounded.pill-sm}"
|
|
||||||
padding: "0px {spacing.md}"
|
|
||||||
icon-button-circular:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
borderColor: "{colors.hairline}"
|
|
||||||
rounded: "{rounded.full}"
|
|
||||||
card-marketing:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
rounded: "{rounded.md}"
|
|
||||||
padding: "{spacing.lg}"
|
|
||||||
card-marketing-large:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.xl}"
|
|
||||||
card-soft:
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
rounded: "{rounded.md}"
|
|
||||||
padding: "{spacing.lg}"
|
|
||||||
template-card:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
rounded: "{rounded.md}"
|
|
||||||
padding: "{spacing.md}"
|
|
||||||
code-editor-mockup:
|
|
||||||
backgroundColor: "{colors.primary}"
|
|
||||||
textColor: "{colors.on-primary}"
|
|
||||||
typography: "{typography.code}"
|
|
||||||
rounded: "{rounded.md}"
|
|
||||||
padding: "{spacing.lg}"
|
|
||||||
form-input:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
borderColor: "{colors.hairline}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
rounded: "{rounded.sm}"
|
|
||||||
padding: "0px {spacing.sm}"
|
|
||||||
height: 40px
|
|
||||||
form-input-sm:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
borderColor: "{colors.hairline}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
rounded: "{rounded.sm}"
|
|
||||||
padding: "0px {spacing.sm}"
|
|
||||||
height: 32px
|
|
||||||
form-input-lg:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
borderColor: "{colors.hairline}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
rounded: "{rounded.sm}"
|
|
||||||
padding: "0px {spacing.sm}"
|
|
||||||
height: 48px
|
|
||||||
badge-secondary:
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
textColor: "{colors.body}"
|
|
||||||
typography: "{typography.caption}"
|
|
||||||
rounded: "{rounded.full}"
|
|
||||||
padding: "0px {spacing.xs}"
|
|
||||||
pricing-card:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.xl}"
|
|
||||||
pricing-card-featured:
|
|
||||||
backgroundColor: "{colors.primary}"
|
|
||||||
textColor: "{colors.on-primary}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.xl}"
|
|
||||||
logo-strip:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.body}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
padding: "{spacing.lg} {spacing.xl}"
|
|
||||||
hero-band:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.display-xl}"
|
|
||||||
padding: "{spacing.4xl} {spacing.lg}"
|
|
||||||
feature-mesh-band:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.display-lg}"
|
|
||||||
padding: "{spacing.5xl} {spacing.lg}"
|
|
||||||
showcase-band-light:
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
typography: "{typography.display-lg}"
|
|
||||||
padding: "{spacing.5xl} {spacing.lg}"
|
|
||||||
showcase-band-dark:
|
|
||||||
backgroundColor: "{colors.primary}"
|
|
||||||
textColor: "{colors.on-primary}"
|
|
||||||
typography: "{typography.display-lg}"
|
|
||||||
padding: "{spacing.5xl} {spacing.lg}"
|
|
||||||
footer:
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
textColor: "{colors.body}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
padding: "{spacing.4xl} {spacing.lg}"
|
|
||||||
link-inline:
|
|
||||||
textColor: "{colors.link}"
|
|
||||||
typography: "{typography.body-md}"
|
|
||||||
banner-marketing:
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
textColor: "{colors.body}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
rounded: "{rounded.full}"
|
|
||||||
padding: "{spacing.xs} {spacing.sm}"
|
|
||||||
|
|
||||||
# ─── Examples (illustrative) — auto-derived; resolve any TO_FILL markers below ───
|
|
||||||
ex-pricing-tier:
|
|
||||||
description: "Default tier card. Mirrors pricing-card chrome on canvas-soft surface with a hairline border."
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
textColor: "{colors.ink}"
|
|
||||||
borderColor: "{colors.hairline}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.xl}"
|
|
||||||
ex-pricing-tier-featured:
|
|
||||||
description: "Featured tier — polarity-flipped to ink primary with white text and white CTA."
|
|
||||||
backgroundColor: "{colors.ink}"
|
|
||||||
textColor: "{colors.on-primary}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.xl}"
|
|
||||||
ex-product-selector:
|
|
||||||
description: "What's Included summary card — repurposed for the brand's GPU / inference / Pro feature tiers."
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
rounded: "{rounded.md}"
|
|
||||||
padding: "{spacing.lg}"
|
|
||||||
ex-cart-drawer:
|
|
||||||
description: "Subscription summary — line items per add-on (NOT a literal e-commerce cart)."
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
rounded: "{rounded.md}"
|
|
||||||
padding: "{spacing.lg}"
|
|
||||||
item-divider: "{colors.hairline}"
|
|
||||||
ex-app-shell-row:
|
|
||||||
description: "Sidebar nav row. Active state uses brand primary as a left-edge indicator bar."
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
activeIndicator: "{colors.primary}"
|
|
||||||
rounded: "{rounded.sm}"
|
|
||||||
padding: "{spacing.xs} {spacing.sm}"
|
|
||||||
ex-data-table-cell:
|
|
||||||
description: "Mirrors the brand's table chrome. Header uses caption-mono uppercase mono; body uses body-sm."
|
|
||||||
headerBackground: "{colors.canvas-soft}"
|
|
||||||
headerTypography: "{typography.caption-mono}"
|
|
||||||
bodyTypography: "{typography.body-sm}"
|
|
||||||
cellPadding: "{spacing.xs} {spacing.sm}"
|
|
||||||
rowBorder: "{colors.hairline}"
|
|
||||||
ex-auth-form-card:
|
|
||||||
description: "Sign-in / sign-up card. Mirrors card-marketing-large chrome with form-input primitives inside."
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.xl}"
|
|
||||||
ex-modal-card:
|
|
||||||
description: "Modal dialog surface — same chrome as card-marketing-large with Level 5 modal shadow."
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.xl}"
|
|
||||||
ex-empty-state-card:
|
|
||||||
description: "Empty-state illustration frame. Generous padding on canvas-soft."
|
|
||||||
backgroundColor: "{colors.canvas-soft}"
|
|
||||||
rounded: "{rounded.lg}"
|
|
||||||
padding: "{spacing.3xl}"
|
|
||||||
captionTypography: "{typography.body-md}"
|
|
||||||
ex-toast:
|
|
||||||
description: "Toast notification surface — flat-cornered card-marketing chrome with Level 4 shadow."
|
|
||||||
backgroundColor: "{colors.canvas}"
|
|
||||||
rounded: "{rounded.md}"
|
|
||||||
padding: "{spacing.sm} {spacing.md}"
|
|
||||||
typography: "{typography.body-sm}"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Vercel is a developer-platform brand — the page is a deployment dashboard's marketing surface, written for engineers who already know the syntax. It earns that posture with one of the cleanest stark systems on the web: near-white `{colors.canvas-soft}` body background, ink-near-black `{colors.ink}` text, a 200-step gray scale that gives every divider, border, and disabled state its own deliberate step. The only place the brand introduces colour at marketing scale is the multi-stop mesh gradient (`{colors.gradient-develop-start}` → `{colors.gradient-preview-end}` → `{colors.gradient-ship-start}` → cyan / magenta / amber) that floats in atmospheric backdrops, never miniaturised to a swatch. That gradient is the entire decoration system.
|
|
||||||
|
|
||||||
Type is the second decisive voice. The brand's own custom geometric sans (Geist) carries display, body, button — everything narrative — at weight 600 for display, 500 for buttons, 400 for body. A matching monospaced face (Geist Mono) carries technical labels: terminal mockups, code blocks, sometimes filename captions. Headlines are sentence-case with aggressive negative letter-spacing (`-2.4px` at 48 px hero) — the brand never letter-spaces positively, never goes uppercase outside of mono labels.
|
|
||||||
|
|
||||||
Surfaces use a four-step ladder: `{colors.canvas}` (pure white for cards), `{colors.canvas-soft}` 98% (the page body), `{colors.canvas-soft-2}` 95% (occasional inset region), `{colors.primary}` (the deep ink-near-black used as the polarity-flipped band when a section needs the dark mode treatment). Shadows are exceptionally subtle — every elevated card carries a stacked shadow built from `0px 1px 1px #00000005` + `0px 2px 2px #0000000a` + an inset border. Cards never float on heavy drop-shadow; they sit on the page held by hairline + soft glow.
|
|
||||||
|
|
||||||
**Key Characteristics:**
|
|
||||||
- A single black-ink primary CTA `{colors.primary}` carries every conversion target, paired with white-on-white `button-secondary` for the secondary action. The brand uses 100 px pill shape for marketing CTAs and a tight 6 px square shape for in-app nav buttons.
|
|
||||||
- A multi-stop mesh gradient (cyan-blue-magenta-amber) is the only decorative chrome — used at hero scale and inside feature-band atmospheric backdrops. It is the brand.
|
|
||||||
- Every section eyebrow and small label uses the monospace face `{typography.caption-mono}` or `{typography.code}`; everything else is in the geometric sans.
|
|
||||||
- Subtle stacked-shadow elevation — three offsets layered with 4-12 % black opacity — never a single heavy drop-shadow.
|
|
||||||
- A complete 100–1000 gray + blue + red + amber + green + teal + purple + pink colour scale exists as a system token set, but the marketing surface uses only the `100`, `1000`, and `700`-level tones; the rest stay in the design-system tokens for in-product surfaces.
|
|
||||||
- An "Active CPU" pricing rhythm: `pricing-card` lays out 3-up on the pricing page with `pricing-card-featured` (Pro tier) polarity-flipped to `{colors.primary}` against white-card siblings.
|
|
||||||
|
|
||||||
## Colors
|
|
||||||
|
|
||||||
### Brand & Accent
|
|
||||||
- **Ink** (`{colors.primary}` — `#171717`): The single primary CTA color. Black-near-pure ink that carries every Sign Up pill, every footer CTA, the dark-band polarity-flip. Used as text color throughout the page on light surfaces. (Resolved from `--ds-gray-1000`.)
|
|
||||||
- **Cyan** (`{colors.cyan}` — `#50e3c2`): A signature mint-cyan used in the brand gradient and inside Geist-system spotlight tokens. Visible inside the hero gradient stops.
|
|
||||||
- **Highlight Pink** (`{colors.highlight-pink}` — `#ff0080`): The brand's highlight magenta, used as the high-saturation stop in the preview-gradient pair.
|
|
||||||
- **Violet** (`{colors.violet}` — `#7928ca`): The deep purple used as the start of the preview-gradient and inside developer-console highlights.
|
|
||||||
- **Link Blue** (`{colors.link}` — `#0070f3`): The brand's primary link color and the legacy `--geist-success` semantic.
|
|
||||||
|
|
||||||
### Surface
|
|
||||||
- **Canvas** (`{colors.canvas}` — `#ffffff`): The pure-white card / dialog / modal surface.
|
|
||||||
- **Canvas Soft** (`{colors.canvas-soft}` — `#fafafa`): The default page background — 98 % white. Almost every section sits on this tone.
|
|
||||||
- **Canvas Soft 2** (`{colors.canvas-soft-2}` — `#f5f5f5`): A slightly deeper inset surface for "code editor inner background", template-card hover states, and dropdown menus.
|
|
||||||
- **Hairline** (`{colors.hairline}` — `#ebebeb`): 1 px dividers — table rows, card borders, input borders.
|
|
||||||
- **Hairline Strong** (`{colors.hairline-strong}` — `#a1a1a1`): The 500-level gray, used as the slightly-stronger divider on light bands and as the deemphasised text color.
|
|
||||||
|
|
||||||
### Text
|
|
||||||
- **Ink** (`{colors.ink}` — `#171717`): Every heading and body paragraph on light surfaces.
|
|
||||||
- **Body** (`{colors.body}` — `#4d4d4d`): Secondary text — sub-headings, body captions, nav-link inactive text, footer column body.
|
|
||||||
- **Mute** (`{colors.mute}` — `#888888`): Lowest-priority text — placeholder text, fine print, low-key labels.
|
|
||||||
- **On Primary** (`{colors.on-primary}` — `#ffffff`): All text on `{colors.primary}` surfaces.
|
|
||||||
|
|
||||||
### Semantic
|
|
||||||
- **Success / Link** (`{colors.success}` — `#0070f3`): The brand's legacy success indicator doubles as the primary link color. Visible underline-on-hover for inline body links.
|
|
||||||
- **Link Deep** (`{colors.link-deep}` — `#0761d1`): The pressed / visited tone for inline links.
|
|
||||||
- **Link Bg Soft** (`{colors.link-bg-soft}` — `#d3e5ff`): Soft pastel blue fill for "what's new" pill banners and informational badges.
|
|
||||||
- **Error** (`{colors.error}` — `#ee0000`): Validation red for destructive actions and form errors.
|
|
||||||
- **Error Soft** (`{colors.error-soft}` — `#f7d4d6`): Soft pastel red for destructive-state backgrounds.
|
|
||||||
- **Error Deep** (`{colors.error-deep}` — `#c50000`): Pressed / deep destructive state.
|
|
||||||
- **Warning** (`{colors.warning}` — `#f5a623`): Caution / pending status indicator.
|
|
||||||
- **Warning Soft** (`{colors.warning-soft}` — `#ffefcf`) / **Warning Deep** (`{colors.warning-deep}` — `#ab570a`): Background + pressed variants.
|
|
||||||
|
|
||||||
### Brand Gradient
|
|
||||||
The brand's signature decoration is a three-pair gradient stack:
|
|
||||||
- **Develop** (`{colors.gradient-develop-start}` `#007cf0` → `{colors.gradient-develop-end}` `#00dfd8`) — the blue-to-teal pair used to mark the "deploy" / "develop" rhythm.
|
|
||||||
- **Preview** (`{colors.gradient-preview-start}` `#7928ca` → `{colors.gradient-preview-end}` `#ff0080`) — the violet-to-pink pair used for "preview" surfaces.
|
|
||||||
- **Ship** (`{colors.gradient-ship-start}` `#ff4d4d` → `{colors.gradient-ship-end}` `#f9cb28`) — the coral-to-amber pair used for "ship" surfaces.
|
|
||||||
|
|
||||||
The three pairs collapse into a single multi-color mesh gradient when used as the hero atmospheric backdrop. Treat the gradient as one unified object — do not crop down to a single colour, do not reorder the stops, and do not miniaturise. Used at hero scale only.
|
|
||||||
|
|
||||||
## Typography
|
|
||||||
|
|
||||||
### Font Family
|
|
||||||
Two custom faces carry the entire system:
|
|
||||||
|
|
||||||
1. **A custom geometric sans** (extracted as `Geist`) for every display, body, button, link, and label. Weights 400 / 500 / 600 are the working set; the face never appears in 700 or heavier. Display sizes are tracked aggressively negative (`-2.4 px` at 48 px hero, `-1.28 px` at 32 px section); body stays at neutral or slightly-negative tracking.
|
|
||||||
2. **A custom monospaced face** (extracted as `Geist Mono`) for terminal mockups, code blocks, and small mono-caption labels — anything that wants to signal "technical." Weight 400 only at 12 – 13 px. Tracking neutral.
|
|
||||||
|
|
||||||
A condensed display sans (`Space Grotesk`) is loaded as a third face for occasional editorial moments but does not render as the primary face anywhere in the captured surfaces.
|
|
||||||
|
|
||||||
### Hierarchy
|
|
||||||
|
|
||||||
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| `{typography.display-xl}` | 48px | 600 | 48px | -2.4px | Hero headline ("Build and deploy on the AI Cloud."). |
|
|
||||||
| `{typography.display-lg}` | 32px | 600 | 40px | -1.28px | Section headlines ("Your frontend, delivered.", "A compute model for all workloads."). |
|
|
||||||
| `{typography.display-md}` | 24px | 600 | 32px | -0.96px | Card-cluster headlines, pricing-tier names. |
|
|
||||||
| `{typography.display-sm}` | 20px | 600 | 28px | -0.6px | Inline display micro-headings. |
|
|
||||||
| `{typography.body-lg}` | 18px | 400 | 28px | 0 | Lead paragraphs under section headlines. |
|
|
||||||
| `{typography.body-md}` | 16px | 400 | 24px | 0 | Default body paragraph. |
|
|
||||||
| `{typography.body-md-strong}` | 16px | 500 | 24px | 0 | Bolded inline body. |
|
|
||||||
| `{typography.body-sm}` | 14px | 400 | 20px | -0.28px | Secondary body, nav-link text, button-md labels. |
|
|
||||||
| `{typography.body-sm-strong}` | 14px | 500 | 20px | -0.28px | Nav CTA labels, table-row emphasis. |
|
|
||||||
| `{typography.caption}` | 12px | 400 | 16px | 0 | Footer secondary lines, badge labels. |
|
|
||||||
| `{typography.caption-mono}` | 12px | 400 | 16px | 0 | Section eyebrows and label captions that want a technical voice. |
|
|
||||||
| `{typography.code}` | 13px | 400 | 20px | 0 | Inline code, terminal mockups, command snippets. |
|
|
||||||
| `{typography.button-md}` | 14px | 500 | 20px | 0 | Small / nav-scale button labels. |
|
|
||||||
| `{typography.button-lg}` | 16px | 500 | 24px | 0 | Marketing-scale pill button labels. |
|
|
||||||
|
|
||||||
### Principles
|
|
||||||
- **Negative tracking is part of the voice.** Display sizes use aggressive `-2.4` to `-0.6` px tracking. Reverting to default tracking breaks the brand.
|
|
||||||
- **Sentence-case headlines, period-terminated.** Headlines like "Build and deploy on the AI Cloud." end with a deliberate period — that punctuation is part of the brand's voice.
|
|
||||||
- **Mono for the technical layer only.** Section eyebrows, code blocks, terminal mockups. Body paragraphs never set in mono.
|
|
||||||
- **Weight 600 is the display ceiling.** The geometric sans never appears at 700 / 800. The brand reads as a calmer system because of this.
|
|
||||||
|
|
||||||
### Note on Font Substitutes
|
|
||||||
The two primary faces are proprietary (custom-cut for the brand). Open-source substitutes:
|
|
||||||
- **Geometric sans** — *Inter* (400 / 500 / 600) is the closest stylistic match; `font-feature-settings: "ss01", "ss02"` enables the geometric alternates. *Satoshi* is a passable second choice.
|
|
||||||
- **Monospace** — *JetBrains Mono* (400) at 12 – 13 px matches the technical voice. *IBM Plex Mono* is the second-best option.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
### Spacing System
|
|
||||||
- **Base unit**: 4 px. The brand's `--geist-space` token is exactly 4 px and every captured value is a multiple of 4.
|
|
||||||
- **Tokens**: `{spacing.xxs}` 4 px · `{spacing.xs}` 8 px · `{spacing.sm}` 12 px · `{spacing.md}` 16 px · `{spacing.lg}` 24 px · `{spacing.xl}` 32 px · `{spacing.2xl}` 40 px · `{spacing.3xl}` 48 px · `{spacing.4xl}` 64 px · `{spacing.5xl}` 96 px · `{spacing.6xl}` 128 px · `{spacing.section}` 192 px.
|
|
||||||
- **Section padding**: marketing bands use `{spacing.4xl}` to `{spacing.5xl}` top/bottom. Hero bands stretch to `{spacing.section}` to give the mesh gradient room to breathe.
|
|
||||||
- **Card interior padding**: marketing cards sit at `{spacing.lg}` to `{spacing.xl}`; template-grid cards stay tighter at `{spacing.md}` because they sit in a denser grid.
|
|
||||||
- **Inline gap**: button rows, nav rows, and chip rows use `{spacing.sm}` to `{spacing.md}` between siblings. The brand's `--geist-gap` is exactly 24 px.
|
|
||||||
|
|
||||||
### Grid & Container
|
|
||||||
- **Max width**: ~1400 px (`--ds-page-width`); the legacy `--geist-page-width` is 1200 px and still appears on some marketing surfaces. Content centres with horizontal gutters of `{spacing.lg}` 24 px on desktop, `{spacing.md}` 16 px on mobile.
|
|
||||||
- **Column patterns**:
|
|
||||||
- Three-feature row: 3-up at desktop, 1-up at mobile (rows like "Web Apps / Composable Commerce / Multi-tenant Platforms").
|
|
||||||
- Tab pill row: 5-up centred row of `tab-ghost` pills.
|
|
||||||
- Template-grid cluster: 5-up at desktop, scaling to 1-up at mobile.
|
|
||||||
- Pricing tier grid: 3-up at desktop with the middle tier polarity-flipped.
|
|
||||||
- Logo strip: ~5 logos wide, single row.
|
|
||||||
|
|
||||||
### Whitespace Philosophy
|
|
||||||
The mesh gradient does most of the heavy decorative lifting; whitespace separates the bands. Section spacing is generous — `{spacing.4xl}` to `{spacing.5xl}` between bands lets the gradient breathe. Inside a card, the headline/paragraph stack is tight (`{spacing.xs}` 8 px gap), then a wider gap before the CTA cluster. The page reads as engineered — large gaps + tight interior, never the other way around.
|
|
||||||
|
|
||||||
### Responsive Strategy
|
|
||||||
|
|
||||||
#### Breakpoints
|
|
||||||
|
|
||||||
| Name | Width | Key Changes |
|
|
||||||
|---|---|---|
|
|
||||||
| Mobile | < 600px | Hero stacks; nav collapses to hamburger; 3-up feature grids drop to 1-up; tab pill row enables horizontal scroll. |
|
|
||||||
| Tablet | 600–959px | 3-up grids drop to 2-up; nav still horizontal. |
|
|
||||||
| Desktop | 960–1199px | Full 3-up grids; pricing 3-up. |
|
|
||||||
| Wide | 1200–1399px | Container caps at 1400 px content width. |
|
|
||||||
| Ultra-wide | ≥ 1400px | Content stays centred at 1400 px; bands stretch edge-to-edge in colour but content holds the max-width. |
|
|
||||||
|
|
||||||
#### Touch Targets
|
|
||||||
The `button-primary` pill renders at ~32 px tall in nav and ~48 px tall in marketing contexts. Marketing CTAs comfortably meet WCAG AAA at all breakpoints; nav buttons inflate touch area through `{spacing.xs}` padding on mobile to meet the 44 × 44 px floor.
|
|
||||||
|
|
||||||
#### Collapsing Strategy
|
|
||||||
- **Nav**: full link row + Ask AI / Log In / Sign Up pills at desktop. Collapses to logo + hamburger at mobile with the menu opening as a full-overlay.
|
|
||||||
- **Hero**: mesh gradient stays centred; headline + body stack vertically at all breakpoints (the brand doesn't use a split-hero pattern).
|
|
||||||
- **Three-feature row**: 3-up → 2-up → 1-up at the breakpoints above; cards keep their `{rounded.md}` 8 px shape across all viewports.
|
|
||||||
- **Pricing card grid**: 3-up at desktop, vertical stack at mobile with `pricing-card-featured` always sitting in the middle.
|
|
||||||
- **Template grid**: 5-up → 3-up → 2-up → 1-up. Each `template-card` keeps its 16:9 aspect on the image.
|
|
||||||
|
|
||||||
#### Image Behavior
|
|
||||||
- **Mesh gradient**: rendered as inline SVG or canvas-painted gradient; scales fluidly with the hero container; never crops, never tiles.
|
|
||||||
- **Customer logos**: rendered as monochrome SVGs in the logo strip; consistent 24 px height.
|
|
||||||
- **Code editor mockup**: dark `{colors.primary}` rectangle with mono text rendered inside; treated as an image at the layout level.
|
|
||||||
- **Template thumbnails**: 16:9 landscape inside `{rounded.md}` card chrome; lazy-loaded; consistent grayscale palette in the placeholder state.
|
|
||||||
|
|
||||||
## Elevation & Depth
|
|
||||||
|
|
||||||
| Level | Treatment | Use |
|
|
||||||
|---|---|---|
|
|
||||||
| Level 0 — Flat | No shadow, no border. | Full-bleed hero bands and the polarity-flipped dark sections. |
|
|
||||||
| Level 1 — Inset Hairline | `0 0 0 1px #00000014` inset 1 px border. | Default card chrome — the brand's universal "you can see this card" cue. |
|
|
||||||
| Level 2 — Subtle Drop | `0px 1px 1px #00000005, 0px 2px 2px #0000000a` plus inset hairline. | Slightly elevated cards (template-grid, marketing-card). |
|
|
||||||
| Level 3 — Soft Stack | `0px 2px 2px #0000000a, 0px 8px 8px -8px #0000000a` plus inset hairline. | The "medium" elevation — feature-grid cards. |
|
|
||||||
| Level 4 — Float Stack | `0px 2px 2px #0000000a, 0px 8px 16px -4px #0000000a` plus inset hairline. | "Large" elevation — pricing cards, callout panels. |
|
|
||||||
| Level 5 — Modal | `0px 1px 1px #00000005, 0px 8px 16px -4px #0000000a, 0px 24px 32px -8px #0000000f` plus inset hairline. | Modal / dialog surfaces and dropdown menus. |
|
|
||||||
|
|
||||||
The brand uses STACKED shadows — multiple small offsets layered to fake natural light — never a single 8-px-blur generic drop. Inset hairline rings are always added so the card edge stays crisp.
|
|
||||||
|
|
||||||
### Decorative Depth
|
|
||||||
- **Mesh gradient as atmospheric depth**: the hero's multi-stop gradient is the brand's only "atmospheric" effect — applied as a flat 2-D backdrop rather than a 3-D illustration.
|
|
||||||
- **Polarity-flipped dark band as section-depth**: switching the surface from `{colors.canvas-soft}` to `{colors.primary}` (the deep ink) is the brand's chief depth cue between bands.
|
|
||||||
- **Inset-shadow + drop-shadow combo**: the cards' combination of an inset 1 px ring and a multi-stop drop produces a "card sits on the page" effect without ever feeling material-heavy.
|
|
||||||
|
|
||||||
## Shapes
|
|
||||||
|
|
||||||
### Border Radius Scale
|
|
||||||
|
|
||||||
| Token | Value | Use |
|
|
||||||
|---|---|---|
|
|
||||||
| `{rounded.none}` | 0px | Full-bleed hero / footer bands. |
|
|
||||||
| `{rounded.xs}` | 4px | Tightest inline pill — the `nav-cta-signup` 6-px-radius button (mapped to `xs/sm`). |
|
|
||||||
| `{rounded.sm}` | 6px | The brand's `--geist-radius` token — base UI radius for in-app buttons, form inputs, dropdown menus. |
|
|
||||||
| `{rounded.md}` | 8px | The brand's `--geist-marketing-radius` token — feature cards, template cards. |
|
|
||||||
| `{rounded.lg}` | 12px | Slightly larger card chrome (pricing-card variants). |
|
|
||||||
| `{rounded.xl}` | 16px | Largest card chrome — when a card hosts a hero image cap. |
|
|
||||||
| `{rounded.pill-sm}` | 64px | Tab-ghost pills inside the "AI Apps / Web Apps / Ecommerce / Marketing / Platforms" row. |
|
|
||||||
| `{rounded.pill}` | 100px | The marketing CTA pill — `button-primary`, `button-secondary`, "Start Deploying" pill. |
|
|
||||||
| `{rounded.full}` | 9999px | Icon-button circular containers, nav-link ghost pills. |
|
|
||||||
|
|
||||||
### Photography Geometry
|
|
||||||
- **Mesh gradient**: full-bleed 2-D atmospheric backdrop, never cropped to a frame; treated as the page's wallpaper.
|
|
||||||
- **Customer logos**: monochrome SVG, consistent 24 px height in a flex row.
|
|
||||||
- **Code editor mockup**: 16:10 dark rectangle, `{rounded.md}` corners.
|
|
||||||
- **Template thumbnails**: 16:9 landscape inside `{rounded.md}` chrome.
|
|
||||||
- **Showcase imagery**: 2:1 or 16:9 inside `{rounded.lg}` to `{rounded.xl}` chrome with a stacked shadow.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
### Buttons
|
|
||||||
|
|
||||||
**`button-primary`** — the canonical 100-px-radius black pill, marketing scale.
|
|
||||||
- Background `{colors.primary}`, text `{colors.on-primary}`, label set in `{typography.button-lg}`, padding `0px {spacing.sm}` 12 px, shape `{rounded.pill}` 100 px. Renders ~48 px tall when paired with the marketing flex layout.
|
|
||||||
|
|
||||||
**`button-secondary`** — the white pill paired with the black primary inside marketing bands.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, same typography + padding as `button-primary`, shape `{rounded.pill}`.
|
|
||||||
|
|
||||||
**`button-primary-sm`** — the smaller-scale primary pill used inside nav and pricing-card CTAs.
|
|
||||||
- Background `{colors.primary}`, text `{colors.on-primary}`, label set in `{typography.button-md}` (14 px / 500), shape `{rounded.pill}`.
|
|
||||||
|
|
||||||
**`button-secondary-sm`** — the smaller-scale white pill paired with `button-primary-sm`.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, same typography + shape as `button-primary-sm`.
|
|
||||||
|
|
||||||
**`tab-ghost`** — the centred-row tab pill ("AI Apps / Web Apps / Ecommerce / Marketing / Platforms").
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, label set in `{typography.body-sm}`, padding `0px {spacing.md}`, shape `{rounded.pill-sm}` 64 px.
|
|
||||||
|
|
||||||
**`icon-button-circular`** — the circular icon container (often a "?" or arrow inside).
|
|
||||||
- Background `{colors.canvas}`, dark icon, 1 px solid hairline border, shape `{rounded.full}`.
|
|
||||||
|
|
||||||
**Nav CTAs:**
|
|
||||||
|
|
||||||
**`nav-cta-signup`** — the small black "Sign Up" button in the nav row.
|
|
||||||
- Background `{colors.primary}`, text `{colors.on-primary}`, label `{typography.body-sm-strong}`, padding `0px {spacing.xs}`, height 28 px, shape `{rounded.sm}` 6 px (the brand's `--geist-radius`).
|
|
||||||
|
|
||||||
**`nav-cta-login`** — the white "Log In" button in the nav.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, same typography / height / shape as `nav-cta-signup`.
|
|
||||||
|
|
||||||
**`nav-cta-ask-ai`** — the small "Ask AI" button with a faint border.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, 1 px solid `{colors.hairline}` border (extracted as `0px solid rgb(235, 235, 235)`), same typography / height / shape.
|
|
||||||
|
|
||||||
### Cards & Containers
|
|
||||||
|
|
||||||
**`card-marketing`** — the canonical marketing feature card (3-up section cards).
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.lg}` 24 px, shape `{rounded.md}` 8 px (the `--geist-marketing-radius`). Carries Level 3 soft-stack shadow.
|
|
||||||
|
|
||||||
**`card-marketing-large`** — the larger marketing card used for "compute model" / "AI Gateway" callouts.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.xl}`, shape `{rounded.lg}` 12 px. Carries Level 4 float-stack shadow.
|
|
||||||
|
|
||||||
**`card-soft`** — the soft-tinted card used inside cluster groups (lighter than canvas-soft).
|
|
||||||
- Background `{colors.canvas-soft}`, text `{colors.ink}`, padding `{spacing.lg}`, shape `{rounded.md}`.
|
|
||||||
|
|
||||||
**`template-card`** — the deploy-template card in the "Deploy your first app" grid.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.md}` 16 px, shape `{rounded.md}` 8 px. Hosts a 16:9 thumbnail at the top.
|
|
||||||
|
|
||||||
**`code-editor-mockup`** — the dark code-preview surface inside marketing bands.
|
|
||||||
- Background `{colors.primary}`, text `{colors.on-primary}`, body in `{typography.code}` (13 px / Geist Mono), padding `{spacing.lg}` 24 px, shape `{rounded.md}` 8 px.
|
|
||||||
|
|
||||||
**`pricing-card`** — the default pricing-tier card.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.xl}` 32 px, shape `{rounded.lg}` 12 px. Inside: tier name in `{typography.display-md}`, price in `{typography.display-xl}`, feature list in `{typography.body-md}` rows, CTA at the bottom.
|
|
||||||
|
|
||||||
**`pricing-card-featured`** — the polarity-flipped "Pro" tier card.
|
|
||||||
- Background `{colors.primary}`, text `{colors.on-primary}`, same shape + padding as `pricing-card`. CTA inverts to `button-secondary-sm` (white pill on black card).
|
|
||||||
|
|
||||||
### Inputs & Forms
|
|
||||||
|
|
||||||
**`form-input`** — the canonical text input.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, 1 px solid `{colors.hairline}` border, body in `{typography.body-sm}` (14 px), padding `0px {spacing.sm}`, height 40 px (the brand's `--geist-form-height`), shape `{rounded.sm}` 6 px.
|
|
||||||
|
|
||||||
**`form-input-sm`** — small-height variant (32 px tall) for tight forms.
|
|
||||||
- Same as `form-input` but height 32 px (the `--geist-form-small-height`).
|
|
||||||
|
|
||||||
**`form-input-lg`** — large-height variant (48 px tall) for hero CTAs.
|
|
||||||
- Same as `form-input` but height 48 px (the `--geist-form-large-height`); body in `{typography.body-md}` 16 px.
|
|
||||||
|
|
||||||
### Navigation
|
|
||||||
|
|
||||||
**`nav-bar`** — the sticky top nav.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, height 64 px (the brand's `--header-height`), padding `{spacing.sm} {spacing.lg}`. Layout: logo left, link row centre, "Ask AI / Log In / Sign Up" cluster right.
|
|
||||||
|
|
||||||
**`nav-link`** — the centred link row inside `nav-bar`.
|
|
||||||
- Text `{colors.body}`, set in `{typography.body-sm}`, padding `{spacing.xs} {spacing.sm}`, shape `{rounded.full}` (ghost pill — visible only on hover or active, but the radius is documented).
|
|
||||||
|
|
||||||
**`footer`** — the bottom 4-column nav.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.body}`, padding `{spacing.4xl} {spacing.lg}`. Eyebrow column labels in `{typography.caption-mono}` (uppercase mono effect); link rows in `{typography.body-sm}`.
|
|
||||||
|
|
||||||
### Signature Components
|
|
||||||
|
|
||||||
**`hero-band`** — the white hero with the mesh gradient backdrop.
|
|
||||||
- Background `{colors.canvas}` (or `{colors.canvas-soft}` on some surfaces), text `{colors.ink}`, padding `{spacing.4xl} {spacing.lg}`. Inside: a small mono badge above the headline, the headline in `{typography.display-xl}` (sentence-case, period-terminated), a body lead in `{typography.body-lg}`, then a CTA row with `button-primary` + `button-secondary`. The mesh gradient sits behind, scaled to occupy roughly the top half of the band.
|
|
||||||
|
|
||||||
**`feature-mesh-band`** — the secondary section that hosts a mesh-gradient atmospheric backdrop with feature copy on top.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.5xl} {spacing.lg}`. Section headline in `{typography.display-lg}`; supporting body in `{typography.body-md}`.
|
|
||||||
|
|
||||||
**`showcase-band-light`** — a soft-canvas section ("Deploy your first app in seconds").
|
|
||||||
- Background `{colors.canvas-soft}`, text `{colors.ink}`, padding `{spacing.5xl} {spacing.lg}`.
|
|
||||||
|
|
||||||
**`showcase-band-dark`** — the polarity-flipped dark band ("A compute model for all workloads").
|
|
||||||
- Background `{colors.primary}`, text `{colors.on-primary}`, padding `{spacing.5xl} {spacing.lg}`. Section headline in `{typography.display-lg}` (white on black). Often contains a `code-editor-mockup` flush with the band.
|
|
||||||
|
|
||||||
**`logo-strip`** — the customer-logo wrapping row near the top of the page.
|
|
||||||
- Background `{colors.canvas}`, text `{colors.body}`, padding `{spacing.lg} {spacing.xl}`. Logos rendered as monochrome SVGs at consistent height.
|
|
||||||
|
|
||||||
**`badge-secondary`** — the small inline metadata pill ("New", "Beta", "Live").
|
|
||||||
- Background `{colors.canvas-soft}`, text `{colors.body}`, body in `{typography.caption}`, padding `0px {spacing.xs}`, shape `{rounded.full}`.
|
|
||||||
|
|
||||||
**`banner-marketing`** — the "Introducing X" announcement pill at the top of pages.
|
|
||||||
- Background `{colors.canvas-soft}`, text `{colors.body}`, body in `{typography.body-sm}`, padding `{spacing.xs} {spacing.sm}`, shape `{rounded.full}`.
|
|
||||||
|
|
||||||
**`link-inline`** — body-copy inline links.
|
|
||||||
- Text `{colors.link}` (`#0070f3`), body in `{typography.body-md}`, underlined.
|
|
||||||
|
|
||||||
### Examples (illustrative)
|
|
||||||
|
|
||||||
> Auto-derived kit-mirror demonstration surfaces (`scripts/derive-examples-block.mjs`). Each `ex-*` entry references brand-native primitives so downstream consumers (`/preview-design`, `/generate-kit`) re-skin the same 10 surfaces consistently. `TO_FILL` markers indicate missing primitives — resolve in the LLM judgment pass.
|
|
||||||
|
|
||||||
**`ex-pricing-tier`** — Default Pricing tier card. Re-uses feature-card chrome with brand canvas-soft surface.
|
|
||||||
- Properties: `backgroundColor`, `textColor`, `borderColor`, `rounded`, `padding`
|
|
||||||
|
|
||||||
**`ex-pricing-tier-featured`** — Featured/highlighted tier — polarity-flipped surface (dark fill + light text in light mode, light fill + dark text in dark mode).
|
|
||||||
- Properties: `backgroundColor`, `textColor`, `rounded`, `padding`
|
|
||||||
|
|
||||||
**`ex-product-selector`** — What's Included summary card — re-purposed for SaaS / B2B verticals (NOT a literal product gallery).
|
|
||||||
- Properties: `backgroundColor`, `rounded`, `padding`
|
|
||||||
|
|
||||||
**`ex-cart-drawer`** — Subscription summary — re-purposed for SaaS / B2B (line items per add-on, not literal cart).
|
|
||||||
- Properties: `backgroundColor`, `rounded`, `padding`, `item-divider`
|
|
||||||
|
|
||||||
**`ex-app-shell-row`** — Sidebar nav row inside the App Shell example. Active state uses brand primary as the indicator.
|
|
||||||
- Properties: `backgroundColor`, `activeIndicator`, `rounded`, `padding`
|
|
||||||
|
|
||||||
**`ex-data-table-cell`** — Default data-table th + td chrome. Header uses mono-caps eyebrow typography; body uses body-sm.
|
|
||||||
- Properties: `headerBackground`, `headerTypography`, `bodyTypography`, `cellPadding`, `rowBorder`
|
|
||||||
|
|
||||||
**`ex-auth-form-card`** — Sign-in / sign-up card. Re-uses feature-card chrome with text-input primitives inside.
|
|
||||||
- Properties: `backgroundColor`, `rounded`, `padding`
|
|
||||||
|
|
||||||
**`ex-modal-card`** — Modal dialog surface — same chrome as feature-card with elevated shadow.
|
|
||||||
- Properties: `backgroundColor`, `rounded`, `padding`
|
|
||||||
|
|
||||||
**`ex-empty-state-card`** — Empty-state illustration frame.
|
|
||||||
- Properties: `backgroundColor`, `rounded`, `padding`, `captionTypography`
|
|
||||||
|
|
||||||
**`ex-toast`** — Toast notification surface — feature-card shape + medium shadow.
|
|
||||||
- Properties: `backgroundColor`, `rounded`, `padding`, `typography`
|
|
||||||
|
|
||||||
|
|
||||||
## Do's and Don'ts
|
|
||||||
|
|
||||||
### Do
|
|
||||||
- Reserve `{colors.primary}` (`#171717`) for primary CTAs across the page. Black ink IS the conversion target.
|
|
||||||
- Use `{rounded.pill}` 100 px for every marketing-scale CTA and `{rounded.sm}` 6 px for nav-scale buttons. The two pill scales coexist deliberately.
|
|
||||||
- Set every headline in `{typography.display-*}` weight 600, sentence-case, often period-terminated. Aggressive negative tracking is part of the voice.
|
|
||||||
- Use the brand mesh gradient as atmospheric decoration at hero scale only — never miniaturise it to an icon, never reduce to a single colour.
|
|
||||||
- Layer stacked shadows (multiple small offsets with inset hairline) rather than single heavy drops. The brand's elevation is calmer than Material.
|
|
||||||
- Cycle page surfaces in `{colors.canvas-soft}` → `{colors.canvas}` → `{colors.primary}` polarity-flipped bands; the dark band IS the depth cue.
|
|
||||||
- Set every code block and technical eyebrow in `{typography.code}` / `{typography.caption-mono}`. Mono is the voice of the platform.
|
|
||||||
|
|
||||||
### Don't
|
|
||||||
- Don't introduce a sixth accent colour. The brand operates with ink + gray + the four-pair gradient palette; new accents flatten the voice.
|
|
||||||
- Don't render headlines in all-caps. Sentence-case + negative tracking is non-negotiable.
|
|
||||||
- Don't drop a single heavy drop-shadow on cards. The brand's elevation is built from stacked small offsets + inset hairline rings.
|
|
||||||
- Don't render the brand gradient at icon scale or in a single-colour reduced form. The gradient lives at hero scale only.
|
|
||||||
- Don't promote the geometric sans to weight 700. The brand's display ceiling is 600.
|
|
||||||
- Don't pair the marketing 100-px pill CTA shape with the 6-px nav radius on the same screen — pick a scale and stay there.
|
|
||||||
- Don't set body paragraphs in the mono face. The mono is for code + technical labels only.
|
|
||||||
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 |
9
package-lock.json
generated
9
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@axhub/annotation": "^1.0.10",
|
"@axhub/annotation": "^1.0.10",
|
||||||
|
"@axhub/commentary-react": "^1.0.0",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
@@ -535,6 +536,14 @@
|
|||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@axhub/commentary-react": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@axhub/commentary-react/-/commentary-react-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-2wyrXZxowCr48fo+lLRImd2Vzzvt+IdAT+xXRwq8BIoat0HQXi6vl+MTXvKWQjMS54UVfrGkLdFgAQeEMB7YXQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@axhub/annotation": "^1.0.10",
|
"@axhub/annotation": "^1.0.10",
|
||||||
|
"@axhub/commentary-react": "^1.0.0",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/engineering/to-prd/SKILL.md",
|
"skillPath": "skills/engineering/to-prd/SKILL.md",
|
||||||
"computedHash": "5a733e19d825f22e83de52e68b8840c9fd2fa7501282277a122c1aa28a922059"
|
"computedHash": "5a733e19d825f22e83de52e68b8840c9fd2fa7501282277a122c1aa28a922059"
|
||||||
|
},
|
||||||
|
"user-story-mapping": {
|
||||||
|
"source": "deanpeters/product-manager-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/user-story-mapping/SKILL.md",
|
||||||
|
"computedHash": "e4ffdd47016435d03ba1bc3bcfae5e13ac3bd7a7e81569a026c11066f5c6c174"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const MORE_ACTION_ICONS: Record<string, LucideIcon> = {
|
|||||||
manage: Settings2,
|
manage: Settings2,
|
||||||
preview: Eye,
|
preview: Eye,
|
||||||
download: Download,
|
download: Download,
|
||||||
|
failDetail: FileText,
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderMenuItemLabel(item: OperationActionItem) {
|
function renderMenuItemLabel(item: OperationActionItem) {
|
||||||
@@ -95,7 +96,7 @@ function ViewIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MoreIcon() {
|
function MoreIcon() {
|
||||||
return <MoreHorizontal className="vm-op-action__icon" aria-hidden />;
|
return <MoreHorizontal className="vm-op-more-btn__icon" aria-hidden strokeWidth={1.75} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PrimaryActionButton = React.forwardRef<
|
const PrimaryActionButton = React.forwardRef<
|
||||||
@@ -121,18 +122,22 @@ const PrimaryActionButton = React.forwardRef<
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
|
const isMore = icon === 'more';
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="button"
|
type="button"
|
||||||
className={['vm-op-action', className].filter(Boolean).join(' ')}
|
className={[
|
||||||
|
isMore ? 'vm-op-more-btn' : 'vm-op-action',
|
||||||
|
className,
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-haspopup={ariaHasPopup ? 'menu' : undefined}
|
aria-haspopup={ariaHasPopup ? 'menu' : undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{icon === 'view' ? <ViewIcon /> : <MoreIcon />}
|
{isMore ? <MoreIcon /> : <ViewIcon />}
|
||||||
<span className="vm-op-action__label">{label}</span>
|
{isMore ? null : <span className="vm-op-action__label">{label}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -236,9 +241,9 @@ export function OperationActions({
|
|||||||
<PrimaryActionButton
|
<PrimaryActionButton
|
||||||
icon="more"
|
icon="more"
|
||||||
label={moreLabel}
|
label={moreLabel}
|
||||||
ariaLabel={moreLabel + '操作'}
|
ariaLabel="更多操作"
|
||||||
ariaHasPopup
|
ariaHasPopup
|
||||||
className={moreOpen ? 'vm-op-action--open' : undefined}
|
className={moreOpen ? 'vm-op-more-btn--open' : undefined}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -11,6 +11,41 @@ var H2_VERIFY_UNVERIFIED = 'unverified';
|
|||||||
var H2_RECORD_SOURCE_STATION = 'station';
|
var H2_RECORD_SOURCE_STATION = 'station';
|
||||||
var H2_RECORD_SOURCE_LINGNIU = 'lingniu';
|
var H2_RECORD_SOURCE_LINGNIU = 'lingniu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原型本地「系统车辆列表」车牌(不含尾缀 F)。命中 → 羚牛车辆,否则 → 非羚牛车辆。
|
||||||
|
* 正式环境应改为车辆管理接口;未接真实 API。
|
||||||
|
*/
|
||||||
|
var H2_FLEET_PLATE_KEYS = [
|
||||||
|
'浙A55666', '浙A77888', '浙A99001', '浙A12345', '浙A67890', '浙A88888', '浙A03561',
|
||||||
|
'浙B23456', '浙B99999', '浙B58888',
|
||||||
|
'沪A88888', '沪BDB9161', '沪ADB9161',
|
||||||
|
'苏E33333',
|
||||||
|
'浙A88H201', '浙F00688', '浙F07588', '浙F06618',
|
||||||
|
'粤AGR8556', '粤AGP5156',
|
||||||
|
/* 车辆氢费明细演示车牌 */
|
||||||
|
'浙AD12345', '浙AH55660', '浙BK33210', '粤BK33210', '沪AD12345', '苏EF99887', '京CN88771', '川AL55602'
|
||||||
|
];
|
||||||
|
|
||||||
|
var H2_FLEET_PLATE_SET = (function () {
|
||||||
|
var set = {};
|
||||||
|
H2_FLEET_PLATE_KEYS.forEach(function (p) {
|
||||||
|
set[String(p).toUpperCase()] = true;
|
||||||
|
});
|
||||||
|
return set;
|
||||||
|
})();
|
||||||
|
|
||||||
|
/** 车牌规范化键:去尾缀 F、大写、去空白 */
|
||||||
|
function h2BridgeNormalizePlateKey(plateNo) {
|
||||||
|
return String(plateNo || '').trim().toUpperCase().replace(/F$/u, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否羚牛车辆(系统车辆列表命中) */
|
||||||
|
function h2BridgeIsLingniuVehicle(plateNo) {
|
||||||
|
var key = h2BridgeNormalizePlateKey(plateNo);
|
||||||
|
if (!key) return false;
|
||||||
|
return Boolean(H2_FLEET_PLATE_SET[key]);
|
||||||
|
}
|
||||||
|
|
||||||
var H2_STATION_CODE_MAP = {
|
var H2_STATION_CODE_MAP = {
|
||||||
'中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001',
|
'中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001',
|
||||||
'杭州临平加氢站': 'HZ-H2-002',
|
'杭州临平加氢站': 'HZ-H2-002',
|
||||||
@@ -41,7 +76,9 @@ var H2_CANONICAL_LEDGER_SEED = [
|
|||||||
{ key: 'rf-7', id: 'rf-7', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costTotal: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer', mileageKm: 110340, creatorName: '李四', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-05-25 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-05-25 10:00:00' },
|
{ key: 'rf-7', id: 'rf-7', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costTotal: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer', mileageKm: 110340, creatorName: '李四', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-05-25 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-05-25 10:00:00' },
|
||||||
{ key: 'rf-8', id: 'rf-8', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costTotal: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal', mileageKm: 88420, creatorName: '王静', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-21 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-04-21 10:00:00' },
|
{ key: 'rf-8', id: 'rf-8', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costTotal: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal', mileageKm: 88420, creatorName: '王静', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-21 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-04-21 10:00:00' },
|
||||||
{ key: 'rf-9', id: 'rf-9', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costTotal: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self', mileageKm: 76500, creatorName: '张三', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-09 10:00:00', reconcileStatus: H2_RECONCILE_PENDING, reconcileDate: null },
|
{ key: 'rf-9', id: 'rf-9', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costTotal: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self', mileageKm: 76500, creatorName: '张三', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-09 10:00:00', reconcileStatus: H2_RECONCILE_PENDING, reconcileDate: null },
|
||||||
{ key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING }
|
{ key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING },
|
||||||
|
/* 非羚牛车辆演示:车牌不在系统车辆列表 → 列表核对/对账字段应显示为空 */
|
||||||
|
{ key: 'rf-ext-1', id: 'rf-ext-1', stationId: 'JX-H2-001', stationName: '中国石油中油高新能源牙谷加油加氢站', hydrogenTime: '2026-07-13 14:22:00', plateNo: '浙C77801F', customerName: '中国石油中油高新能源牙谷加油加氢站·站端上报', hydrogenKg: 8.5, costUnitPrice: 42.5, costTotal: 361.25, customerUnitPrice: 42.5, customerAmount: 361.25, settlementStatus: 'customer', mileageKm: 88000, creatorName: '站端账号', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING }
|
||||||
];
|
];
|
||||||
|
|
||||||
function h2BridgeFormatDateTime(value) {
|
function h2BridgeFormatDateTime(value) {
|
||||||
@@ -263,26 +300,28 @@ function h2BridgeResolveStationId(stationId, stationName) {
|
|||||||
return a || '';
|
return a || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手工台账种子:昨日已上传;今日默认未上传,便于演示拦截新增 */
|
/** 手工台账种子:前日已上传;昨日默认未上传,便于演示「前一日缺失则今日禁新增」 */
|
||||||
function h2BridgeBuildInitialManualLedgers() {
|
function h2BridgeBuildInitialManualLedgers() {
|
||||||
var yesterday = h2BridgeYesterdayDateKey();
|
var d = new Date();
|
||||||
|
d.setDate(d.getDate() - 2);
|
||||||
|
var dayBeforeYesterday = h2BridgeTodayDateKey(d);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'ml-seed-yesterday',
|
id: 'ml-seed-day-before-yesterday',
|
||||||
stationId: 'JX-H2-001',
|
stationId: 'JX-H2-001',
|
||||||
stationName: '中国石油中油高新能源牙谷加油加氢站',
|
stationName: '中国石油中油高新能源牙谷加油加氢站',
|
||||||
ledgerDate: yesterday,
|
ledgerDate: dayBeforeYesterday,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
id: 'ml-img-seed-1',
|
id: 'ml-img-seed-1',
|
||||||
name: '昨日手工台账.jpg',
|
name: '前日手工台账.jpg',
|
||||||
dataUrl: '',
|
dataUrl: '',
|
||||||
placeholder: true,
|
placeholder: true,
|
||||||
archived: true,
|
archived: true,
|
||||||
uploadedAt: yesterday + ' 18:30:00'
|
uploadedAt: dayBeforeYesterday + ' 18:30:00'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
updatedAt: yesterday + ' 18:30:00'
|
updatedAt: dayBeforeYesterday + ' 18:30:00'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -507,6 +546,9 @@ function h2BridgeMapToStationLedgerRow(row, index) {
|
|||||||
costTotal: costTotal,
|
costTotal: costTotal,
|
||||||
settlementStatus: row.settlementStatus,
|
settlementStatus: row.settlementStatus,
|
||||||
orderNo: h2BridgeBuildRefuelOrderNo(row, index),
|
orderNo: h2BridgeBuildRefuelOrderNo(row, index),
|
||||||
|
/* 对账单筛选依赖:须透出核对状态,否则站点侧会把全部行当成未核对 */
|
||||||
|
verifyStatus: row.verifyStatus === H2_VERIFY_VERIFIED ? H2_VERIFY_VERIFIED : H2_VERIFY_UNVERIFIED,
|
||||||
|
verifiedAt: row.verifiedAt || null,
|
||||||
reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING),
|
reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING),
|
||||||
statementRecordId: row.statementRecordId || null,
|
statementRecordId: row.statementRecordId || null,
|
||||||
reconcileDate: row.reconcileDate || null,
|
reconcileDate: row.reconcileDate || null,
|
||||||
@@ -732,6 +774,7 @@ function h2BridgeInit() {
|
|||||||
totalAmountMismatch: h2BridgeTotalAmountMismatch,
|
totalAmountMismatch: h2BridgeTotalAmountMismatch,
|
||||||
lookupStationUnitPrice: h2BridgeLookupStationUnitPrice,
|
lookupStationUnitPrice: h2BridgeLookupStationUnitPrice,
|
||||||
todayDateKey: h2BridgeTodayDateKey,
|
todayDateKey: h2BridgeTodayDateKey,
|
||||||
|
yesterdayDateKey: h2BridgeYesterdayDateKey,
|
||||||
getManualLedger: function (stationIdOrName, ledgerDate) {
|
getManualLedger: function (stationIdOrName, ledgerDate) {
|
||||||
return store.getManualLedger(stationIdOrName, ledgerDate);
|
return store.getManualLedger(stationIdOrName, ledgerDate);
|
||||||
},
|
},
|
||||||
@@ -744,9 +787,30 @@ function h2BridgeInit() {
|
|||||||
upsertManualLedger: function (input) {
|
upsertManualLedger: function (input) {
|
||||||
return store.upsertManualLedger(input);
|
return store.upsertManualLedger(input);
|
||||||
},
|
},
|
||||||
|
/** 前一日台账未上传(且昨日≥本站首笔加氢日)→ 今日禁新增 */
|
||||||
|
isManualLedgerGateBlocked: function (stationIdOrName) {
|
||||||
|
var station = String(stationIdOrName || '').trim();
|
||||||
|
if (!station) return false;
|
||||||
|
var todayKey = h2BridgeTodayDateKey();
|
||||||
|
var yesterdayKey = h2BridgeYesterdayDateKey();
|
||||||
|
var rows = store.getRows() || [];
|
||||||
|
var earliest = null;
|
||||||
|
rows.forEach(function (row) {
|
||||||
|
var sid = String(row.stationId || '').trim();
|
||||||
|
var sn = String(row.stationName || '').trim();
|
||||||
|
if (sid !== station && sn !== station) return;
|
||||||
|
var m = String(row.hydrogenTime || '').match(/^(\d{4}-\d{2}-\d{2})/);
|
||||||
|
if (!m) return;
|
||||||
|
if (!earliest || m[1] < earliest) earliest = m[1];
|
||||||
|
});
|
||||||
|
if (!earliest || yesterdayKey > todayKey || yesterdayKey < earliest) return false;
|
||||||
|
return !store.hasManualLedgerForDate(station, yesterdayKey);
|
||||||
|
},
|
||||||
upsertRow: function (row) { return store.upsertRow(row); },
|
upsertRow: function (row) { return store.upsertRow(row); },
|
||||||
removeRow: function (id) { return store.removeRow(id); },
|
removeRow: function (id) { return store.removeRow(id); },
|
||||||
subscribe: function (fn) { return store.subscribe(fn); }
|
subscribe: function (fn) { return store.subscribe(fn); },
|
||||||
|
isLingniuVehicle: h2BridgeIsLingniuVehicle,
|
||||||
|
normalizePlateKey: h2BridgeNormalizePlateKey
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return store;
|
return store;
|
||||||
@@ -776,5 +840,7 @@ export {
|
|||||||
h2BridgeFindDuplicateRow,
|
h2BridgeFindDuplicateRow,
|
||||||
h2BridgeTotalAmountMismatch,
|
h2BridgeTotalAmountMismatch,
|
||||||
h2BridgeLookupStationUnitPrice,
|
h2BridgeLookupStationUnitPrice,
|
||||||
h2BridgeInferRecordSource
|
h2BridgeInferRecordSource,
|
||||||
|
h2BridgeIsLingniuVehicle,
|
||||||
|
h2BridgeNormalizePlateKey
|
||||||
};
|
};
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ export interface ApprovalCardProps {
|
|||||||
|
|
||||||
function statusClassName(status: ApprovalCardItem['status']): string {
|
function statusClassName(status: ApprovalCardItem['status']): string {
|
||||||
if (status === 'approved') return 'ap-status ap-status--approved';
|
if (status === 'approved') return 'ap-status ap-status--approved';
|
||||||
if (status === 'rejected') return 'ap-status ap-status--rejected';
|
if (status === 'rejected' || status === 'terminated') return 'ap-status ap-status--rejected';
|
||||||
return 'ap-status ap-status--pending';
|
return 'ap-status ap-status--pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export interface ApprovalDetailPlaceholderProps {
|
|||||||
|
|
||||||
function statusBadgeClass(status: ApprovalCardItem['status']): string {
|
function statusBadgeClass(status: ApprovalCardItem['status']): string {
|
||||||
if (status === 'approved') return 'ap-detail__badge ap-detail__badge--approved';
|
if (status === 'approved') return 'ap-detail__badge ap-detail__badge--approved';
|
||||||
if (status === 'rejected') return 'ap-detail__badge ap-detail__badge--rejected';
|
if (status === 'rejected' || status === 'terminated') {
|
||||||
|
return 'ap-detail__badge ap-detail__badge--rejected';
|
||||||
|
}
|
||||||
return 'ap-detail__badge ap-detail__badge--pending';
|
return 'ap-detail__badge ap-detail__badge--pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ const STATUS_LABEL_MAP: Record<ApprovalStatus, string> = {
|
|||||||
pending: '审批中',
|
pending: '审批中',
|
||||||
processing: '审批中',
|
processing: '审批中',
|
||||||
approved: '已通过',
|
approved: '已通过',
|
||||||
rejected: '已驳回',
|
rejected: '被驳回',
|
||||||
|
terminated: '被终止',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getApprovalTypeLabel(type: ApprovalTypeKey): string {
|
export function getApprovalTypeLabel(type: ApprovalTypeKey): string {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected';
|
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected' | 'terminated';
|
||||||
|
|
||||||
export type ApprovalTabKey = 'todo' | 'done' | 'initiated' | 'cc';
|
export type ApprovalTabKey = 'todo' | 'done' | 'initiated' | 'cc';
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -84,6 +84,12 @@
|
|||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ln-primary, #32a06e) 35%, transparent);
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ln-primary, #32a06e) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vm-op-more-btn--open,
|
||||||
|
.vm-op-more-btn[aria-expanded="true"] {
|
||||||
|
color: var(--ln-ink, #0f172a);
|
||||||
|
background: var(--ln-canvas-soft, #f4f4f5);
|
||||||
|
}
|
||||||
|
|
||||||
.vm-op-more-btn__icon {
|
.vm-op-more-btn__icon {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|||||||
@@ -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 承载**保单台账**与**比价采购**两条独立业务线:运维部、采购部可一车一档维护交强 / 商业等五类险种并办理停保复驶退保,车辆管理与交车据此校验核心险种有效性;采购部可在比价单中完成多方报价、付费预警与采购审批跟踪,审批通过后须人工补录正式保单——台账与比价互不同步,审批不在本页办理。
|
||||||
@@ -43,7 +43,10 @@ const moment = dayjs;
|
|||||||
const ANCHOR_TODAY = '2026-06-01';
|
const ANCHOR_TODAY = '2026-06-01';
|
||||||
const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1';
|
const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1';
|
||||||
const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1';
|
const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1';
|
||||||
const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v2';
|
const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v3';
|
||||||
|
const POLICY_RECOGN_FAIL_REASON_BLUR = 'OCR 识别失败,请检查文件清晰度或重新上传';
|
||||||
|
const POLICY_RECOGN_FAIL_REASON_FORMAT = '文件格式或版式无法解析,请上传清晰 PDF / 图片保单';
|
||||||
|
const POLICY_RECOGN_FAIL_REASON_CORRUPT = '文件损坏或内容为空,请重新导出后上传';
|
||||||
const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1';
|
const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1';
|
||||||
const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate';
|
const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate';
|
||||||
const NO_PLATE_LABEL = '暂无车牌';
|
const NO_PLATE_LABEL = '暂无车牌';
|
||||||
@@ -1084,17 +1087,35 @@ const buildMockOcrResults = (files, mode, insuranceTypeLabel, allInsurance) => (
|
|||||||
mode
|
mode
|
||||||
);
|
);
|
||||||
if (files.length >= 2 && idx === files.length - 1) {
|
if (files.length >= 2 && idx === files.length - 1) {
|
||||||
return {
|
return markRecognResultFailed(result, POLICY_RECOGN_FAIL_REASON_BLUR);
|
||||||
...result,
|
|
||||||
recognSuccess: false,
|
|
||||||
matched: false,
|
|
||||||
matchTip: 'OCR 识别失败,请检查文件清晰度或重新上传',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** 识别失败原因:优先 failReason,回退 matchTip */
|
||||||
|
const getPolicyRecognFailReason = (result) => (
|
||||||
|
String(result?.failReason || result?.matchTip || '').trim() || '识别失败,原因未知'
|
||||||
|
);
|
||||||
|
|
||||||
|
const getPolicyRecognFailResults = (results) => (
|
||||||
|
(results || [])
|
||||||
|
.filter((r) => r.recognSuccess === false)
|
||||||
|
.map((r) => ({
|
||||||
|
id: r.id || r.fileUid || r.fileName,
|
||||||
|
fileName: r.fileName || '未命名文件',
|
||||||
|
failReason: getPolicyRecognFailReason(r),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const markRecognResultFailed = (result, reason) => ({
|
||||||
|
...result,
|
||||||
|
recognSuccess: false,
|
||||||
|
matched: false,
|
||||||
|
matchTip: reason,
|
||||||
|
failReason: reason,
|
||||||
|
});
|
||||||
|
|
||||||
const recognResultToPolicyDetail = (result, taskMode = 'policy') => {
|
const recognResultToPolicyDetail = (result, taskMode = 'policy') => {
|
||||||
const mode = resolvePolicyRecognEffectiveMode(taskMode, result);
|
const mode = resolvePolicyRecognEffectiveMode(taskMode, result);
|
||||||
const pd = result.policyDetail || {};
|
const pd = result.policyDetail || {};
|
||||||
@@ -2561,6 +2582,13 @@ const createMockPolicyRecognTasks = () => {
|
|||||||
];
|
];
|
||||||
const resultsCompleted1 = buildMockOcrResults(filesCompleted1, 'policy', '交强险', insMap);
|
const resultsCompleted1 = buildMockOcrResults(filesCompleted1, 'policy', '交强险', insMap);
|
||||||
if (resultsCompleted1[0]) resultsCompleted1[0].confirmed = true;
|
if (resultsCompleted1[0]) resultsCompleted1[0].confirmed = true;
|
||||||
|
// 末条失败:清晰度问题(buildMockOcrResults 已标记);再给另一张不同失败原因
|
||||||
|
if (resultsCompleted1[1]) {
|
||||||
|
resultsCompleted1[1] = markRecognResultFailed(
|
||||||
|
resultsCompleted1[1],
|
||||||
|
POLICY_RECOGN_FAIL_REASON_FORMAT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const filesCompleted2 = [
|
const filesCompleted2 = [
|
||||||
{ uid: 'demo-c4', name: '粤B88888_复驶批单.pdf', status: 'done' },
|
{ uid: 'demo-c4', name: '粤B88888_复驶批单.pdf', status: 'done' },
|
||||||
@@ -2572,6 +2600,14 @@ const createMockPolicyRecognTasks = () => {
|
|||||||
resultsCompleted2.forEach((r) => {
|
resultsCompleted2.forEach((r) => {
|
||||||
if (r.recognSuccess !== false) r.confirmed = true;
|
if (r.recognSuccess !== false) r.confirmed = true;
|
||||||
});
|
});
|
||||||
|
// 末条已是清晰度失败;确保 failReason 字段齐全
|
||||||
|
const lastResumeFail = resultsCompleted2[resultsCompleted2.length - 1];
|
||||||
|
if (lastResumeFail && lastResumeFail.recognSuccess === false) {
|
||||||
|
resultsCompleted2[resultsCompleted2.length - 1] = markRecognResultFailed(
|
||||||
|
lastResumeFail,
|
||||||
|
POLICY_RECOGN_FAIL_REASON_BLUR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const filesSuspend = [
|
const filesSuspend = [
|
||||||
{ uid: 'demo-s1', name: '沪A06192F_商业险_停保批单.pdf', status: 'done' },
|
{ uid: 'demo-s1', name: '沪A06192F_商业险_停保批单.pdf', status: 'done' },
|
||||||
@@ -2579,6 +2615,18 @@ const createMockPolicyRecognTasks = () => {
|
|||||||
];
|
];
|
||||||
const resultsSuspend = buildMockOcrResults(filesSuspend, 'suspend', '', insMap);
|
const resultsSuspend = buildMockOcrResults(filesSuspend, 'suspend', '', insMap);
|
||||||
|
|
||||||
|
// 全部失败任务:便于演示「无成功仍可看失败明细」
|
||||||
|
const filesFailOnly = [
|
||||||
|
{ uid: 'demo-f1', name: '损坏文件_保单.pdf', status: 'done' },
|
||||||
|
{ uid: 'demo-f2', name: '非保单截图.jpg', status: 'done' },
|
||||||
|
];
|
||||||
|
const resultsFailOnly = buildMockOcrResults(filesFailOnly, 'policy', '商业险', insMap).map((r, idx) => (
|
||||||
|
markRecognResultFailed(
|
||||||
|
r,
|
||||||
|
idx === 0 ? POLICY_RECOGN_FAIL_REASON_CORRUPT : POLICY_RECOGN_FAIL_REASON_FORMAT,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
buildPolicyRecognTaskRecord({
|
buildPolicyRecognTaskRecord({
|
||||||
id: 'TASK-83892906',
|
id: 'TASK-83892906',
|
||||||
@@ -2616,6 +2664,18 @@ const createMockPolicyRecognTasks = () => {
|
|||||||
totalFileCount: filesSuspend.length,
|
totalFileCount: filesSuspend.length,
|
||||||
recognDoneCount: filesSuspend.length,
|
recognDoneCount: filesSuspend.length,
|
||||||
}),
|
}),
|
||||||
|
buildPolicyRecognTaskRecord({
|
||||||
|
id: 'TASK-84450001',
|
||||||
|
entry: 'ocr',
|
||||||
|
mode: 'policy',
|
||||||
|
insuranceType: '商业险',
|
||||||
|
results: resultsFailOnly,
|
||||||
|
createdAt: '2026-06-03 11:05:00',
|
||||||
|
completedAt: '2026-06-03 11:06:20',
|
||||||
|
phase: 'results',
|
||||||
|
totalFileCount: filesFailOnly.length,
|
||||||
|
recognDoneCount: filesFailOnly.length,
|
||||||
|
}),
|
||||||
buildPolicyRecognTaskRecord({
|
buildPolicyRecognTaskRecord({
|
||||||
id: 'TASK-84210588',
|
id: 'TASK-84210588',
|
||||||
entry: 'ocr',
|
entry: 'ocr',
|
||||||
@@ -3317,6 +3377,8 @@ const Component = function () {
|
|||||||
return stored && stored.length ? stored : createMockPolicyRecognTasks();
|
return stored && stored.length ? stored : createMockPolicyRecognTasks();
|
||||||
});
|
});
|
||||||
const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false);
|
const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false);
|
||||||
|
const [policyRecognFailDetailOpen, setPolicyRecognFailDetailOpen] = useState(false);
|
||||||
|
const [policyRecognFailDetailTask, setPolicyRecognFailDetailTask] = useState(null);
|
||||||
const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
||||||
const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
||||||
const [policyAddOpen, setPolicyAddOpen] = useState(false);
|
const [policyAddOpen, setPolicyAddOpen] = useState(false);
|
||||||
@@ -4602,10 +4664,27 @@ const Component = function () {
|
|||||||
if (preferred) {
|
if (preferred) {
|
||||||
setPolicyRecognActiveResultId(preferred.id);
|
setPolicyRecognActiveResultId(preferred.id);
|
||||||
setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, task.mode || 'policy'));
|
setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, task.mode || 'policy'));
|
||||||
showPolicyRecognResultPreview(preferred);
|
} else {
|
||||||
|
setPolicyRecognActiveResultId('');
|
||||||
|
setPolicyRecognConfirmDraft({ ...EMPTY_POLICY_DETAIL });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openPolicyRecognFailDetail = (task) => {
|
||||||
|
const fails = getPolicyRecognFailResults(task?.results);
|
||||||
|
if (!fails.length) {
|
||||||
|
message.info('该任务没有识别失败记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPolicyRecognFailDetailTask(task);
|
||||||
|
setPolicyRecognFailDetailOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const policyRecognFailDetailRows = useMemo(
|
||||||
|
() => getPolicyRecognFailResults(policyRecognFailDetailTask?.results),
|
||||||
|
[policyRecognFailDetailTask],
|
||||||
|
);
|
||||||
|
|
||||||
const closePolicyRecogn = () => {
|
const closePolicyRecogn = () => {
|
||||||
const syncedResults = policyRecognPhase === 'results'
|
const syncedResults = policyRecognPhase === 'results'
|
||||||
? persistActiveRecognDraft()
|
? persistActiveRecognDraft()
|
||||||
@@ -8120,13 +8199,31 @@ const Component = function () {
|
|||||||
{
|
{
|
||||||
title: '识别失败数',
|
title: '识别失败数',
|
||||||
dataIndex: 'recognFailCount',
|
dataIndex: 'recognFailCount',
|
||||||
width: 96,
|
width: 108,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (val) => (
|
render: (val, record) => {
|
||||||
<span style={{ fontVariantNumeric: 'tabular-nums', color: val > 0 ? '#dc2626' : '#64748b', fontWeight: 600 }}>
|
const count = val ?? 0;
|
||||||
{val ?? 0}
|
if (count > 0) {
|
||||||
</span>
|
return (
|
||||||
),
|
<button
|
||||||
|
type="button"
|
||||||
|
className="lc-policy-recogn-fail-count-btn"
|
||||||
|
aria-label={`查看 ${count} 条识别失败明细`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openPolicyRecognFailDetail(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span style={{ fontVariantNumeric: 'tabular-nums', color: '#64748b', fontWeight: 600 }}>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '识别进度',
|
title: '识别进度',
|
||||||
@@ -8171,31 +8268,112 @@ const Component = function () {
|
|||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const recognizing = isPolicyRecognTaskRecognizing(record);
|
const recognizing = isPolicyRecognTaskRecognizing(record);
|
||||||
const noSuccess = !(record.recognSuccessCount > 0);
|
const hasSuccess = (record.recognSuccessCount || 0) > 0;
|
||||||
const disabled = recognizing || noSuccess;
|
const hasFail = (record.recognFailCount || 0) > 0;
|
||||||
const action = (
|
|
||||||
<OperationActions
|
|
||||||
view={{
|
|
||||||
label: '确认识别结果',
|
|
||||||
disabled,
|
|
||||||
onClick: () => openPolicyRecognTaskRecord(record),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
if (recognizing) {
|
if (recognizing) {
|
||||||
return (
|
return (
|
||||||
<Tooltip title="请等待识别完成后操作">
|
<Tooltip title="请等待识别完成后操作">
|
||||||
<span>{action}</span>
|
<span>
|
||||||
|
<OperationActions
|
||||||
|
view={{
|
||||||
|
label: '确认识别结果',
|
||||||
|
disabled: true,
|
||||||
|
onClick: () => {},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return action;
|
if (!hasSuccess && hasFail) {
|
||||||
|
return (
|
||||||
|
<OperationActions
|
||||||
|
view={{
|
||||||
|
label: '查看失败明细',
|
||||||
|
onClick: () => openPolicyRecognFailDetail(record),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<OperationActions
|
||||||
|
view={{
|
||||||
|
label: '确认识别结果',
|
||||||
|
disabled: !hasSuccess,
|
||||||
|
onClick: () => openPolicyRecognTaskRecord(record),
|
||||||
|
}}
|
||||||
|
more={hasFail ? [{
|
||||||
|
key: 'failDetail',
|
||||||
|
label: '查看失败明细',
|
||||||
|
onClick: () => openPolicyRecognFailDetail(record),
|
||||||
|
}] : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
className="lc-policy-recogn-fail-detail-modal"
|
||||||
|
open={policyRecognFailDetailOpen}
|
||||||
|
title={policyRecognFailDetailTask
|
||||||
|
? `识别失败明细(${policyRecognFailDetailRows.length})`
|
||||||
|
: '识别失败明细'}
|
||||||
|
width={720}
|
||||||
|
centered
|
||||||
|
footer={(
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setPolicyRecognFailDetailOpen(false);
|
||||||
|
setPolicyRecognFailDetailTask(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
onCancel={() => {
|
||||||
|
setPolicyRecognFailDetailOpen(false);
|
||||||
|
setPolicyRecognFailDetailTask(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 14, borderRadius: 8 }}
|
||||||
|
message="以下文件识别未成功,不会进入确认写入台账;可核对文件后重新发起识别。"
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
size="middle"
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={policyRecognFailDetailRows}
|
||||||
|
pagination={policyRecognFailDetailRows.length > 8
|
||||||
|
? { pageSize: 8, showSizeChanger: false }
|
||||||
|
: false}
|
||||||
|
locale={{ emptyText: '暂无失败记录' }}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '文件/保单名',
|
||||||
|
dataIndex: 'fileName',
|
||||||
|
width: 280,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (val) => (
|
||||||
|
<span style={{ fontWeight: 600, color: '#0f172a' }}>{val || '—'}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '失败原因',
|
||||||
|
dataIndex: 'failReason',
|
||||||
|
render: (val) => (
|
||||||
|
<span style={{ color: '#b91c1c', lineHeight: 1.45 }}>{val || '—'}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
className="lc-policy-recogn-modal"
|
className="lc-policy-recogn-modal"
|
||||||
title={
|
title={
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -460,6 +460,46 @@
|
|||||||
margin-bottom: var(--oneos-space-3);
|
margin-bottom: var(--oneos-space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 识别任务:失败数可点击入口 */
|
||||||
|
.lc-policy-recogn-fail-count-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lc-policy-recogn-fail-count-btn:hover,
|
||||||
|
.lc-policy-recogn-fail-count-btn:focus-visible {
|
||||||
|
color: #b91c1c;
|
||||||
|
background: color-mix(in srgb, #ef4444 10%, transparent);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lc-policy-recogn-fail-count-btn:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, #ef4444 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.lc-policy-recogn-fail-count-btn {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1280px) {
|
@media (max-width: 1280px) {
|
||||||
.vm-page.ipc-page .lc-alert-stats-row {
|
.vm-page.ipc-page .lc-alert-stats-row {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
Building2,
|
Building2,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
CalendarClock,
|
||||||
Car,
|
Car,
|
||||||
CircleDollarSign,
|
CircleDollarSign,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
@@ -772,34 +773,53 @@ export const SELF_OPERATED_MODULES: LineModule[] = [
|
|||||||
id: 'self-contract',
|
id: 'self-contract',
|
||||||
step: 1,
|
step: 1,
|
||||||
title: '自营合同',
|
title: '自营合同',
|
||||||
subtitle: '关键信息录入 · 上传合同 · 自动交车',
|
subtitle: '关键信息录入 · 业务确认生效 · 可派车',
|
||||||
shortLabel: '合同',
|
shortLabel: '合同',
|
||||||
accent: 'blue',
|
accent: 'blue',
|
||||||
icon: FileSignature,
|
icon: FileSignature,
|
||||||
roles: ['业务管理组', '业务服务组'],
|
roles: ['业务管理组', '业务服务组'],
|
||||||
start: '创建自营合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。',
|
start: '创建自营运力服务合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。',
|
||||||
process: [
|
process: [
|
||||||
'录入自营项目与客户方、车型、车辆数量等合同要素。',
|
'录入自营项目与客户方、车型、车辆数量等合同要素。',
|
||||||
'填写交车时间、交车地点,上传自营合同扫描件。',
|
'填写交车时间、交车地点,上传自营合同扫描件。',
|
||||||
'合同提交后自动生成交车任务,推送运维条线处理交付。',
|
'业务确认生效后可创建轻量调度任务,并提示运维交车交付。',
|
||||||
],
|
],
|
||||||
closure: '合同信息与交车任务联动,自营项目从签约起即可进入交付与运营环节。',
|
closure: '合同界定项目边界,成为调度派车与交车的上游凭证。',
|
||||||
prototypeHref: '/prototypes/oneos-web-lease-contract',
|
prototypeHref: '/prototypes/self-operated-contract',
|
||||||
prototypeLabel: '打开车辆租赁合同',
|
prototypeLabel: '打开自营合同',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'self-dispatch',
|
||||||
|
step: 2,
|
||||||
|
title: '调度任务',
|
||||||
|
subtitle: '轻量派车 · 交还车校验 · 办结出车',
|
||||||
|
shortLabel: '调度',
|
||||||
|
accent: 'indigo',
|
||||||
|
icon: CalendarClock,
|
||||||
|
roles: ['业务服务组', '调度岗', '运维部'],
|
||||||
|
start: '在生效自营合同下创建出车任务,派车派司机;非 TMS,不做配载与承运商调度。',
|
||||||
|
process: [
|
||||||
|
'选择合同、计划出车日、线路说明、是否多趟与计价口径。',
|
||||||
|
'派车派司机;车辆未交车时先完成运维交车(原型可模拟交车)。',
|
||||||
|
'办结确认实际出车后,自动生成物流业务明细一行。',
|
||||||
|
],
|
||||||
|
closure: '出车事实可追溯到合同与车牌,并驱动台账落账。',
|
||||||
|
prototypeHref: '/prototypes/self-operated-dispatch-task',
|
||||||
|
prototypeLabel: '打开调度任务',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'self-ledger',
|
id: 'self-ledger',
|
||||||
step: 2,
|
step: 3,
|
||||||
title: '自营台账',
|
title: '自营台账',
|
||||||
subtitle: '导入台账 · 成本利润核算 · 项目盈亏',
|
subtitle: '任务自动生成 · 补录导入 · 盈亏核算',
|
||||||
shortLabel: '台账',
|
shortLabel: '台账',
|
||||||
accent: 'emerald',
|
accent: 'emerald',
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
roles: ['业务服务组', '财务部'],
|
roles: ['业务服务组', '财务部'],
|
||||||
start: '根据导入的自营台账与账单信息,汇总项目侧收入与成本数据。',
|
start: '承接调度办结自动生成的出车明细,并以导入/行编辑补录异常与历史数据。',
|
||||||
process: [
|
process: [
|
||||||
'导入或维护自营运营台账、账单明细。',
|
'调度办结后自动写入出车营收侧字段,并计算金额/总成本/盈亏。',
|
||||||
'自动计算利润侧数据,以及司机成本、车辆成本侧数据。',
|
'氢费、ETC 等成本一期可手工补录;导入模板保留为补录通道。',
|
||||||
'汇集至项目盈亏表,支撑项目经营分析。',
|
'汇集至项目盈亏表,支撑项目经营分析。',
|
||||||
'演进阶段关联财务「收款记录」,形成业财记录闭环。',
|
'演进阶段关联财务「收款记录」,形成业财记录闭环。',
|
||||||
],
|
],
|
||||||
@@ -924,26 +944,26 @@ export const BUSINESS_LINES: BusinessLineConfig[] = [
|
|||||||
shortLabel: '自营',
|
shortLabel: '自营',
|
||||||
hero: {
|
hero: {
|
||||||
title: '自营业务条线',
|
title: '自营业务条线',
|
||||||
tagline: '自营物流条线合同->账单业财闭环',
|
tagline: '自营合同 → 轻量调度 → 交还车 → 台账自动生成',
|
||||||
summary:
|
summary:
|
||||||
'两大模块串联自营合同与自营台账:合同驱动交车任务,台账导入后自动核算司机、车辆成本与项目盈亏,逐步对接财务收款形成业财闭环。',
|
'三大模块串联自营合同、轻量调度任务与自营台账:合同界定项目,调度派车办结驱动明细自动生成,运维交还车保障运营态,逐步对接财务收款形成业财闭环。',
|
||||||
stats: [
|
stats: [
|
||||||
{ value: '2', label: '业务模块' },
|
{ value: '3', label: '业务模块' },
|
||||||
{ value: '4+', label: '协同部门' },
|
{ value: '4+', label: '协同部门' },
|
||||||
{ value: '1', label: '业财闭环' },
|
{ value: '1', label: '经营闭环' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
modules: SELF_OPERATED_MODULES,
|
modules: SELF_OPERATED_MODULES,
|
||||||
loopTitle: '合同到台账,自营经营可核算',
|
loopTitle: '合同到调度再到台账,自营经营可核算',
|
||||||
loopSummary:
|
loopSummary:
|
||||||
'自营合同录入并生成交车任务 → 运营数据导入自营台账 → 自动汇总利润与司机、车辆成本 → 汇集项目盈亏表 → 关联收款记录完成业财闭环。',
|
'自营合同生效 → 轻量调度派车(交车后可办结)→ 办结自动生成物流业务明细 → 自动汇总盈亏 → 汇集项目盈亏表 → 演进关联收款记录完成业财闭环。',
|
||||||
evolution: {
|
evolution: {
|
||||||
currentLabel: '现阶段 · 人工核算台账',
|
currentLabel: '现阶段 · 合同 + 调度办结落账',
|
||||||
currentText:
|
currentText:
|
||||||
'人工发起自营合同,人工计算车辆成本、司机成本与自营台账,项目盈亏依赖线下表格汇总与人工核对。',
|
'自营合同业务确认生效;轻量调度派车办结(须交车);台账以自动生成为主、导入为辅,盈亏在台账内可算。',
|
||||||
targetLabel: '演进方向 · 业财记录闭环',
|
targetLabel: '演进方向 · 业财记录闭环',
|
||||||
targetText:
|
targetText:
|
||||||
'仍人工发起合同;司机在线管理、司机成本自动计算;自营台账人工导入后自动汇总,关联财务「收款记录」模块,形成业财记录闭环。',
|
'成本侧氢费/ETC/日成本自动回填;司机在线管理;台账关联财务「收款记录」,形成业财记录闭环;不做完整 TMS。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
# 加氢订单(H5)— 已确认需求与设计决策
|
# 加氢订单(H5)— 已确认需求与设计决策
|
||||||
|
|
||||||
|
> **状态:已收敛 / 部分作废(2026-07-18)**
|
||||||
|
> 现行权威规格以 [requirements-prd.md](./requirements-prd.md) 及同目录功能规格为准。
|
||||||
|
> 下文保留为 2026-07-14 对齐快照;下列条目**已被后续决策覆盖,勿再按本文件验收**:
|
||||||
|
>
|
||||||
|
> | 快照原文 | 现行口径 |
|
||||||
|
> |---|---|
|
||||||
|
> | 顶栏演示可切换示例站 | **H5 无切站**,固定登录本站 |
|
||||||
|
> | 列表展示对账状态 / 对账时间 | 列表只展示**核对**;对账见详情 |
|
||||||
|
> | 已对账才只读 | **羚牛**已核对或已对账即锁定;非羚牛不锁 |
|
||||||
|
> | 无底 Tab | 已有底栏 Tab:加氢订单 / 手工台账 |
|
||||||
|
> | 分步 OCR 向导 | 已改为单表单 + OCR 拍摄区 |
|
||||||
|
|
||||||
| 项 | 内容 |
|
| 项 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 日期 | 2026-07-14 |
|
| 日期 | 2026-07-14(快照);覆盖说明 2026-07-18 |
|
||||||
| 原型 ID | `oneos-h5-h2-order` |
|
| 原型 ID | `oneos-h5-h2-order` |
|
||||||
| 菜单位置 | OneOS → 加氢站管理 → 加氢订单 |
|
| 菜单位置 | OneOS → 加氢站管理 → 加氢订单 |
|
||||||
| 文档性质 | 决策快照(对齐当时确认,不随实现逐行同步) |
|
| 文档性质 | 历史决策快照(**不随实现同步**;冲突处以 PRD 为准) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,8 +26,8 @@
|
|||||||
| 问题 | 用户选择 |
|
| 问题 | 用户选择 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 列表数据范围 | **A. 仅本站**(当前演示站点绑定的加氢站) |
|
| 列表数据范围 | **A. 仅本站**(当前演示站点绑定的加氢站) |
|
||||||
| 登录态 | **A. 已登录态**;顶栏展示站名,演示可切换示例站 |
|
| 登录态 | **A. 已登录态**;顶栏展示站名 ~~演示可切换示例站~~ → **已收敛:无切站** |
|
||||||
| 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除 |
|
| 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除 → **已收敛:羚牛·已核对或已对账均锁** |
|
||||||
| OCR 演示 | **A. 拍照或选图 → 约 1s 模拟识别 → 可校正**(已改为单表单,非三步) |
|
| OCR 演示 | **A. 拍照或选图 → 约 1s 模拟识别 → 可校正**(已改为单表单,非三步) |
|
||||||
|
|
||||||
### 1.1 目标与范围
|
### 1.1 目标与范围
|
||||||
@@ -61,8 +73,8 @@
|
|||||||
| 预览壳 | 居中 390×844 手机框;真机浏览器可全宽 |
|
| 预览壳 | 居中 390×844 手机框;真机浏览器可全宽 |
|
||||||
| 首屏 | 列表为主 + 悬浮「+」 |
|
| 首屏 | 列表为主 + 悬浮「+」 |
|
||||||
| 卡片优先级 | 车牌、状态、时间、量与金额;单价 / 对账时间次级 |
|
| 卡片优先级 | 车牌、状态、时间、量与金额;单价 / 对账时间次级 |
|
||||||
| 无底 Tab | 本模块单入口,不套小羚羚全局 Tab |
|
| 无底 Tab | ~~本模块单入口~~ → **已收敛:底栏「加氢订单 / 手工台账」** |
|
||||||
| 演示切换 | 顶栏站点下拉,仅演示,不代表正式鉴权 |
|
| 演示切换 | ~~顶栏站点下拉~~ → **已收敛:H5 固定本站、无切站** |
|
||||||
|
|
||||||
### 2.2 信息架构
|
### 2.2 信息架构
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。
|
查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。
|
||||||
|
|
||||||
## 详情展示
|
## 详情展示
|
||||||
|
|
||||||
@@ -11,15 +11,18 @@
|
|||||||
- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图)
|
- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图)
|
||||||
- 明细行:站点、车牌、里程、单价、核对/对账状态与时间
|
- 明细行:站点、车牌、里程、单价、核对/对账状态与时间
|
||||||
|
|
||||||
|
> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。
|
||||||
|
|
||||||
## 锁定规则
|
## 锁定规则
|
||||||
|
|
||||||
| 条件 | 结果 |
|
| 优先级 | 条件 | 结果 |
|
||||||
|------|------|
|
|--------|------|------|
|
||||||
| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |
|
| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |
|
||||||
| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |
|
| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |
|
||||||
| 未核对且未对账 | 可编辑、可删除 |
|
| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |
|
||||||
|
| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |
|
||||||
|
|
||||||
判定函数:`isOrderLocked`(核对或对账任一成立即锁)。
|
判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。
|
||||||
|
|
||||||
## 编辑
|
## 编辑
|
||||||
|
|
||||||
@@ -27,11 +30,12 @@
|
|||||||
|
|
||||||
## 删除
|
## 删除
|
||||||
|
|
||||||
二次确认后从 Bridge 移除该行。
|
二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|
||||||
1. 未核对未对账:底部有删除/编辑
|
1. 羚牛 ∧ 未核对未对账:底部有删除/编辑
|
||||||
2. 已核对或已对账:无操作栏,有锁定说明
|
2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明
|
||||||
3. 详情含对账信息;列表卡片不含对账
|
3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」
|
||||||
4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图)
|
4. 详情含对账信息;列表卡片不含对账
|
||||||
|
5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图)
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
| 项 | 规则 |
|
| 项 | 规则 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |
|
| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |
|
||||||
| 站点范围 | 仅展示当前登录用户角色对应站点;单站人员仅 1 个、多站可切换、超级管理员可选全部(原型用标注状态切换演示,未接真实登录) |
|
| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |
|
||||||
| 上传形态 | 仅图片(拍照/相册),可多张 |
|
| 上传形态 | 仅图片(拍照/相册),可多张 |
|
||||||
| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |
|
| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |
|
||||||
| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |
|
| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |
|
||||||
| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |
|
| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |
|
||||||
| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |
|
| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |
|
||||||
| 二次确认 | Web 点「确认上传/补传」先弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
|
| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
|
||||||
| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |
|
| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |
|
||||||
| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |
|
| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |
|
||||||
|
|
||||||
## 判定顺序(新增拦截)
|
## 判定顺序(新增拦截)
|
||||||
@@ -24,8 +24,9 @@
|
|||||||
| 优先级 | 条件 | 结果 |
|
| 优先级 | 条件 | 结果 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| 1 | 操作=编辑已有记录 | 不校验手工台账 |
|
| 1 | 操作=编辑已有记录 | 不校验手工台账 |
|
||||||
| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |
|
| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |
|
||||||
| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |
|
| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |
|
||||||
|
| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |
|
||||||
|
|
||||||
## 日历与补传
|
## 日历与补传
|
||||||
|
|
||||||
@@ -36,26 +37,27 @@
|
|||||||
| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |
|
| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |
|
||||||
| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |
|
| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |
|
||||||
| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |
|
| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |
|
||||||
| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |
|
| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |
|
||||||
|
|
||||||
## 用户可见
|
## 用户可见
|
||||||
|
|
||||||
- 今日状态双反馈(Web 放在右侧「当日台账」卡片内):未上传「今日尚未上传手工台账(YYYY-MM-DD)」异常态;已上传「今日已上传手工台账(YYYY-MM-DD)」正常态
|
- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」
|
||||||
- 今日未上传:橙/黄提示 + Tab 红点
|
- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」
|
||||||
- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要
|
- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要
|
||||||
- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高
|
- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高
|
||||||
|
|
||||||
## 代码路径
|
## 代码路径
|
||||||
|
|
||||||
- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`)
|
- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`)
|
||||||
- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`)
|
- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`)
|
||||||
- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历)
|
- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日)
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|
||||||
1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传
|
1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日)
|
||||||
2. 上传至少一张图并确认后,可新增电子记录
|
2. 补传昨日台账并确认后,可新增加氢记录
|
||||||
3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传
|
3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传
|
||||||
4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截
|
4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截
|
||||||
5. 确认上传后的图显示已归档且不可删除;仅可追加补传
|
5. 确认上传后的图显示已归档且不可删除;仅可追加补传
|
||||||
6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话)
|
6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话)
|
||||||
|
7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
原型本地判定,未接真实车辆管理 API。
|
原型本地判定,未接真实车辆管理 API。
|
||||||
|
|
||||||
|
全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5):
|
||||||
|
[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)
|
||||||
|
|
||||||
## 判定顺序
|
## 判定顺序
|
||||||
|
|
||||||
| 优先级 | 条件 | 结果 |
|
| 优先级 | 条件 | 结果 |
|
||||||
@@ -17,14 +20,14 @@
|
|||||||
|
|
||||||
## 数据源
|
## 数据源
|
||||||
|
|
||||||
- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。
|
- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。
|
||||||
- 正式环境应改为查询车辆管理 / 系统车辆列表接口。
|
|
||||||
|
|
||||||
## 用户可见结果
|
## 用户可见结果
|
||||||
|
|
||||||
- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。
|
- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。
|
||||||
|
- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。
|
||||||
- 不拦截提交(仅提示归属)。
|
- 不拦截提交(仅提示归属)。
|
||||||
|
|
||||||
## 代码路径
|
## 代码路径
|
||||||
|
|
||||||
- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。
|
- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`
|
||||||
|
|||||||
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`。
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |
|
| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |
|
||||||
| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||||
|
| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||||
| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |
|
| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
|
|
||||||
> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。
|
> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2. 数据流
|
## 2. 数据流
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -26,23 +29,29 @@
|
|||||||
|
|
||||||
能源部 · 完成核对
|
能源部 · 完成核对
|
||||||
→ verifyStatus=verified + verifiedAt
|
→ verifyStatus=verified + verifiedAt
|
||||||
→ 站端同步「已核对」,禁改删
|
→ 站端同步「已核对」;羚牛车辆禁改删
|
||||||
|
|
||||||
站点信息 · 对账单提交(仅已核对 ∧ 未对账)
|
站点信息 · 对账单提交(仅已核对 ∧ 未对账)
|
||||||
→ reconcileStatus=reconciled + reconcileDate + statementRecordId
|
→ reconcileStatus=reconciled + reconcileDate + statementRecordId
|
||||||
→ 站端 / 台账同步「已对账」
|
→ 站端 / 台账同步「已对账」;羚牛车辆禁改删
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 3. 用户可见结果(H5)
|
## 3. 用户可见结果(H5)
|
||||||
|
|
||||||
| 结果 | 行为 |
|
| 结果 | 行为 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 未核对 | 可编辑、删除 |
|
| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |
|
||||||
| 已核对未对账 | 只读;提示能源部已核对 |
|
| 羚牛 · 未核对 | 可编辑、删除 |
|
||||||
| 已对账 | 只读;提示已纳入对账单 |
|
| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |
|
||||||
|
| 羚牛 · 已对账 | 只读;提示已纳入对账单 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 4. 边界
|
## 4. 边界
|
||||||
|
|
||||||
- 站端上报默认未核对、未对账
|
- 站端上报默认未核对、未对账
|
||||||
|
- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)
|
||||||
- 对账时间只认 `reconcileDate`(对账单回写)
|
- 对账时间只认 `reconcileDate`(对账单回写)
|
||||||
- 跨页联动依赖同浏览器会话共享 Store
|
- 跨页联动依赖同浏览器会话共享 Store
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
| 项 | 内容 |
|
| 项 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 文档版本 | v1.5 |
|
| 文档版本 | v1.7 |
|
||||||
| 模块名称 | 加氢站管理 → 加氢订单 |
|
| 模块名称 | 加氢站管理 → 加氢订单 |
|
||||||
| 所属系统 | ONE-OS(加氢站手机浏览器) |
|
| 所属系统 | ONE-OS(加氢站手机浏览器) |
|
||||||
| 交互原型 | `/prototypes/oneos-h5-h2-order` |
|
| 交互原型 | `/prototypes/oneos-h5-h2-order` |
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。
|
采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。
|
||||||
|
|
||||||
每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。
|
每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。
|
||||||
|
|
||||||
### 1.1 目标用户
|
### 1.1 目标用户
|
||||||
|
|
||||||
@@ -46,14 +46,15 @@
|
|||||||
|
|
||||||
1. 菜单位于 OneOS → 加氢站管理 → 加氢订单
|
1. 菜单位于 OneOS → 加氢站管理 → 加氢订单
|
||||||
2. 默认已登录本站;列表仅本站(无切站)
|
2. 默认已登录本站;列表仅本站(无切站)
|
||||||
3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分)
|
3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分)
|
||||||
4. 已核对或已对账不可编辑/删除;未核对未对账可
|
4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删
|
||||||
5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间
|
5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛)
|
||||||
6. 同站同车牌同加氢时间重复保存被拦截
|
6. 同站同车牌同加氢时间重复保存被拦截
|
||||||
7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store)
|
7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store)
|
||||||
8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock)
|
8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock)
|
||||||
9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索
|
9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索
|
||||||
10. 详情页展示车牌与面板带水印原图
|
10. 详情页展示车牌与面板带水印原图
|
||||||
|
11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,11 +62,11 @@
|
|||||||
|
|
||||||
| 主题 | 摘要 | 全文 |
|
| 主题 | 摘要 | 全文 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
| 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||||
| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |
|
| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |
|
||||||
| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||||
| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |
|
| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |
|
||||||
| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) |
|
| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -77,4 +78,16 @@
|
|||||||
| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |
|
| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |
|
||||||
| `detail` | 详情字段、锁定说明、编辑/删除 |
|
| `detail` | 详情字段、锁定说明、编辑/删除 |
|
||||||
|
|
||||||
|
右侧工具栏「原型目录」结构:
|
||||||
|
|
||||||
|
| 目录 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| PRD 全文 | requirements-prd.md |
|
||||||
|
| 需求角色 | roles.md |
|
||||||
|
| 用户故事 | user-stories.md |
|
||||||
|
| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |
|
||||||
|
| 流程图 | flows.md(Mermaid) |
|
||||||
|
| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |
|
||||||
|
| 按页面 | list / create / detail 要点 |
|
||||||
|
|
||||||
标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。
|
标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。
|
||||||
|
|||||||
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 识别羚牛车辆
|
||||||
|
|
||||||
|
**作为** 加氢站操作人员,
|
||||||
|
**我希望** 在列表/详情看到是否羚牛车辆,
|
||||||
|
**以便** 理解后续核对对账是否适用该车。
|
||||||
|
|
||||||
|
**验收要点**
|
||||||
|
|
||||||
|
- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定)
|
||||||
@@ -5,13 +5,13 @@
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"prototypeName": "oneos-h5-h2-order",
|
"prototypeName": "oneos-h5-h2-order",
|
||||||
"pageId": "list",
|
"pageId": "list",
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-hero",
|
"id": "h5-hero",
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-summary",
|
"id": "h5-summary",
|
||||||
@@ -52,8 +52,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-card",
|
"id": "h5-card",
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-fab",
|
"id": "h5-fab",
|
||||||
@@ -94,8 +94,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-time",
|
"id": "h5-time",
|
||||||
@@ -115,8 +115,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-station",
|
"id": "h5-station",
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-plate",
|
"id": "h5-plate",
|
||||||
@@ -157,8 +157,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-mileage",
|
"id": "h5-mileage",
|
||||||
@@ -178,8 +178,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-dispenser-brands",
|
"id": "h5-dispenser-brands",
|
||||||
@@ -199,8 +199,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-price",
|
"id": "h5-price",
|
||||||
@@ -220,8 +220,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-kg",
|
"id": "h5-kg",
|
||||||
@@ -241,8 +241,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-total-label",
|
"id": "h5-total-label",
|
||||||
@@ -262,8 +262,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-detail",
|
"id": "h5-detail",
|
||||||
@@ -283,8 +283,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-detail-photos",
|
"id": "h5-detail-photos",
|
||||||
@@ -304,8 +304,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-manual",
|
"id": "h5-manual",
|
||||||
@@ -325,8 +325,8 @@
|
|||||||
{
|
{
|
||||||
"images": [],
|
"images": [],
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"createdAt": 1784280448632,
|
"createdAt": 1784378211658,
|
||||||
"updatedAt": 1784280448632,
|
"updatedAt": 1784378211658,
|
||||||
"hasMarkdown": true,
|
"hasMarkdown": true,
|
||||||
"annotationText": "",
|
"annotationText": "",
|
||||||
"id": "h5-manual-calendar",
|
"id": "h5-manual-calendar",
|
||||||
@@ -352,16 +352,16 @@
|
|||||||
"h5-fab": "## 底栏新增\n\n固定在列表底部,不遮挡卡片滚动区。点击进入新增表单,加氢时间默认=点击时刻。\n\n详见功能说明「新增加氢记录」。",
|
"h5-fab": "## 底栏新增\n\n固定在列表底部,不遮挡卡片滚动区。点击进入新增表单,加氢时间默认=点击时刻。\n\n详见功能说明「新增加氢记录」。",
|
||||||
"h5-time": "## 加氢时间(必填)\n\n默认=点击「新增」时的当前时间;点击后底部弹层选择日期 + 时 + 分。",
|
"h5-time": "## 加氢时间(必填)\n\n默认=点击「新增」时的当前时间;点击后底部弹层选择日期 + 时 + 分。",
|
||||||
"h5-station": "## 加氢站名称\n\n只读。默认显示当前登录账号对应加氢站名称,不可切换。",
|
"h5-station": "## 加氢站名称\n\n只读。默认显示当前登录账号对应加氢站名称,不可切换。",
|
||||||
"h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n",
|
"h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||||
"h5-mileage": "## 里程(km)\n\n选填。支持「从车机获取」(原型 Mock);识别车牌后可自动回填。正式环境对接车机 / T-Box。\n\n详见 [新增加氢记录](feature-create)。",
|
"h5-mileage": "## 里程(km)\n\n选填。支持「从车机获取」(原型 Mock);识别车牌后可自动回填。正式环境对接车机 / T-Box。\n\n详见 [新增加氢记录](feature-create)。",
|
||||||
"h5-dispenser-brands": "## 加氢机品牌参考\n\n展示站点信息「加氢机品牌管理」维护的品牌·型号,供面板 OCR 模型判断参考。数据源:`h2DispenserBrandStore`。",
|
"h5-dispenser-brands": "## 加氢机品牌参考\n\n展示站点信息「加氢机品牌管理」维护的品牌·型号,供面板 OCR 模型判断参考。数据源:`h2DispenserBrandStore`。",
|
||||||
"h5-price": "## 氢气单价(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额(单价×加氢量)。",
|
"h5-price": "## 氢气单价(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额(单价×加氢量)。",
|
||||||
"h5-kg": "## 加氢量(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额。",
|
"h5-kg": "## 加氢量(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额。",
|
||||||
"h5-total-label": "## 加氢总额(元)\n\n只读展示;由单价×加氢量自动计算(两位小数)。界面标签不展示公式文案。",
|
"h5-total-label": "## 加氢总额(元)\n\n只读展示;由单价×加氢量自动计算(两位小数)。界面标签不展示公式文案。",
|
||||||
"h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
"h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||||
"h5-detail-photos": "## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片;照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。",
|
"h5-detail-photos": "## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片;照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。",
|
||||||
"h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API) |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示条 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历) \n\n## 验收\n\n1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n",
|
"h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||||
"h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。仅「今日」是否上传影响新增加氢记录。\n\n详见 [手工台账上传](feature-manual-ledger)。"
|
"h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。**前一日**未上传时禁止新增加氢记录(今日台账状态不作为门禁)。\n\n详见 [手工台账上传](feature-manual-ledger)。"
|
||||||
},
|
},
|
||||||
"assetMap": {},
|
"assetMap": {},
|
||||||
"directory": {
|
"directory": {
|
||||||
@@ -376,9 +376,90 @@
|
|||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"id": "h5-doc-prd",
|
"id": "h5-doc-prd",
|
||||||
"title": "PRD 全文",
|
"title": "PRD 全文",
|
||||||
"markdown": "# 加氢订单(H5)— 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.5 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS(加氢站手机浏览器) |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员(户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态;已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分) \n4. 已核对或已对账不可编辑/删除;未核对未对账可 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间 \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store) \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock) \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n",
|
"markdown": "# 加氢订单(H5)— 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.7 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS(加氢站手机浏览器) |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员(户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态;已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分) \n4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛) \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store) \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock) \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n右侧工具栏「原型目录」结构:\n\n| 目录 | 内容 |\n|---|---|\n| PRD 全文 | requirements-prd.md |\n| 需求角色 | roles.md |\n| 用户故事 | user-stories.md |\n| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |\n| 流程图 | flows.md(Mermaid) |\n| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |\n| 按页面 | list / create / detail 要点 |\n\n标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n",
|
||||||
"markdownPath": ".spec/requirements-prd.md"
|
"markdownPath": ".spec/requirements-prd.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "folder",
|
||||||
|
"id": "h5-doc-roles",
|
||||||
|
"title": "需求角色",
|
||||||
|
"defaultExpanded": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "markdown",
|
||||||
|
"id": "h5-doc-roles-md",
|
||||||
|
"title": "角色说明",
|
||||||
|
"markdown": "# 需求角色\n\n| 项 | 说明 |\n|---|---|\n| 适用原型 | 加氢订单(H5)`oneos-h5-h2-order` |\n| 关联 Web | 加氢记录 `oneos-web-h2-station`(同一业务、不同端) |\n| 数据 | 原型本地种子 + 共享 Bridge(未接真实登录 / API) |\n\n---\n\n## 1. 角色一览\n\n| 角色 | 端 | 核心诉求 | 本原型是否为主用户 |\n|---|---|---|---|\n| **加氢站操作人员** | H5(手机浏览器) | 在站场快速上报本站加氢流水;补传手工台账照片 | **是** |\n| **加氢站站长 / 多站管理员** | Web 为主,H5 可看本站 | 多站台账、导出、手工台账补传与督导 | 间接(H5 固定单站演示) |\n| **能源部核对人员** | 车辆氢费明细 | 完成核对,回写「已核对」 | 否(触发方在台账) |\n| **站点对账人员** | 站点信息 · 对账单 | 提交对账单,回写「已对账」 | 否(触发方在站点信息) |\n| **超级管理员** | Web | 查看全部站点流水与台账 | 否(Web 标注状态演示) |\n\n---\n\n## 2. 本原型主角色:加氢站操作人员\n\n| 项 | 说明 |\n|---|---|\n| 场景 | 户外 / 站场,手机竖屏 |\n| 权限边界 | 仅本站数据;不可切站(原型固定演示站) |\n| 日常动作 | 看列表 KPI → 上传昨日手工台账(若缺失)→ 新增加氢记录 → 查看/编辑未锁定记录 |\n| 约束 | 前一日手工台账未上传则今日不可新增;已核对或已对账不可改删 |\n\n---\n\n## 3. 角色边界(本期不做)\n\n- 真登录与组织权限中心对接 \n- 能源部 / 对账人员在 H5 内操作核对或对账 \n- 跨站汇总与导出(属 Web「加氢记录」) \n",
|
||||||
|
"markdownPath": ".spec/roles.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "folder",
|
||||||
|
"id": "h5-doc-stories",
|
||||||
|
"title": "用户故事",
|
||||||
|
"defaultExpanded": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "markdown",
|
||||||
|
"id": "h5-doc-stories-md",
|
||||||
|
"title": "用户故事",
|
||||||
|
"markdown": "# 用户故事\n\n> 以加氢站操作人员为主;验收口径见 [requirements-prd.md](./requirements-prd.md)。\n\n---\n\n## US-01 查看本站加氢订单\n\n**作为** 加氢站操作人员, \n**我希望** 打开加氢订单即看到本站列表与 KPI, \n**以便** 快速了解今日/近期上报情况。\n\n**验收要点**\n\n- 默认已登录本站;无切站 \n- KPI:订单数 / 加氢量 / 加氢金额随列表汇总 \n- 卡片展示加氢时间、车牌、量、额、核对状态等 \n\n---\n\n## US-02 补传缺失的手工台账后再新增\n\n**作为** 加氢站操作人员, \n**我希望** 在前一日手工台账未上传时被明确拦截,并直达台账页补传, \n**以便** 保证纸质台账作为对账依据不缺失。\n\n**验收要点**\n\n- 顶栏橙条 + 底栏提示:「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 点「新增」或顶栏橙条跳转手工台账并选中昨日 \n- 补传昨日确认后可新增 \n\n---\n\n## US-03 新增加氢记录\n\n**作为** 加氢站操作人员, \n**我希望** 用车牌键盘 + 面板拍照/OCR 快速填单, \n**以便** 在站场少打字完成上报。\n\n**验收要点**\n\n- 加氢时间默认=点击新增时刻(到分) \n- 站名只读为本站 \n- 总额 = 单价 × 加氢量(界面不展示公式) \n- 同站同车牌同时间重复保存拦截 \n\n---\n\n## US-04 查看详情并编辑/删除未锁定记录\n\n**作为** 加氢站操作人员, \n**我希望** 打开详情查看字段与识别原图,并对未核对未对账记录改删, \n**以便** 纠正录入错误。\n\n**验收要点**\n\n- 羚牛车辆已核对或已对账:不可编辑/删除,有锁定说明;非羚牛不锁定 \n- 详情展示车牌/面板含水印原图 \n\n---\n\n## US-05 按日历管理手工台账\n\n**作为** 加氢站操作人员, \n**我希望** 在月历上看已传/未传并补传过往日, \n**以便** 多日未操作后仍能补齐缺口。\n\n**验收要点**\n\n- 自首笔加氢日起绿/橙标记;首笔前不要求 \n- 禁选未来日;已归档图不可删,仅可追加 \n\n---\n\n## US-06 识别羚牛车辆\n\n**作为** 加氢站操作人员, \n**我希望** 在列表/详情看到是否羚牛车辆, \n**以便** 理解后续核对对账是否适用该车。\n\n**验收要点**\n\n- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定) \n",
|
||||||
|
"markdownPath": ".spec/user-stories.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "folder",
|
||||||
|
"id": "h5-doc-logic",
|
||||||
|
"title": "关键逻辑",
|
||||||
|
"defaultExpanded": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "markdown",
|
||||||
|
"id": "h5-doc-logic-index",
|
||||||
|
"title": "逻辑索引",
|
||||||
|
"markdown": "# 关键逻辑索引\n\n本页汇总加氢订单(H5)复杂判定入口;细则见各规格全文。原型数据均为本地种子 / 内存 Store,**未接真实 API**。\n\n---\n\n## 1. 判定主题一览\n\n| 主题 | 一句话 | 全文 |\n|---|---|---|\n| 手工台账门禁 | **昨日**未传 → 今日禁新增;固定提示文案并直达台账 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 核对 ≠ 对账 | 能源部核对 / 站点对账单提交,触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md) |\n| 记录锁定 | **仅羚牛**:已核对或已对账 → 站端不可改删;非羚牛不锁 | [feature-detail.md](./feature-detail.md)、[reconcile-linkage.md](./reconcile-linkage.md)、[fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 重复键 | 同站 + 同车牌 + 同加氢时间 | Bridge / [Web record-data-model](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 总额计算 | 总额 = 单价 × 量(两位小数) | [feature-create.md](./feature-create.md) |\n| 羚牛车辆 | 本地系统车牌集合 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n\n---\n\n## 2. 手工台账门禁(摘要)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 编辑已有记录 | 不校验 |\n| 2 | 昨日早于本站首笔加氢日 | 不要求昨日台账,可新增 |\n| 3 | 昨日已有 ≥1 张台账图 | 可新增 |\n| 4 | 昨日未上传 | 提示并跳转手工台账(选中昨日) |\n\n用户可见文案:`手工台账有缺失,作为对账重要依据,请先上传手工台账`\n\n---\n\n## 3. 核对 / 对账(摘要)\n\n| 状态 | 谁触发 | 站端影响 |\n|---|---|---|\n| 已核对 | 车辆氢费明细「完成核对」 | **羚牛**不可改删 |\n| 已对账 | 站点信息对账单提交 | **羚牛**不可改删;回写对账时间 |\n| 非羚牛 | — | 不参与核对/对账展示与锁定 |\n\nH5 列表主要展示核对状态(仅羚牛);对账细节见联动规格。\n\n---\n\n## 4. 代码路径\n\n| 能力 | 路径 |\n|---|---|\n| Bridge | `src/common/h2VehicleLedgerBridge.js` |\n| 门禁工具 | `utils/manual-ledger.ts` → `isManualLedgerGateBlocked` |\n| 订单 CRUD | `utils/orders.ts` |\n| 页面 | `index.tsx` / `ManualLedgerPanel` / `CreateWizard` |\n",
|
||||||
|
"markdownPath": ".spec/key-logic.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "markdown",
|
||||||
|
"id": "h5-doc-logic-manual",
|
||||||
|
"title": "手工台账门禁",
|
||||||
|
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||||
|
"markdownPath": ".spec/feature-manual-ledger.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "markdown",
|
||||||
|
"id": "h5-doc-logic-reconcile",
|
||||||
|
"title": "核对与对账联动",
|
||||||
|
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||||
|
"markdownPath": ".spec/reconcile-linkage.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "markdown",
|
||||||
|
"id": "h5-doc-logic-fleet",
|
||||||
|
"title": "羚牛车辆标签",
|
||||||
|
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||||
|
"markdownPath": ".spec/fleet-plate-tag.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "folder",
|
||||||
|
"id": "h5-doc-flows",
|
||||||
|
"title": "流程图",
|
||||||
|
"defaultExpanded": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "markdown",
|
||||||
|
"id": "h5-doc-flows-md",
|
||||||
|
"title": "操作流程图",
|
||||||
|
"markdown": "# 流程图\n\n> 标注面板支持 Mermaid 渲染。下列流程与当前原型行为一致(本地 Store,未接真实 API)。\n\n---\n\n## 1. 主路径:从打开到上报\n\n```mermaid\nflowchart TD\n A[打开加氢订单 H5] --> B[查看本站列表与 KPI]\n B --> C{前一日手工台账已上传?}\n C -->|否| D[提示:手工台账有缺失…]\n D --> E[进入手工台账 · 选中昨日]\n E --> F[拍照/相册上传并确认]\n F --> C\n C -->|是| G[点新增]\n G --> H[填写时间/车牌/面板/量价]\n H --> I{同站同车牌同时间已存在?}\n I -->|是| J[拦截提示]\n J --> H\n I -->|否| K[保存写入 Bridge]\n K --> B\n```\n\n---\n\n## 2. 手工台账补传\n\n```mermaid\nflowchart LR\n A[手工台账 Tab] --> B[月历:绿已传 / 橙未传]\n B --> C[点选 ≤今日 且 ≥首笔日]\n C --> D[上传图片]\n D --> E[二次确认]\n E --> F[归档不可删 · 可追加]\n```\n\n---\n\n## 3. 编辑 / 删除与锁定\n\n```mermaid\nflowchart TD\n A[打开详情] --> L{羚牛车辆?}\n L -->|否| D[可编辑 / 可删除]\n L -->|是| B{已核对或已对账?}\n B -->|是| C[只读 · 禁改删]\n B -->|否| D\n D --> E[写回 Bridge]\n```\n\n---\n\n## 4. 与 Web / 台账联动(概念)\n\n```mermaid\nsequenceDiagram\n participant H5 as 加氢订单 H5\n participant BR as Bridge Store\n participant WEB as 加氢记录 Web\n participant LED as 车辆氢费明细\n participant SITE as 站点对账单\n H5->>BR: 新增/改删流水 · 上传台账\n WEB->>BR: 同 Store 读写 · 导出\n LED->>BR: 完成核对 → 已核对\n SITE->>BR: 提交对账单 → 已对账\n```\n",
|
||||||
|
"markdownPath": ".spec/flows.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "folder",
|
"type": "folder",
|
||||||
"id": "h5-doc-features",
|
"id": "h5-doc-features",
|
||||||
@@ -403,28 +484,28 @@
|
|||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"id": "h5-doc-manual",
|
"id": "h5-doc-manual",
|
||||||
"title": "手工台账上传",
|
"title": "手工台账上传",
|
||||||
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API) |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示条 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历) \n\n## 验收\n\n1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n",
|
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||||
"markdownPath": ".spec/feature-manual-ledger.md"
|
"markdownPath": ".spec/feature-manual-ledger.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"id": "h5-doc-detail",
|
"id": "h5-doc-detail",
|
||||||
"title": "详情编辑删除",
|
"title": "详情编辑删除",
|
||||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||||
"markdownPath": ".spec/feature-detail.md"
|
"markdownPath": ".spec/feature-detail.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"id": "h5-doc-fleet",
|
"id": "h5-doc-fleet",
|
||||||
"title": "羚牛车辆标签",
|
"title": "羚牛车辆标签",
|
||||||
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n",
|
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||||
"markdownPath": ".spec/fleet-plate-tag.md"
|
"markdownPath": ".spec/fleet-plate-tag.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"id": "h5-doc-reconcile",
|
"id": "h5-doc-reconcile",
|
||||||
"title": "核对与对账联动",
|
"title": "核对与对账联动",
|
||||||
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」,禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」\n```\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 未核对 | 可编辑、删除 |\n| 已核对未对账 | 只读;提示能源部已核对 |\n| 已对账 | 只读;提示已纳入对账单 |\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||||
"markdownPath": ".spec/reconcile-linkage.md"
|
"markdownPath": ".spec/reconcile-linkage.md"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -433,7 +514,7 @@
|
|||||||
"type": "folder",
|
"type": "folder",
|
||||||
"id": "h5-doc-pages",
|
"id": "h5-doc-pages",
|
||||||
"title": "按页面",
|
"title": "按页面",
|
||||||
"defaultExpanded": true,
|
"defaultExpanded": false,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "folder",
|
"type": "folder",
|
||||||
@@ -500,7 +581,7 @@
|
|||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"id": "h5-page-detail-md",
|
"id": "h5-page-detail-md",
|
||||||
"title": "详情页要点",
|
"title": "详情页要点",
|
||||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n"
|
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user