Преглед изворни кода

chore: validation passed for CapsuleSafeArea integration

xuyunhui пре 1 месец
родитељ
комит
188439665d
100 измењених фајлова са 4991 додато и 4171 уклоњено
  1. 155 0
      .claude/commands/opsx/apply.md
  2. 160 0
      .claude/commands/opsx/archive.md
  3. 172 0
      .claude/commands/opsx/explore.md
  4. 107 0
      .claude/commands/opsx/propose.md
  5. 143 0
      .claude/commands/opsx/sync.md
  6. 159 0
      .claude/skills/openspec-apply-change/SKILL.md
  7. 117 0
      .claude/skills/openspec-archive-change/SKILL.md
  8. 287 0
      .claude/skills/openspec-explore/SKILL.md
  9. 111 0
      .claude/skills/openspec-propose/SKILL.md
  10. 147 0
      .claude/skills/openspec-sync-specs/SKILL.md
  11. 83 0
      docs/custom-tabbar-plan.md
  12. 265 0
      docs/role-prototype-diff.md
  13. 194 0
      docs/style-redesign.md
  14. 46 0
      docs/superpowers/reports/2026-06-24-fix-missing-custom-tabbar-verify.md
  15. 147 0
      docs/superpowers/specs/2026-06-24-weapp-capsule-safearea-design.md
  16. 7 6
      openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/.comet.yaml
  17. 0 0
      openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/.openspec.yaml
  18. 0 0
      openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/design.md
  19. 0 0
      openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/proposal.md
  20. 1 1
      openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/tasks.md
  21. 22 0
      openspec/changes/weapp-capsule-safearea/.comet.yaml
  22. 34 0
      openspec/changes/weapp-capsule-safearea/.comet/handoff/brainstorm-summary.md
  23. 14 0
      openspec/changes/weapp-capsule-safearea/.comet/handoff/design-context.json
  24. 210 0
      openspec/changes/weapp-capsule-safearea/.comet/handoff/design-context.md
  25. 6 4
      openspec/changes/weapp-capsule-safearea/.comet/subagent-progress.md
  26. 2 0
      openspec/changes/weapp-capsule-safearea/.openspec.yaml
  27. 66 0
      openspec/changes/weapp-capsule-safearea/design.md
  28. 30 0
      openspec/changes/weapp-capsule-safearea/proposal.md
  29. 51 0
      openspec/changes/weapp-capsule-safearea/specs/weapp-capsule-safearea/spec.md
  30. 625 216
      package-lock.json
  31. 7 4
      package.json
  32. 36 0
      scripts/check-wxss-escapes.js
  33. 61 0
      scripts/generate-tabbar-icons.py
  34. 25 0
      scripts/rename-subpackages-back.py
  35. 42 0
      scripts/rename-subpackages.py
  36. 128 0
      scripts/update-paths.py
  37. 9 0
      src/api/auth.ts
  38. 2 0
      src/api/project.ts
  39. 0 69
      src/components/common/BottomNav.vue
  40. 69 0
      src/components/common/CapsuleSafeArea.test.ts
  41. 2 2
      src/components/common/ChartView.vue
  42. 5 5
      src/components/common/FilterTabs.vue
  43. 2 2
      src/components/common/Modal.vue
  44. 18 0
      src/components/common/StatusBar.vue
  45. 57 0
      src/components/dispatch/ScheduleView.test.ts
  46. 50 0
      src/components/home/ConstructionHome.test.ts.bak
  47. 0 1
      src/components/index.ts
  48. 136 0
      src/components/my/ProfileView.vue
  49. 1 1
      src/components/project/ProjectCard.vue
  50. 81 0
      src/components/task/ConstructionTaskCard.vue
  51. 115 0
      src/components/task/DispatchTaskCard.vue
  52. 77 0
      src/components/task/SalesTaskCard.vue
  53. 7 17
      src/components/task/TaskCard.vue
  54. 23 8
      src/components/uni-card/uni-card.vue
  55. 21 13
      src/components/uni-list/uni-list-item.vue
  56. 2 2
      src/components/uni-list/uni-list.vue
  57. 25 0
      src/composables/useSystemInfo.ts
  58. 11 1
      src/manifest.json
  59. 106 33
      src/mock/index.ts
  60. 349 362
      src/pages.json
  61. 0 237
      src/pages/common/addProject.vue
  62. 0 137
      src/pages/common/profile.vue
  63. 0 136
      src/pages/common/projectDetail.vue
  64. 0 265
      src/pages/common/taskDetail.vue
  65. 0 163
      src/pages/common/taskList.vue
  66. 0 86
      src/pages/construction/basicInfo.vue
  67. 0 85
      src/pages/construction/changePassword.vue
  68. 0 143
      src/pages/construction/dailyTask.vue
  69. 0 139
      src/pages/construction/helpCenter.vue
  70. 0 199
      src/pages/construction/home.vue
  71. 0 150
      src/pages/construction/myPage.vue
  72. 0 78
      src/pages/construction/taskList.vue
  73. 0 138
      src/pages/dispatch/availableVehicles.vue
  74. 0 59
      src/pages/dispatch/basicInfo.vue
  75. 0 120
      src/pages/dispatch/changePassword.vue
  76. 0 66
      src/pages/dispatch/emergencyStandby.vue
  77. 0 76
      src/pages/dispatch/helpCenter.vue
  78. 0 177
      src/pages/dispatch/home.vue
  79. 0 120
      src/pages/dispatch/myPage.vue
  80. 0 93
      src/pages/dispatch/taskList.vue
  81. 0 133
      src/pages/dispatch/vehicleList.vue
  82. 18 0
      src/pages/home/home.vue
  83. 1 6
      src/pages/index/index.vue
  84. 52 73
      src/pages/login/login.vue
  85. 9 0
      src/pages/my/my.vue
  86. 0 83
      src/pages/sales/basicInfo.vue
  87. 0 120
      src/pages/sales/changePassword.vue
  88. 0 124
      src/pages/sales/helpCenter.vue
  89. 0 210
      src/pages/sales/home.vue
  90. 7 0
      src/pages/schedule/schedule.vue
  91. 32 0
      src/pages/task/task.vue
  92. BIN
      src/static/tabbar/home-active.png
  93. BIN
      src/static/tabbar/home.png
  94. BIN
      src/static/tabbar/my-active.png
  95. BIN
      src/static/tabbar/my.png
  96. BIN
      src/static/tabbar/task-active.png
  97. BIN
      src/static/tabbar/task.png
  98. 14 5
      src/stores/auth.ts
  99. 8 1
      src/stores/project.ts
  100. 22 2
      src/stores/task.ts

+ 155 - 0
.claude/commands/opsx/apply.md

@@ -0,0 +1,155 @@
+---
+name: "OPSX: Apply"
+description: Implement tasks from an OpenSpec change (Experimental)
+category: Workflow
+tags: [workflow, artifacts, experimental]
+---
+
+Implement tasks from an OpenSpec change.
+
+**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **Select the change**
+
+   If a name is provided, use it. Otherwise:
+   - Infer from conversation context if the user mentioned a change
+   - Auto-select if only one active change exists
+   - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
+
+   Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
+
+2. **Check status to understand the schema**
+   ```bash
+   openspec status --change "<name>" --json
+   ```
+   Parse the JSON to understand:
+   - `schemaName`: The workflow being used (e.g., "spec-driven")
+   - `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints
+   - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
+
+3. **Get apply instructions**
+
+   ```bash
+   openspec instructions apply --change "<name>" --json
+   ```
+
+   This returns:
+   - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
+   - Progress (total, complete, remaining)
+   - Task list with status
+   - Dynamic instruction based on current state
+
+   **Handle states:**
+   - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
+   - If `state: "all_done"`: congratulate, suggest archive
+   - Otherwise: proceed to implementation
+
+   **Workspace guard:** If status JSON reports `actionContext.mode: "workspace-planning"` and `allowedEditRoots` is empty, explain that full workspace apply is not supported in this slice. Treat linked repos and folders as read-only context, ask the user to select an affected area through an explicit implementation workflow, and STOP before editing files.
+
+4. **Read context files**
+
+   Read every file path listed under `contextFiles` from the apply instructions output.
+   The files depend on the schema being used:
+   - **spec-driven**: proposal, specs, design, tasks
+   - Other schemas: follow the contextFiles from CLI output
+
+5. **Show current progress**
+
+   Display:
+   - Schema being used
+   - Progress: "N/M tasks complete"
+   - Remaining tasks overview
+   - Dynamic instruction from CLI
+
+6. **Implement tasks (loop until done or blocked)**
+
+   For each pending task:
+   - Show which task is being worked on
+   - Make the code changes required
+   - Keep changes minimal and focused
+   - Mark task complete in the tasks file: `- [ ]` → `- [x]`
+   - Continue to next task
+
+   **Pause if:**
+   - Task is unclear → ask for clarification
+   - Implementation reveals a design issue → suggest updating artifacts
+   - Error or blocker encountered → report and wait for guidance
+   - User interrupts
+
+7. **On completion or pause, show status**
+
+   Display:
+   - Tasks completed this session
+   - Overall progress: "N/M tasks complete"
+   - If all done: suggest archive
+   - If paused: explain why and wait for guidance
+
+**Output During Implementation**
+
+```
+## Implementing: <change-name> (schema: <schema-name>)
+
+Working on task 3/7: <task description>
+[...implementation happening...]
+✓ Task complete
+
+Working on task 4/7: <task description>
+[...implementation happening...]
+✓ Task complete
+```
+
+**Output On Completion**
+
+```
+## Implementation Complete
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Progress:** 7/7 tasks complete ✓
+
+### Completed This Session
+- [x] Task 1
+- [x] Task 2
+...
+
+All tasks complete! You can archive this change with `/opsx:archive`.
+```
+
+**Output On Pause (Issue Encountered)**
+
+```
+## Implementation Paused
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Progress:** 4/7 tasks complete
+
+### Issue Encountered
+<description of the issue>
+
+**Options:**
+1. <option 1>
+2. <option 2>
+3. Other approach
+
+What would you like to do?
+```
+
+**Guardrails**
+- Keep going through tasks until done or blocked
+- Always read context files before starting (from the apply instructions output)
+- If task is ambiguous, pause and ask before implementing
+- If implementation reveals issues, pause and suggest artifact updates
+- Keep code changes minimal and scoped to each task
+- Update task checkbox immediately after completing each task
+- Pause on errors, blockers, or unclear requirements - don't guess
+- Use contextFiles from CLI output, don't assume specific file names
+
+**Fluid Workflow Integration**
+
+This skill supports the "actions on a change" model:
+
+- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
+- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

+ 160 - 0
.claude/commands/opsx/archive.md

@@ -0,0 +1,160 @@
+---
+name: "OPSX: Archive"
+description: Archive a completed change in the experimental workflow
+category: Workflow
+tags: [workflow, archive, experimental]
+---
+
+Archive a completed change in the experimental workflow.
+
+**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+
+   Show only active changes (not already archived).
+   Include the schema used for each change if available.
+
+   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Check artifact completion status**
+
+   Run `openspec status --change "<name>" --json` to check artifact completion.
+
+   Parse the JSON to understand:
+   - `schemaName`: The workflow being used
+   - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context
+   - `artifacts`: List of artifacts with their status (`done` or other)
+
+   If status reports `actionContext.mode: "workspace-planning"`, explain that workspace archive is not supported in this slice and STOP. Do not move workspace changes into repo-local archives or edit linked repos.
+
+   **If any artifacts are not `done`:**
+   - Display warning listing incomplete artifacts
+   - Prompt user for confirmation to continue
+   - Proceed if user confirms
+
+3. **Check task completion status**
+
+   Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
+
+   Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
+
+   **If incomplete tasks found:**
+   - Display warning showing count of incomplete tasks
+   - Prompt user for confirmation to continue
+   - Proceed if user confirms
+
+   **If no tasks file exists:** Proceed without task-related warning.
+
+4. **Assess delta spec sync state**
+
+   Use `artifactPaths.specs.existingOutputPaths` from status JSON to check for delta specs. If none exist, proceed without sync prompt.
+
+   **If delta specs exist:**
+   - Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
+   - Determine what changes would be applied (adds, modifications, removals, renames)
+   - Show a combined summary before prompting
+
+   **Prompt options:**
+   - If changes needed: "Sync now (recommended)", "Archive without syncing"
+   - If already synced: "Archive now", "Sync anyway", "Cancel"
+
+   If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
+
+5. **Perform the archive**
+
+   Create an `archive` directory under `planningHome.changesDir` if it doesn't exist:
+   ```bash
+   mkdir -p "<planningHome.changesDir>/archive"
+   ```
+
+   Generate target name using current date: `YYYY-MM-DD-<change-name>`
+
+   **Check if target already exists:**
+   - If yes: Fail with error, suggest renaming existing archive or using different date
+   - If no: Move `changeRoot` to the archive directory
+
+   ```bash
+   mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
+   ```
+
+6. **Display summary**
+
+   Show archive completion summary including:
+   - Change name
+   - Schema that was used
+   - Archive location
+   - Spec sync status (synced / sync skipped / no delta specs)
+   - Note about any warnings (incomplete artifacts/tasks)
+
+**Output On Success**
+
+```
+## Archive Complete
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-<name>/
+**Specs:** ✓ Synced to main specs
+
+All artifacts complete. All tasks complete.
+```
+
+**Output On Success (No Delta Specs)**
+
+```
+## Archive Complete
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-<name>/
+**Specs:** No delta specs
+
+All artifacts complete. All tasks complete.
+```
+
+**Output On Success With Warnings**
+
+```
+## Archive Complete (with warnings)
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-<name>/
+**Specs:** Sync skipped (user chose to skip)
+
+**Warnings:**
+- Archived with 2 incomplete artifacts
+- Archived with 3 incomplete tasks
+- Delta spec sync was skipped (user chose to skip)
+
+Review the archive if this was not intentional.
+```
+
+**Output On Error (Archive Exists)**
+
+```
+## Archive Failed
+
+**Change:** <change-name>
+**Target:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-<name>/
+
+Target archive directory already exists.
+
+**Options:**
+1. Rename the existing archive
+2. Delete the existing archive if it's a duplicate
+3. Wait until a different date to archive
+```
+
+**Guardrails**
+- Always prompt for change selection if not provided
+- Use artifact graph (openspec status --json) for completion checking
+- Don't block archive on warnings - just inform and confirm
+- Preserve .openspec.yaml when moving to archive (it moves with the directory)
+- Show clear summary of what happened
+- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
+- If delta specs exist, always run the sync assessment and show the combined summary before prompting

+ 172 - 0
.claude/commands/opsx/explore.md

@@ -0,0 +1,172 @@
+---
+name: "OPSX: Explore"
+description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
+category: Workflow
+tags: [workflow, explore, experimental, thinking]
+---
+
+Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
+
+**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
+
+**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
+
+**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
+- A vague idea: "real-time collaboration"
+- A specific problem: "the auth system is getting unwieldy"
+- A change name: "add-dark-mode" (to explore in context of that change)
+- A comparison: "postgres vs sqlite for this"
+- Nothing (just enter explore mode)
+
+---
+
+## The Stance
+
+- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
+- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
+- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
+- **Adaptive** - Follow interesting threads, pivot when new information emerges
+- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
+- **Grounded** - Explore the actual codebase when relevant, don't just theorize
+
+---
+
+## What You Might Do
+
+Depending on what the user brings, you might:
+
+**Explore the problem space**
+- Ask clarifying questions that emerge from what they said
+- Challenge assumptions
+- Reframe the problem
+- Find analogies
+
+**Investigate the codebase**
+- Map existing architecture relevant to the discussion
+- Find integration points
+- Identify patterns already in use
+- Surface hidden complexity
+
+**Compare options**
+- Brainstorm multiple approaches
+- Build comparison tables
+- Sketch tradeoffs
+- Recommend a path (if asked)
+
+**Visualize**
+```
+┌─────────────────────────────────────────┐
+│     Use ASCII diagrams liberally        │
+├─────────────────────────────────────────┤
+│                                         │
+│      ┌────────┐         ┌────────┐      │
+│      │ State  │────────▶│ State  │      │
+│      │   A    │         │   B    │      │
+│      └────────┘         └────────┘      │
+│                                         │
+│   System diagrams, state machines,      │
+│   data flows, architecture sketches,    │
+│   dependency graphs, comparison tables  │
+│                                         │
+└─────────────────────────────────────────┘
+```
+
+**Surface risks and unknowns**
+- Identify what could go wrong
+- Find gaps in understanding
+- Suggest spikes or investigations
+
+---
+
+## OpenSpec Awareness
+
+You have full context of the OpenSpec system. Use it naturally, don't force it.
+
+### Check for context
+
+At the start, quickly check what exists:
+```bash
+openspec list --json
+```
+
+This tells you:
+- If there are active changes
+- Their names, schemas, and status
+- What the user might be working on
+
+If the user mentioned a specific change name, read its artifacts for context.
+
+### When no change exists
+
+Think freely. When insights crystallize, you might offer:
+
+- "This feels solid enough to start a change. Want me to create a proposal?"
+- Or keep exploring - no pressure to formalize
+
+### When a change exists
+
+If the user mentions a change or you detect one is relevant:
+
+1. **Resolve and read existing artifacts for context**
+   - Run `openspec status --change "<name>" --json`.
+   - Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON.
+   - Read existing files from `artifactPaths.<artifact>.existingOutputPaths`.
+
+2. **Reference them naturally in conversation**
+   - "Your design mentions using Redis, but we just realized SQLite fits better..."
+   - "The proposal scopes this to premium users, but we're now thinking everyone..."
+
+3. **Offer to capture when decisions are made**
+
+    | Insight Type               | Where to Capture               |
+    |----------------------------|--------------------------------|
+    | New requirement discovered | `specs/<capability>/spec.md` |
+    | Requirement changed        | `specs/<capability>/spec.md` |
+    | Design decision made       | `design.md`                  |
+    | Scope changed              | `proposal.md`                |
+    | New work identified        | `tasks.md`                   |
+    | Assumption invalidated     | Relevant artifact              |
+
+   Example offers:
+   - "That's a design decision. Capture it in design.md?"
+   - "This is a new requirement. Add it to specs?"
+   - "This changes scope. Update the proposal?"
+
+4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
+
+---
+
+## What You Don't Have To Do
+
+- Follow a script
+- Ask the same questions every time
+- Produce a specific artifact
+- Reach a conclusion
+- Stay on topic if a tangent is valuable
+- Be brief (this is thinking time)
+
+---
+
+## Ending Discovery
+
+There's no required ending. Discovery might:
+
+- **Flow into a proposal**: "Ready to start? I can create a change proposal."
+- **Result in artifact updates**: "Updated design.md with these decisions"
+- **Just provide clarity**: User has what they need, moves on
+- **Continue later**: "We can pick this up anytime"
+
+When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
+
+---
+
+## Guardrails
+
+- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
+- **Don't fake understanding** - If something is unclear, dig deeper
+- **Don't rush** - Discovery is thinking time, not task time
+- **Don't force structure** - Let patterns emerge naturally
+- **Don't auto-capture** - Offer to save insights, don't just do it
+- **Do visualize** - A good diagram is worth many paragraphs
+- **Do explore the codebase** - Ground discussions in reality
+- **Do question assumptions** - Including the user's and your own

+ 107 - 0
.claude/commands/opsx/propose.md

@@ -0,0 +1,107 @@
+---
+name: "OPSX: Propose"
+description: Propose a new change - create it and generate all artifacts in one step
+category: Workflow
+tags: [workflow, artifacts, experimental]
+---
+
+Propose a new change - create the change and generate all artifacts in one step.
+
+I'll create a change with artifacts:
+- proposal.md (what & why)
+- design.md (how)
+- tasks.md (implementation steps)
+
+When ready to implement, run /opsx:apply
+
+---
+
+**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
+
+**Steps**
+
+1. **If no input provided, ask what they want to build**
+
+   Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
+   > "What change do you want to work on? Describe what you want to build or fix."
+
+   From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
+
+   **IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
+
+2. **Create the change directory**
+   ```bash
+   openspec new change "<name>"
+   ```
+   This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`.
+
+3. **Get the artifact build order**
+   ```bash
+   openspec status --change "<name>" --json
+   ```
+   Parse the JSON to get:
+   - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
+   - `artifacts`: list of all artifacts with their status and dependencies
+   - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
+
+4. **Create artifacts in sequence until apply-ready**
+
+   Use the **TodoWrite tool** to track progress through the artifacts.
+
+   Loop through artifacts in dependency order (artifacts with no pending dependencies first):
+
+   a. **For each artifact that is `ready` (dependencies satisfied)**:
+      - Get instructions:
+        ```bash
+        openspec instructions <artifact-id> --change "<name>" --json
+        ```
+      - The instructions JSON includes:
+        - `context`: Project background (constraints for you - do NOT include in output)
+        - `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
+        - `template`: The structure to use for your output file
+        - `instruction`: Schema-specific guidance for this artifact type
+        - `resolvedOutputPath`: Resolved path or pattern to write the artifact
+        - `dependencies`: Completed artifacts to read for context
+      - Read any completed dependency files for context
+      - Create the artifact file using `template` as the structure and write it to `resolvedOutputPath`
+      - Apply `context` and `rules` as constraints - but do NOT copy them into the file
+      - Show brief progress: "Created <artifact-id>"
+
+   b. **Continue until all `applyRequires` artifacts are complete**
+      - After creating each artifact, re-run `openspec status --change "<name>" --json`
+      - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
+      - Stop when all `applyRequires` artifacts are done
+
+   c. **If an artifact requires user input** (unclear context):
+      - Use **AskUserQuestion tool** to clarify
+      - Then continue with creation
+
+5. **Show final status**
+   ```bash
+   openspec status --change "<name>"
+   ```
+
+**Output**
+
+After completing all artifacts, summarize:
+- Change name and location
+- List of artifacts created with brief descriptions
+- What's ready: "All artifacts created! Ready for implementation."
+- Prompt: "Run `/opsx:apply` to start implementing."
+
+**Artifact Creation Guidelines**
+
+- Follow the `instruction` field from `openspec instructions` for each artifact type
+- The schema defines what each artifact should contain - follow it
+- Read dependency artifacts for context before creating new ones
+- Use `template` as the structure for your output file - fill in its sections
+- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
+  - Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
+  - These guide what you write, but should never appear in the output
+
+**Guardrails**
+- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
+- Always read dependency artifacts before creating a new one
+- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
+- If a change with that name already exists, ask if user wants to continue it or create a new one
+- Verify each artifact file exists after writing before proceeding to next

+ 143 - 0
.claude/commands/opsx/sync.md

@@ -0,0 +1,143 @@
+---
+name: "OPSX: Sync"
+description: Sync delta specs from a change to main specs
+category: Workflow
+tags: [workflow, specs, experimental]
+---
+
+Sync delta specs from a change to main specs.
+
+This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
+
+**Input**: Optionally specify a change name after `/opsx:sync` (e.g., `/opsx:sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+
+   Show changes that have delta specs (under `specs/` directory).
+
+   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Resolve change context**
+
+   Run:
+   ```bash
+   openspec status --change "<name>" --json
+   ```
+
+   If status reports `actionContext.mode: "workspace-planning"`, explain that workspace spec sync is not supported in this slice and STOP. Do not fall back to repo-local paths or edit linked repos.
+
+3. **Find delta specs**
+
+   Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the list of delta spec files.
+
+   Each delta spec file contains sections like:
+   - `## ADDED Requirements` - New requirements to add
+   - `## MODIFIED Requirements` - Changes to existing requirements
+   - `## REMOVED Requirements` - Requirements to remove
+   - `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
+
+   If no delta specs found, inform user and stop.
+
+4. **For each delta spec, apply changes to main specs**
+
+   For each repo-local capability delta spec path returned by the CLI:
+
+   a. **Read the delta spec** to understand the intended changes
+
+   b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
+
+   c. **Apply changes intelligently**:
+
+      **ADDED Requirements:**
+      - If requirement doesn't exist in main spec → add it
+      - If requirement already exists → update it to match (treat as implicit MODIFIED)
+
+      **MODIFIED Requirements:**
+      - Find the requirement in main spec
+      - Apply the changes - this can be:
+        - Adding new scenarios (don't need to copy existing ones)
+        - Modifying existing scenarios
+        - Changing the requirement description
+      - Preserve scenarios/content not mentioned in the delta
+
+      **REMOVED Requirements:**
+      - Remove the entire requirement block from main spec
+
+      **RENAMED Requirements:**
+      - Find the FROM requirement, rename to TO
+
+   d. **Create new main spec** if capability doesn't exist yet:
+      - Create `openspec/specs/<capability>/spec.md`
+      - Add Purpose section (can be brief, mark as TBD)
+      - Add Requirements section with the ADDED requirements
+
+5. **Show summary**
+
+   After applying all changes, summarize:
+   - Which capabilities were updated
+   - What changes were made (requirements added/modified/removed/renamed)
+
+**Delta Spec Format Reference**
+
+```markdown
+## ADDED Requirements
+
+### Requirement: New Feature
+The system SHALL do something new.
+
+#### Scenario: Basic case
+- **WHEN** user does X
+- **THEN** system does Y
+
+## MODIFIED Requirements
+
+### Requirement: Existing Feature
+#### Scenario: New scenario to add
+- **WHEN** user does A
+- **THEN** system does B
+
+## REMOVED Requirements
+
+### Requirement: Deprecated Feature
+
+## RENAMED Requirements
+
+- FROM: `### Requirement: Old Name`
+- TO: `### Requirement: New Name`
+```
+
+**Key Principle: Intelligent Merging**
+
+Unlike programmatic merging, you can apply **partial updates**:
+- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
+- The delta represents *intent*, not a wholesale replacement
+- Use your judgment to merge changes sensibly
+
+**Output On Success**
+
+```
+## Specs Synced: <change-name>
+
+Updated main specs:
+
+**<capability-1>**:
+- Added requirement: "New Feature"
+- Modified requirement: "Existing Feature" (added 1 scenario)
+
+**<capability-2>**:
+- Created new spec file
+- Added requirement: "Another Feature"
+
+Main specs are now updated. The change remains active - archive when implementation is complete.
+```
+
+**Guardrails**
+- Read both delta and main specs before making changes
+- Preserve existing content not mentioned in delta
+- If something is unclear, ask for clarification
+- Show what you're changing as you go
+- The operation should be idempotent - running twice should give same result

+ 159 - 0
.claude/skills/openspec-apply-change/SKILL.md

@@ -0,0 +1,159 @@
+---
+name: openspec-apply-change
+description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+  author: openspec
+  version: "1.0"
+  generatedBy: "1.4.1"
+---
+
+Implement tasks from an OpenSpec change.
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **Select the change**
+
+   If a name is provided, use it. Otherwise:
+   - Infer from conversation context if the user mentioned a change
+   - Auto-select if only one active change exists
+   - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
+
+   Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
+
+2. **Check status to understand the schema**
+   ```bash
+   openspec status --change "<name>" --json
+   ```
+   Parse the JSON to understand:
+   - `schemaName`: The workflow being used (e.g., "spec-driven")
+   - `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints
+   - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
+
+3. **Get apply instructions**
+
+   ```bash
+   openspec instructions apply --change "<name>" --json
+   ```
+
+   This returns:
+   - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
+   - Progress (total, complete, remaining)
+   - Task list with status
+   - Dynamic instruction based on current state
+
+   **Handle states:**
+   - If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
+   - If `state: "all_done"`: congratulate, suggest archive
+   - Otherwise: proceed to implementation
+
+   **Workspace guard:** If status JSON reports `actionContext.mode: "workspace-planning"` and `allowedEditRoots` is empty, explain that full workspace apply is not supported in this slice. Treat linked repos and folders as read-only context, ask the user to select an affected area through an explicit implementation workflow, and STOP before editing files.
+
+4. **Read context files**
+
+   Read every file path listed under `contextFiles` from the apply instructions output.
+   The files depend on the schema being used:
+   - **spec-driven**: proposal, specs, design, tasks
+   - Other schemas: follow the contextFiles from CLI output
+
+5. **Show current progress**
+
+   Display:
+   - Schema being used
+   - Progress: "N/M tasks complete"
+   - Remaining tasks overview
+   - Dynamic instruction from CLI
+
+6. **Implement tasks (loop until done or blocked)**
+
+   For each pending task:
+   - Show which task is being worked on
+   - Make the code changes required
+   - Keep changes minimal and focused
+   - Mark task complete in the tasks file: `- [ ]` → `- [x]`
+   - Continue to next task
+
+   **Pause if:**
+   - Task is unclear → ask for clarification
+   - Implementation reveals a design issue → suggest updating artifacts
+   - Error or blocker encountered → report and wait for guidance
+   - User interrupts
+
+7. **On completion or pause, show status**
+
+   Display:
+   - Tasks completed this session
+   - Overall progress: "N/M tasks complete"
+   - If all done: suggest archive
+   - If paused: explain why and wait for guidance
+
+**Output During Implementation**
+
+```
+## Implementing: <change-name> (schema: <schema-name>)
+
+Working on task 3/7: <task description>
+[...implementation happening...]
+✓ Task complete
+
+Working on task 4/7: <task description>
+[...implementation happening...]
+✓ Task complete
+```
+
+**Output On Completion**
+
+```
+## Implementation Complete
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Progress:** 7/7 tasks complete ✓
+
+### Completed This Session
+- [x] Task 1
+- [x] Task 2
+...
+
+All tasks complete! Ready to archive this change.
+```
+
+**Output On Pause (Issue Encountered)**
+
+```
+## Implementation Paused
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Progress:** 4/7 tasks complete
+
+### Issue Encountered
+<description of the issue>
+
+**Options:**
+1. <option 1>
+2. <option 2>
+3. Other approach
+
+What would you like to do?
+```
+
+**Guardrails**
+- Keep going through tasks until done or blocked
+- Always read context files before starting (from the apply instructions output)
+- If task is ambiguous, pause and ask before implementing
+- If implementation reveals issues, pause and suggest artifact updates
+- Keep code changes minimal and scoped to each task
+- Update task checkbox immediately after completing each task
+- Pause on errors, blockers, or unclear requirements - don't guess
+- Use contextFiles from CLI output, don't assume specific file names
+
+**Fluid Workflow Integration**
+
+This skill supports the "actions on a change" model:
+
+- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
+- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

+ 117 - 0
.claude/skills/openspec-archive-change/SKILL.md

@@ -0,0 +1,117 @@
+---
+name: openspec-archive-change
+description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+  author: openspec
+  version: "1.0"
+  generatedBy: "1.4.1"
+---
+
+Archive a completed change in the experimental workflow.
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+
+   Show only active changes (not already archived).
+   Include the schema used for each change if available.
+
+   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Check artifact completion status**
+
+   Run `openspec status --change "<name>" --json` to check artifact completion.
+
+   Parse the JSON to understand:
+   - `schemaName`: The workflow being used
+   - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context
+   - `artifacts`: List of artifacts with their status (`done` or other)
+
+   If status reports `actionContext.mode: "workspace-planning"`, explain that workspace archive is not supported in this slice and STOP. Do not move workspace changes into repo-local archives or edit linked repos.
+
+   **If any artifacts are not `done`:**
+   - Display warning listing incomplete artifacts
+   - Use **AskUserQuestion tool** to confirm user wants to proceed
+   - Proceed if user confirms
+
+3. **Check task completion status**
+
+   Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
+
+   Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
+
+   **If incomplete tasks found:**
+   - Display warning showing count of incomplete tasks
+   - Use **AskUserQuestion tool** to confirm user wants to proceed
+   - Proceed if user confirms
+
+   **If no tasks file exists:** Proceed without task-related warning.
+
+4. **Assess delta spec sync state**
+
+   Use `artifactPaths.specs.existingOutputPaths` from status JSON to check for delta specs. If none exist, proceed without sync prompt.
+
+   **If delta specs exist:**
+   - Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
+   - Determine what changes would be applied (adds, modifications, removals, renames)
+   - Show a combined summary before prompting
+
+   **Prompt options:**
+   - If changes needed: "Sync now (recommended)", "Archive without syncing"
+   - If already synced: "Archive now", "Sync anyway", "Cancel"
+
+   If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
+
+5. **Perform the archive**
+
+   Create an `archive` directory under `planningHome.changesDir` if it doesn't exist:
+   ```bash
+   mkdir -p "<planningHome.changesDir>/archive"
+   ```
+
+   Generate target name using current date: `YYYY-MM-DD-<change-name>`
+
+   **Check if target already exists:**
+   - If yes: Fail with error, suggest renaming existing archive or using different date
+   - If no: Move `changeRoot` to the archive directory
+
+   ```bash
+   mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
+   ```
+
+6. **Display summary**
+
+   Show archive completion summary including:
+   - Change name
+   - Schema that was used
+   - Archive location
+   - Whether specs were synced (if applicable)
+   - Note about any warnings (incomplete artifacts/tasks)
+
+**Output On Success**
+
+```
+## Archive Complete
+
+**Change:** <change-name>
+**Schema:** <schema-name>
+**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-<name>/
+**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
+
+All artifacts complete. All tasks complete.
+```
+
+**Guardrails**
+- Always prompt for change selection if not provided
+- Use artifact graph (openspec status --json) for completion checking
+- Don't block archive on warnings - just inform and confirm
+- Preserve .openspec.yaml when moving to archive (it moves with the directory)
+- Show clear summary of what happened
+- If sync is requested, use openspec-sync-specs approach (agent-driven)
+- If delta specs exist, always run the sync assessment and show the combined summary before prompting

+ 287 - 0
.claude/skills/openspec-explore/SKILL.md

@@ -0,0 +1,287 @@
+---
+name: openspec-explore
+description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+  author: openspec
+  version: "1.0"
+  generatedBy: "1.4.1"
+---
+
+Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
+
+**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
+
+**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
+
+---
+
+## The Stance
+
+- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
+- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
+- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
+- **Adaptive** - Follow interesting threads, pivot when new information emerges
+- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
+- **Grounded** - Explore the actual codebase when relevant, don't just theorize
+
+---
+
+## What You Might Do
+
+Depending on what the user brings, you might:
+
+**Explore the problem space**
+- Ask clarifying questions that emerge from what they said
+- Challenge assumptions
+- Reframe the problem
+- Find analogies
+
+**Investigate the codebase**
+- Map existing architecture relevant to the discussion
+- Find integration points
+- Identify patterns already in use
+- Surface hidden complexity
+
+**Compare options**
+- Brainstorm multiple approaches
+- Build comparison tables
+- Sketch tradeoffs
+- Recommend a path (if asked)
+
+**Visualize**
+```
+┌─────────────────────────────────────────┐
+│     Use ASCII diagrams liberally        │
+├─────────────────────────────────────────┤
+│                                         │
+│      ┌────────┐         ┌────────┐      │
+│      │ State  │────────▶│ State  │      │
+│      │   A    │         │   B    │      │
+│      └────────┘         └────────┘      │
+│                                         │
+│   System diagrams, state machines,      │
+│   data flows, architecture sketches,    │
+│   dependency graphs, comparison tables  │
+│                                         │
+└─────────────────────────────────────────┘
+```
+
+**Surface risks and unknowns**
+- Identify what could go wrong
+- Find gaps in understanding
+- Suggest spikes or investigations
+
+---
+
+## OpenSpec Awareness
+
+You have full context of the OpenSpec system. Use it naturally, don't force it.
+
+### Check for context
+
+At the start, quickly check what exists:
+```bash
+openspec list --json
+```
+
+This tells you:
+- If there are active changes
+- Their names, schemas, and status
+- What the user might be working on
+
+### When no change exists
+
+Think freely. When insights crystallize, you might offer:
+
+- "This feels solid enough to start a change. Want me to create a proposal?"
+- Or keep exploring - no pressure to formalize
+
+### When a change exists
+
+If the user mentions a change or you detect one is relevant:
+
+1. **Resolve and read existing artifacts for context**
+   - Run `openspec status --change "<name>" --json`.
+   - Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON.
+   - Read existing files from `artifactPaths.<artifact>.existingOutputPaths`.
+
+2. **Reference them naturally in conversation**
+   - "Your design mentions using Redis, but we just realized SQLite fits better..."
+   - "The proposal scopes this to premium users, but we're now thinking everyone..."
+
+3. **Offer to capture when decisions are made**
+
+    | Insight Type               | Where to Capture               |
+    |----------------------------|--------------------------------|
+    | New requirement discovered | `specs/<capability>/spec.md` |
+    | Requirement changed        | `specs/<capability>/spec.md` |
+    | Design decision made       | `design.md`                  |
+    | Scope changed              | `proposal.md`                |
+    | New work identified        | `tasks.md`                   |
+    | Assumption invalidated     | Relevant artifact              |
+
+   Example offers:
+   - "That's a design decision. Capture it in design.md?"
+   - "This is a new requirement. Add it to specs?"
+   - "This changes scope. Update the proposal?"
+
+4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
+
+---
+
+## What You Don't Have To Do
+
+- Follow a script
+- Ask the same questions every time
+- Produce a specific artifact
+- Reach a conclusion
+- Stay on topic if a tangent is valuable
+- Be brief (this is thinking time)
+
+---
+
+## Handling Different Entry Points
+
+**User brings a vague idea:**
+```
+User: I'm thinking about adding real-time collaboration
+
+You: Real-time collab is a big space. Let me think about this...
+
+      COLLABORATION SPECTRUM
+      ════════════════════════════════════════════
+
+      Awareness          Coordination         Sync
+          │                   │                 │
+          ▼                   ▼                 ▼
+      ┌────────┐         ┌────────┐        ┌────────┐
+      │Presence│         │Cursors │        │  CRDT  │
+      │  "3    │         │ Multi  │        │Conflict│
+      │online" │         │ select │        │  free  │
+      └────────┘         └────────┘        └────────┘
+          │                   │                 │
+       trivial            moderate           complex
+
+      Where's your head at?
+```
+
+**User brings a specific problem:**
+```
+User: The auth system is a mess
+
+You: [reads codebase]
+
+     ┌─────────────────────────────────────────────┐
+     │              CURRENT AUTH FLOW              │
+     └─────────────────────────────────────────────┘
+                        │
+          ┌─────────────┼─────────────┐
+          ▼             ▼             ▼
+     ┌─────────┐  ┌─────────┐  ┌─────────┐
+     │ Google  │  │  GitHub │  │  Email  │
+     │  OAuth  │  │  OAuth  │  │  Magic  │
+     └────┬────┘  └────┬────┘  └────┬────┘
+          │             │             │
+          └─────────────┼─────────────┘
+                        ▼
+                  ┌───────────┐
+                  │  Session  │
+                  └─────┬─────┘
+                        │
+                        ▼
+                  ┌───────────┐
+                  │   Perms   │
+                  └───────────┘
+
+     I see three tangles. Which one's burning?
+```
+
+**User is stuck mid-implementation:**
+```
+User: /opsx:explore add-auth-system
+      The OAuth integration is more complex than expected
+
+You: [reads change artifacts]
+
+     You're on task 4: "Implement OAuth flow"
+
+     Let me trace what's involved...
+
+     [draws diagram, explores options, suggests paths]
+
+     Want to update the design to reflect this?
+     Or add a spike task to investigate?
+```
+
+**User wants to compare options:**
+```
+User: Should we use Postgres or SQLite?
+
+You: Generic answer is boring. What's the context?
+
+User: A CLI tool that tracks local dev environments
+
+You: That changes everything.
+
+     ┌─────────────────────────────────────────────────┐
+     │          CLI TOOL DATA STORAGE                  │
+     └─────────────────────────────────────────────────┘
+
+     Key constraints:
+     • No daemon running
+     • Must work offline
+     • Single user
+
+                  SQLite          Postgres
+     Deployment   embedded ✓      needs server ✗
+     Offline      yes ✓           no ✗
+     Single file  yes ✓           no ✗
+
+     SQLite. Not even close.
+
+     Unless... is there a sync component?
+```
+
+---
+
+## Ending Discovery
+
+There's no required ending. Discovery might:
+
+- **Flow into a proposal**: "Ready to start? I can create a change proposal."
+- **Result in artifact updates**: "Updated design.md with these decisions"
+- **Just provide clarity**: User has what they need, moves on
+- **Continue later**: "We can pick this up anytime"
+
+When it feels like things are crystallizing, you might summarize:
+
+```
+## What We Figured Out
+
+**The problem**: [crystallized understanding]
+
+**The approach**: [if one emerged]
+
+**Open questions**: [if any remain]
+
+**Next steps** (if ready):
+- Create a change proposal
+- Keep exploring: just keep talking
+```
+
+But this summary is optional. Sometimes the thinking IS the value.
+
+---
+
+## Guardrails
+
+- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
+- **Don't fake understanding** - If something is unclear, dig deeper
+- **Don't rush** - Discovery is thinking time, not task time
+- **Don't force structure** - Let patterns emerge naturally
+- **Don't auto-capture** - Offer to save insights, don't just do it
+- **Do visualize** - A good diagram is worth many paragraphs
+- **Do explore the codebase** - Ground discussions in reality
+- **Do question assumptions** - Including the user's and your own

+ 111 - 0
.claude/skills/openspec-propose/SKILL.md

@@ -0,0 +1,111 @@
+---
+name: openspec-propose
+description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+  author: openspec
+  version: "1.0"
+  generatedBy: "1.4.1"
+---
+
+Propose a new change - create the change and generate all artifacts in one step.
+
+I'll create a change with artifacts:
+- proposal.md (what & why)
+- design.md (how)
+- tasks.md (implementation steps)
+
+When ready to implement, run /opsx:apply
+
+---
+
+**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
+
+**Steps**
+
+1. **If no clear input provided, ask what they want to build**
+
+   Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
+   > "What change do you want to work on? Describe what you want to build or fix."
+
+   From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
+
+   **IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
+
+2. **Create the change directory**
+   ```bash
+   openspec new change "<name>"
+   ```
+   This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`.
+
+3. **Get the artifact build order**
+   ```bash
+   openspec status --change "<name>" --json
+   ```
+   Parse the JSON to get:
+   - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
+   - `artifacts`: list of all artifacts with their status and dependencies
+   - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.
+
+4. **Create artifacts in sequence until apply-ready**
+
+   Use the **TodoWrite tool** to track progress through the artifacts.
+
+   Loop through artifacts in dependency order (artifacts with no pending dependencies first):
+
+   a. **For each artifact that is `ready` (dependencies satisfied)**:
+      - Get instructions:
+        ```bash
+        openspec instructions <artifact-id> --change "<name>" --json
+        ```
+      - The instructions JSON includes:
+        - `context`: Project background (constraints for you - do NOT include in output)
+        - `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
+        - `template`: The structure to use for your output file
+        - `instruction`: Schema-specific guidance for this artifact type
+        - `resolvedOutputPath`: Resolved path or pattern to write the artifact
+        - `dependencies`: Completed artifacts to read for context
+      - Read any completed dependency files for context
+      - Create the artifact file using `template` as the structure and write it to `resolvedOutputPath`
+      - Apply `context` and `rules` as constraints - but do NOT copy them into the file
+      - Show brief progress: "Created <artifact-id>"
+
+   b. **Continue until all `applyRequires` artifacts are complete**
+      - After creating each artifact, re-run `openspec status --change "<name>" --json`
+      - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
+      - Stop when all `applyRequires` artifacts are done
+
+   c. **If an artifact requires user input** (unclear context):
+      - Use **AskUserQuestion tool** to clarify
+      - Then continue with creation
+
+5. **Show final status**
+   ```bash
+   openspec status --change "<name>"
+   ```
+
+**Output**
+
+After completing all artifacts, summarize:
+- Change name and location
+- List of artifacts created with brief descriptions
+- What's ready: "All artifacts created! Ready for implementation."
+- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
+
+**Artifact Creation Guidelines**
+
+- Follow the `instruction` field from `openspec instructions` for each artifact type
+- The schema defines what each artifact should contain - follow it
+- Read dependency artifacts for context before creating new ones
+- Use `template` as the structure for your output file - fill in its sections
+- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
+  - Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
+  - These guide what you write, but should never appear in the output
+
+**Guardrails**
+- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
+- Always read dependency artifacts before creating a new one
+- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
+- If a change with that name already exists, ask if user wants to continue it or create a new one
+- Verify each artifact file exists after writing before proceeding to next

+ 147 - 0
.claude/skills/openspec-sync-specs/SKILL.md

@@ -0,0 +1,147 @@
+---
+name: openspec-sync-specs
+description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+  author: openspec
+  version: "1.0"
+  generatedBy: "1.4.1"
+---
+
+Sync delta specs from a change to main specs.
+
+This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+
+   Show changes that have delta specs (under `specs/` directory).
+
+   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Resolve change context**
+
+   Run:
+   ```bash
+   openspec status --change "<name>" --json
+   ```
+
+   If status reports `actionContext.mode: "workspace-planning"`, explain that workspace spec sync is not supported in this slice and STOP. Do not fall back to repo-local paths or edit linked repos.
+
+3. **Find delta specs**
+
+   Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the list of delta spec files.
+
+   Each delta spec file contains sections like:
+   - `## ADDED Requirements` - New requirements to add
+   - `## MODIFIED Requirements` - Changes to existing requirements
+   - `## REMOVED Requirements` - Requirements to remove
+   - `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
+
+   If no delta specs found, inform user and stop.
+
+4. **For each delta spec, apply changes to main specs**
+
+   For each repo-local capability delta spec path returned by the CLI:
+
+   a. **Read the delta spec** to understand the intended changes
+
+   b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
+
+   c. **Apply changes intelligently**:
+
+      **ADDED Requirements:**
+      - If requirement doesn't exist in main spec → add it
+      - If requirement already exists → update it to match (treat as implicit MODIFIED)
+
+      **MODIFIED Requirements:**
+      - Find the requirement in main spec
+      - Apply the changes - this can be:
+        - Adding new scenarios (don't need to copy existing ones)
+        - Modifying existing scenarios
+        - Changing the requirement description
+      - Preserve scenarios/content not mentioned in the delta
+
+      **REMOVED Requirements:**
+      - Remove the entire requirement block from main spec
+
+      **RENAMED Requirements:**
+      - Find the FROM requirement, rename to TO
+
+   d. **Create new main spec** if capability doesn't exist yet:
+      - Create `openspec/specs/<capability>/spec.md`
+      - Add Purpose section (can be brief, mark as TBD)
+      - Add Requirements section with the ADDED requirements
+
+5. **Show summary**
+
+   After applying all changes, summarize:
+   - Which capabilities were updated
+   - What changes were made (requirements added/modified/removed/renamed)
+
+**Delta Spec Format Reference**
+
+```markdown
+## ADDED Requirements
+
+### Requirement: New Feature
+The system SHALL do something new.
+
+#### Scenario: Basic case
+- **WHEN** user does X
+- **THEN** system does Y
+
+## MODIFIED Requirements
+
+### Requirement: Existing Feature
+#### Scenario: New scenario to add
+- **WHEN** user does A
+- **THEN** system does B
+
+## REMOVED Requirements
+
+### Requirement: Deprecated Feature
+
+## RENAMED Requirements
+
+- FROM: `### Requirement: Old Name`
+- TO: `### Requirement: New Name`
+```
+
+**Key Principle: Intelligent Merging**
+
+Unlike programmatic merging, you can apply **partial updates**:
+- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
+- The delta represents *intent*, not a wholesale replacement
+- Use your judgment to merge changes sensibly
+
+**Output On Success**
+
+```
+## Specs Synced: <change-name>
+
+Updated main specs:
+
+**<capability-1>**:
+- Added requirement: "New Feature"
+- Modified requirement: "Existing Feature" (added 1 scenario)
+
+**<capability-2>**:
+- Created new spec file
+- Added requirement: "Another Feature"
+
+Main specs are now updated. The change remains active - archive when implementation is complete.
+```
+
+**Guardrails**
+- Read both delta and main specs before making changes
+- Preserve existing content not mentioned in delta
+- If something is unclear, ask for clarification
+- Show what you're changing as you go
+- The operation should be idempotent - running twice should give same result

+ 83 - 0
docs/custom-tabbar-plan.md

@@ -0,0 +1,83 @@
+# 清道夫 App 自定义 tabBar 开发文档
+
+## 目标
+
+实现按角色动态显示的底部 tabBar:
+- **销售端 / 施工端**:3 个 tab(首页、任务、我的)
+- **调度端**:4 个 tab(首页、排班、任务、我的)
+
+## 技术方案
+
+uni-app 微信小程序支持自定义 tabBar,通过 `custom-tab-bar` 组件实现。
+
+### 1. 启用自定义 tabBar
+
+在 `src/pages.json` 中:
+- 设置 `tabBar.custom = true`
+- 保留 `tabBar.list` 作为基础配置(微信小程序要求),但删除 `iconPath` / `selectedIconPath`,仅保留 `pagePath` 和 `text`
+
+### 2. 新建自定义 tabBar 组件
+
+文件:`src/custom-tab-bar/index.vue`
+
+组件逻辑:
+- 读取 `authStore.userRole`
+- 根据角色返回不同的 tab 列表
+- 使用 `uni.switchTab` 切换页面
+- 高亮当前选中的 tab
+
+### 3. tab 配置
+
+| 角色 | tab 列表 |
+|------|---------|
+| sales | 首页 (`/pages/home/home`)、任务 (`/pages/task/task`)、我的 (`/pages/my/my`) |
+| construction | 首页 (`/pages/home/home`)、任务 (`/pages/task/task`)、我的 (`/pages/my/my`) |
+| dispatch | 首页 (`/pages/home/home`)、排班 (`/subPackages/pages-dispatch/schedule`)、任务 (`/pages/task/task`)、我的 (`/pages/my/my`) |
+
+### 4. 关键约束
+
+- 自定义 tabBar 中的页面必须是 `pages.json` 中 `tabBar.list` 已注册的页面
+- 但 `subPackages/pages-dispatch/schedule` 是分包页面,**不能**直接放进 `tabBar.list`
+- 解决方案:在 `pages` 主包中建一个轻量壳页 `pages/schedule/schedule.vue`,内容直接渲染调度端排班组件;调度端原 `subPackages/pages-dispatch/schedule.vue` 保留或重定向
+
+### 5. 需要修改/新建的文件
+
+| 文件 | 操作 | 说明 |
+|------|------|------|
+| `src/pages.json` | 修改 | `tabBar.custom = true`,调整 `tabBar.list` |
+| `src/custom-tab-bar/index.vue` | 新建 | 自定义 tabBar 组件 |
+| `src/custom-tab-bar/index.json` | 新建 | 组件配置(component: true) |
+| `src/pages/schedule/schedule.vue` | 新建 | 排班 tabBar 壳页(主包) |
+| `src/components/dispatch/ScheduleView.vue` | 新建 | 排班实际内容组件 |
+| `src/subPackages/pages-dispatch/schedule.vue` | 修改 | 复用 `ScheduleView` 组件 |
+| `src/pages/home/home.vue` | 无需修改 | 已按角色渲染 |
+| `src/pages/task/task.vue` | 无需修改 | 已按角色渲染 |
+| `src/pages/my/my.vue` | 无需修改 | 已按角色渲染 |
+
+### 6. 开发步骤
+
+1. 新建 `src/components/dispatch/ScheduleView.vue`,将 `subPackages/pages-dispatch/schedule.vue` 的内容抽成组件
+2. 新建 `src/pages/schedule/schedule.vue`,引入 `ScheduleView`
+3. 修改 `subPackages/pages-dispatch/schedule.vue`,改为引入 `ScheduleView`
+4. 新建 `src/custom-tab-bar/index.vue` 和 `index.json`
+5. 修改 `src/pages.json`:
+   - `tabBar.custom = true`
+   - `tabBar.list` 注册 4 个页面:home、schedule、task、my
+6. 验证构建和类型检查
+7. 在微信开发者工具中测试三个角色 tabBar 显示是否正确
+
+### 7. 风险点
+
+- 自定义 tabBar 首次加载可能略有延迟
+- 分包页面不能直接作为 tabBar 页,必须通过主包壳页中转
+- 需要确保 `authStore.userRole` 在 tabBar 渲染时已经可用
+
+### 8. 验证方式
+
+- `npm run type-check`
+- `npm run build:mp-weixin`
+- 微信开发者工具:分别用 sales/dispatch/construction 角色登录,检查底部 tabBar 数量和跳转
+
+---
+
+文档生成时间:2026-06-24

+ 265 - 0
docs/role-prototype-diff.md

@@ -0,0 +1,265 @@
+# 清道夫 App 角色功能差异分析文档
+
+> 对比基准:当前代码(`src/` 目录) vs 原型文件 `D:\work\绿水青山\清道夫系统原型_完整版_V21_SOP.html`
+> 生成时间:2026-06-23
+
+---
+
+## 一、总体结论
+
+当前项目已实现**登录角色切换 + 首页组件级切换**的框架,但各角色的**页面内容、功能入口、展示字段、操作流程**与原型相比仍有明显差距。主要表现为:
+
+1. **首页信息结构不完整**:销售端缺「客户管理/合同管理」入口、缺「行业知识」模块;调度端缺「待审核任务列表」、统计项与原型不符。
+2. **任务列表字段单一**:当前统一使用 `TaskCard`,只有项目名、地址、时间、车辆、人员;原型中三端展示字段差异很大(销售端看进度/班组/验收,调度端看申请人/施工类型,施工端看 8 步 SOP 流程)。
+3. **任务详情操作未按角色拆分**:当前 `pages-common/taskDetail.vue` 是通用版,字段固定;调度端/施工端已有独立分包详情页,但与原型比字段和流程仍不完整。
+4. **我的页面角色字段未细化**:当前统一显示「用户名/昵称/角色/手机号」,原型中销售/调度显示「部门/职位/电话/工号/邮箱/入职日期」,施工端显示「班组/职位/电话/工号」。
+5. **部分原型页面尚未实现**:如销售端「项目库→新增项目」的完整字段、调度端「可视化/在岗人员/可用车辆/应急待命」、施工端「历史任务/明日任务」的列表字段等。
+
+---
+
+## 二、通用差异
+
+| 差异点 | 当前实现 | 原型要求 | 影响范围 |
+|--------|----------|----------|----------|
+| 角色切换后首页 | 已按角色渲染 `SalesHome/DispatchHome/ConstructionHome` | 同上,但内容与原型差距大 | 全部 |
+| tabBar | 已接入原生 tabBar(首页/任务/我的) | 一致 | 全部 |
+| 页面跳转路径 | 已统一为分包路径 | 基本一致 | 全部 |
+| 状态色 | 已按原型统一语义色 | 一致 | 全部 |
+| 任务数据模型 | `Task` 字段较少,缺 `address/serviceType/team/vehicleNo/staffName/faultLocation` 等 | 字段更丰富 | 全部 |
+| 底部导航 | 原生 tabBar 已实现 | 一致 | 全部 |
+
+---
+
+## 三、销售端差异
+
+### 3.1 首页(`src/components/home/SalesHome.vue`)
+
+| 模块 | 当前实现 | 原型要求 | 差异等级 |
+|------|----------|----------|----------|
+| 顶部欢迎语 | 「下午好 / 张销售」 | 「您好,欢迎回来 / 张销售」 | 低 |
+| 快捷入口 | 项目库、任务、提醒、公告(4 个) | 项目库、客户管理(禁用)、合同管理(禁用)(3 个) | **中** |
+| 数据统计 | 本月项目、待确认、进行中 | 与原型一致,但缺「任务提醒」入口按钮 | **中** |
+| 任务提醒 | 入口在快捷入口里 | 应在统计卡片下方单独按钮「任务提醒 (2)」 | 低 |
+| 通知公告 | 已实现 | 一致 | 无 |
+| 行业知识 | **未实现** | 原型有「行业知识」模块,展示文章列表 | **高** |
+| 跳转路径 | 项目库→`/pages-common/projectList` | 一致 | 无 |
+
+### 3.2 任务列表(`src/components/task/TaskListView.vue`,role=sales)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 筛选标签 | 全部/待确认/待排班/待出车/施工中/已完成 | 原型为:全部/调度中/施工中/已完成/应急 | **中** |
+| 列表形式 | uni-list + 侧滑(催办/取消) | 卡片式,展示状态标签 + 紧急程度 + 时间 | **中** |
+| 卡片字段 | 项目名、地址、状态 | 项目名、紧急程度、施工班组、位置、验收方式、底部进度条 | **高** |
+| 发布按钮 | 销售端任务页**无发布按钮** | 原型任务页右上角有「发布」按钮 | **高** |
+| 空状态 | 已实现 | 一致 | 无 |
+
+### 3.3 任务详情(`src/subPackages/pages-common/taskDetail.vue`)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 展示字段 | 地址、客户、电话、时间、车辆、人员 | 项目信息、施工信息、故障信息、施工要求、甲方确认人等 | **高** |
+| 操作按钮 | 确认出车/到达现场/开始施工/完成任务/催单/取消 | 销售端原型:取消任务 + 催单;完成态可查看验收单/再次预约 | **高** |
+| 进度展示 | 时间轴 7 步 | 原型为 4 步进度条(发布/调度确认/施工中/完成) | **中** |
+| 地图 | 已集成 | 一致 | 无 |
+| 签名 | 已集成 | 一致 | 无 |
+
+### 3.4 我的页面(`src/components/my/ProfileView.vue`)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 信息卡 | 用户名/昵称/角色/手机号 | 部门、职位、电话 | **中** |
+| 基本信息页 | 当前为通用 `pages-common/basicInfo.vue` | 销售端原型:姓名/部门/职位/工号/电话/邮箱/入职日期 | **高** |
+| 消息通知 | 入口统一走公告 | 原型有独立「消息通知」页,分全部/任务通知/系统消息 | **高** |
+| 任务提醒 | 已有 `pages-sales/taskReminder.vue` | 一致 | 无 |
+
+### 3.5 项目库(`src/subPackages/pages-common/projectList.vue`)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 筛选标签 | 需确认 | 全部/包年/单次/季度/月付 | **中** |
+| 项目卡片字段 | 需确认 | 项目名称、合作类型、设施类型、地址、服务点数、联系人 | **中** |
+| 新增项目 | 已有 `addProject.vue` | 原型字段更完整(省市区、联系人、服务点、合同金额等) | **中** |
+| 项目详情 | 已有 `projectDetail.vue` | 原型有地图定位、项目介绍、位置联系人、费用信息、服务点与服务项 | **中** |
+
+---
+
+## 四、调度端差异
+
+### 4.1 首页(`src/components/home/DispatchHome.vue`)
+
+| 模块 | 当前实现 | 原型要求 | 差异等级 |
+|------|----------|----------|----------|
+| 顶部欢迎语 | 「调度中心 / 控制台」 | 「您好,欢迎回来 / 调度中心」 | 低 |
+| 统计卡片标题 | 「今日概览」 | 「今日任务统计」 | 低 |
+| 统计项 | 今日任务、待排班、应急中 | 与原型一致 | 无 |
+| 快捷操作 | 排班、任务、车辆、数据(4 个) | 排班管理、任务列表、车辆管理、可视化(4 个) | 低 |
+| 待审核任务列表 | **未实现** | 原型首页下方有「待审核任务」模块,展示 2 条待审核任务卡片 | **高** |
+| 通知公告 | 已实现 | 一致 | 无 |
+| 跳转路径 | 排班→`/pages-dispatch/schedule` 等 | 一致 | 无 |
+
+### 4.2 任务列表(`src/components/task/TaskListView.vue`,role=dispatch)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 筛选标签 | 全部/待确认/待排班/待出车/施工中/已完成 | 原型:全部/待审核/待排班/进行中/已完成/已驳回 | **中** |
+| 列表形式 | 统一 `TaskCard` | 卡片式,左侧带状态色条 | **中** |
+| 卡片字段 | 项目名、地址、时间、车辆、人员 | 任务名称、位置、施工类型、时间、申请人、状态备注 | **高** |
+| 新增任务 | tabBar 页无入口,但调度端有 `publishTask.vue` | 任务页右上角有「新增」按钮 | **高** |
+| 操作 | 点击跳转详情 | 点击跳转详情 | 无 |
+
+### 4.3 任务详情(`src/subPackages/pages-dispatch/taskDetail.vue`)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 基本信息 | 任务名称、项目、类型、创建时间、负责人、车辆、班组、排班日期 | 任务名称、故障位置、施工类型、服务时间、申请人、项目信息、故障详情、施工信息 | **高** |
+| 审核操作 | 待审核状态:确认/驳回;确认后显示安排班组车辆 | 与原型基本一致 | 无 |
+| 已驳回状态 | 显示固定文案 | 原型显示具体驳回原因 | **中** |
+| 排班操作 | 已实现班组+车辆选择 | 一致 | 无 |
+
+### 4.4 我的页面(`src/components/my/ProfileView.vue`)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 信息卡 | 用户名/昵称/角色/手机号 | 部门、职位、电话 | **中** |
+| 基本信息页 | 通用版 | 调度端原型:姓名/部门/职位/工号/电话/邮箱/入职日期 | **高** |
+| 帮助中心 | 通用 `pages-common/helpCenter.vue` | 调度端帮助中心问题应围绕审核/排班/资源状态 | **中** |
+| 消息通知 | 入口统一走公告 | 原型有独立通知页 | **高** |
+
+### 4.5 排班与资源页面
+
+| 页面 | 当前实现 | 原型要求 | 差异等级 |
+|------|----------|----------|----------|
+| 排班管理 (`schedule.vue`) | 已有基础页面 | 原型有日历、资源状态卡片(在岗人员/可用车辆/应急待命)、当日排班列表 | **中** |
+| 在岗人员 (`onDutyStaff.vue`) | 已有 | 原型展示人员、班组、技能等级、资质标签 | **中** |
+| 可用车辆 (`availableVehicles.vue`) | 已有 | 原型展示车辆类型、尺寸、载重、驾驶员 | **中** |
+| 应急待命 (`emergencyStandby.vue`) | 已有 | 原型展示应急热线、待命班组、车辆、联系按钮 | **中** |
+| 可视化 (`visualization.vue`) | 已有 | 原型为数据可视化大屏,需确认当前实现程度 | **中** |
+| 车辆列表 (`vehicleList.vue`) | 已有 | 原型有车俩管理列表 | 低 |
+
+---
+
+## 五、施工端差异
+
+### 5.1 首页(`src/components/home/ConstructionHome.vue`)
+
+| 模块 | 当前实现 | 原型要求 | 差异等级 |
+|------|----------|----------|----------|
+| 顶部信息 | 「工程一班 / 张师傅」 | 一致 | 无 |
+| 统计卡片 | 已完成、进行中、明日任务 | 历史完成、当前任务、明日任务(语义一致) | 低 |
+| 每日任务入口 | 已实现 | 一致 | 无 |
+| 快捷功能 | 我的任务、车辆检查、历史任务、帮助中心 | 原型:我的任务、车辆检查、帮助中心(3 个) | 低 |
+| 通知公告 | 已实现 | 一致 | 无 |
+| 当前任务预览 | **未实现** | 原型首页下方有「当前任务」预览卡片 | **中** |
+
+### 5.2 任务列表(`src/components/task/TaskListView.vue`,role=construction)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 筛选标签 | 全部/待出车/已出车/施工中/已完成 | 原型:全部/待确认/待出车/已出车/施工中/待验收 | **中** |
+| 列表形式 | 统一 `TaskCard` | 卡片式,展示项目名、加急/包年标签、服务类型、服务时间、地址、当前步骤流程 | **高** |
+| 流程展示 | **未实现** | 原型每个卡片下方有 7 步流程(确认/出车/到达/施工/完成/清洗/验收) | **高** |
+| 操作 | 点击跳转详情 | 点击跳转详情 | 无 |
+
+### 5.3 任务详情(`src/subPackages/pages-construction/taskDetail.vue`)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 施工流程 | 顶部 5 步流程 | 原型为 8 步(确认/出车/到达/勘验/施工/完成/清洗/验收) | **高** |
+| 操作按钮 | 待确认时确认/反馈/拒绝;待出车时出发;已出车到达现场;已到达开始施工;施工中完成施工 | 与原型基本一致,但缺「勘验」步骤和「清洗」步骤 | **高** |
+| 任务信息 | 任务类型、紧急程度、负责人、车辆、班组、计划日期、创建时间 | 原型有项目信息、施工信息、故障信息、施工要求、甲方确认人等大量字段 | **高** |
+| 出车准备/现场踏勘/施工流程记录 | **未实现** | 原型 SOP 新增:车辆点检、出发时间、到达时间、踏勘一致性、客户同意、7 步上传记录 | **高** |
+| 任务日志 | 当前为简单时间轴 | 原型有时间、操作人、动作 | 低 |
+
+### 5.4 我的页面(`src/components/my/ProfileView.vue`)
+
+| 字段/功能 | 当前实现 | 原型要求 | 差异等级 |
+|-----------|----------|----------|----------|
+| 信息卡 | 用户名/昵称/角色/手机号 | 班组、职位、电话 | **中** |
+| 基本信息页 | 通用版 | 施工端原型:姓名/工号/班组/联系电话(可编辑保存) | **高** |
+| 帮助中心 | 通用版 | 施工端帮助中心问题应围绕确认任务/每日检查/施工流程/联系调度 | **中** |
+| 消息通知 | 入口统一走公告 | 原型有独立通知页,分全部/任务/系统 | **高** |
+
+### 5.5 每日任务与其他页面
+
+| 页面 | 当前实现 | 原型要求 | 差异等级 |
+|------|----------|----------|----------|
+| 每日任务 (`dailyTask.vue`) | 已实现车辆检查 + 今日任务列表 | 原型有车辆检查、设备检查、三大系统检查、备注、提交按钮 | **中** |
+| 历史任务 (`historyTask.vue`) | 已有 | 原型展示项目名-服务类型、状态、地址、完成时间 | 低 |
+| 明日任务 (`tomorrowTask.vue`) | 已有 | 原型展示项目名-服务类型、状态、地址、计划时间 | 低 |
+
+---
+
+## 六、数据模型差异
+
+当前 `src/types/index.ts` 中 `Task` 接口字段不足,导致三端任务卡片和详情无法展示原型要求的字段。需要扩展的字段:
+
+| 字段 | 用途 | 涉及端 |
+|------|------|--------|
+| `address` | 任务地址 | 全部 |
+| `serviceType` | 施工类型(疏通/清淤/排查) | 全部 |
+| `facilityType` | 施工设施(PVC/油污管/化粪池等) | 全部 |
+| `serviceMode` | 服务方式(应急/月计划) | 销售/调度 |
+| `faultLocation` | 故障位置 | 全部 |
+| `faultDesc` | 故障描述 | 全部 |
+| `vehicleNo` / `vehicle` | 车牌号/建议出车 | 全部 |
+| `staffName` / `team` | 负责人/班组 | 全部 |
+| `assignee` | 负责人 | 全部 |
+| `coopType` | 合作类型(包年/单次等) | 销售/调度 |
+| `acceptType` | 验收方式 | 销售/调度 |
+| `urgent` | 紧急程度 | 全部 |
+| `applicant` / `applicantName` | 申请人 | 调度 |
+| `applyTime` | 申请时间 | 调度 |
+| `rejectReason` | 驳回原因 | 调度 |
+| `serviceTime` / `scheduleTime` | 服务时间 | 全部 |
+| `serviceDate` / `scheduledDate` | 服务日期 | 全部 |
+| `customerName` / `customerPhone` | 客户信息 | 全部 |
+| `partyAContacts` | 甲方联系人 | 调度/施工 |
+| `location` | 经纬度 | 全部 |
+
+---
+
+## 七、实施优先级建议
+
+### P0(核心差异,优先完成)
+
+1. **按角色改造首页内容**:补齐销售端「行业知识」、调度端「待审核任务」、施工端「当前任务预览」。
+2. **按角色改造任务列表字段**:销售端展示进度条/班组/验收;调度端展示申请人/施工类型;施工端展示 7 步 SOP 流程。
+3. **按角色改造我的页面**:销售/调度显示部门/职位/工号/邮箱/入职日期;施工端显示班组/职位/工号。
+4. **扩展 Task 数据模型**:补充 `address/serviceType/facilityType/acceptType/applicant` 等字段。
+
+### P1(重要差异,次之)
+
+5. **按角色改造任务详情与操作**:销售端只看信息+催单/取消;调度端审核+排班;施工端 8 步 SOP 流程。
+6. **调整筛选标签**:销售端用「调度中/施工中/已完成/应急」;调度端用「待审核/待排班/进行中/已完成/已驳回」;施工端用「待确认/待出车/已出车/施工中/待验收」。
+7. **销售端任务页增加「发布」按钮**,调度端任务页增加「新增」按钮。
+
+### P2(体验优化,可后续)
+
+8. 完善项目库新增项目字段、项目详情字段。
+9. 完善施工端每日任务(设备检查、三大系统检查、提交)。
+10. 完善各端帮助中心文案和消息通知页。
+11. 增加车辆/人员/应急待命详情字段。
+
+---
+
+## 八、关键文件清单
+
+| 文件路径 | 改造内容 |
+|----------|----------|
+| `src/components/home/SalesHome.vue` | 首页内容 |
+| `src/components/home/DispatchHome.vue` | 首页内容 |
+| `src/components/home/ConstructionHome.vue` | 首页内容 |
+| `src/components/task/TaskListView.vue` | 任务列表筛选与展示 |
+| `src/components/task/TaskCard.vue` / 新增角色卡片 | 任务卡片字段 |
+| `src/components/my/ProfileView.vue` | 我的页面角色字段 |
+| `src/types/index.ts` | 扩展 Task/User 类型 |
+| `src/stores/task.ts` | 补充统计 getter |
+| `src/subPackages/pages-common/taskDetail.vue` | 销售端任务详情 |
+| `src/subPackages/pages-dispatch/taskDetail.vue` | 调度端任务详情 |
+| `src/subPackages/pages-construction/taskDetail.vue` | 施工端任务详情 |
+| `src/subPackages/pages-construction/dailyTask.vue` | 每日任务检查项 |
+
+---
+
+*文档生成完毕,后续改造将按此文档逐项执行。*

+ 194 - 0
docs/style-redesign.md

@@ -0,0 +1,194 @@
+# 清道夫 App 全局样式重设计调整文档
+
+## 一、调整目标
+
+让小程序整体视觉更符合微信小程序原生风格:
+
+1. 统一页面背景色、卡片、文字、图标配色
+2. 大量减少渐变色和花俏装饰
+3. 简化首页头部,去掉毛玻璃、渐变背景
+4. 统一卡片、列表、按钮、标签样式
+5. 保持各角色首页信息结构清晰
+
+---
+
+## 二、色彩规范
+
+### 主色板
+
+| 名称 | 色值 | Tailwind 类 | 用途 |
+|------|------|-------------|------|
+| 主色 | `#2563eb` | `bg-primary` / `text-primary` | 按钮、激活态、主图标 |
+| 主色浅 | `#eff6ff` | `bg-primary-light` | 图标底色、选中背景 |
+| 成功 | `#10b981` | `text-success` | 完成/通过状态 |
+| 成功浅 | `#ecfdf5` | `bg-success-light` | 成功状态背景 |
+| 警告 | `#f59e0b` | `text-warning` | 待处理/提醒 |
+| 警告浅 | `#fff7ed` | `bg-warning-light` | 警告状态背景 |
+| 危险 | `#ef4444` | `bg-danger` / `text-danger` | 取消/拒绝/重要 |
+| 危险浅 | `#fef2f2` | `bg-danger-light` | 危险状态背景 |
+
+### 中性色
+
+| 名称 | 色值 | Tailwind 类 | 用途 |
+|------|------|-------------|------|
+| 页面背景 | `#f5f5f5` | `bg-background` | 全局页面底色 |
+| 浅灰背景 | `#f3f4f6` | `bg-surface` | hover、筛选标签默认态 |
+| 主要文字 | `#1f2937` | `text-gray-800` | 标题、正文 |
+| 次要文字 | `#6b7280` | `text-gray-500` | 备注、说明 |
+| 辅助文字 | `#9ca3af` | `text-gray-400` | 时间、占位 |
+| 分割线 | `#e5e7eb` | `border-gray-200` | 边框、分隔线 |
+
+> 说明:`text-primary`、`text-secondary` 等语义化文字类因 Tailwind 生成规则与命名冲突,统一改用 `text-gray-800/500/400`。
+
+---
+
+## 三、核心样式文件变更
+
+### 1. `src/styles/variables.scss`
+
+- 背景色改为 `#f5f5f5`
+- 主色改为 `#2563eb`
+- 删除渐变按钮、渐变标签相关变量
+- 文字色改为 `#1f2937` / `#6b7280` / `#9ca3af`
+- 卡片阴影更柔和
+
+### 2. `src/styles/global.scss`
+
+- 去掉所有渐变按钮、渐变标签、渐变进度条
+- `.card` 改为白底 + 1px 浅灰边框 + 柔和阴影
+- `.btn-primary` 等改为纯色
+- `.tag-*` 改为纯色小圆角标签
+- 新增 `.icon-box` 图标容器
+- 新增 `.page-header` 页面头部样式
+
+### 3. `tailwind.config.js`
+
+- 扩展颜色:`primary`、`success`、`warning`、`danger` 及其浅色
+- 扩展:`background`、`surface`、`card-bg`
+- 保持 `preflight: false`
+
+---
+
+## 四、页面/组件具体调整
+
+### 首页(SalesHome / DispatchHome / ConstructionHome)
+
+| 调整项 | 改前 | 改后 |
+|--------|------|------|
+| 顶部背景 | `bg-gradient-to-br` 渐变 + 毛玻璃 | 白色背景 + 下边框 |
+| 用户头像区 | 半透明毛玻璃 | `bg-primary-light` + `text-primary` |
+| 通知按钮 | 半透明毛玻璃 | `bg-surface` + `text-gray-500` |
+| 统计卡片 | 彩色背景 `bg-blue-50`/`bg-orange-50` | 白色卡片内 `bg-surface` 统计块 |
+| 统计数字 | 彩色 `text-blue-500` | 语义色 `text-primary`/`text-warning`/`text-success` |
+| 快捷入口图标 | 各功能彩色底 | 统一 `bg-primary-light` + `text-primary` |
+| 卡片阴影 | `is-shadow="true"` | `is-shadow="false"`,统一边框 |
+
+### 任务列表(TaskListView)
+
+- 筛选标签默认态:`bg-surface text-gray-500`
+- 选中态:`bg-primary text-white`
+- 状态图标:统一语义化颜色
+- 加载/空态文字改为 `text-gray-400`
+
+### 我的页面(ProfileView)
+
+- 顶部:白底 + 下边框,头像 `bg-primary-light`
+- 卡片统一 `is-shadow="false"`
+- 退出登录按钮:白底红字 + 灰边框
+
+### 登录页(login.vue)
+
+- 背景:`bg-background`
+- Logo:纯色 `bg-primary`
+- 表单卡片:白底 + 灰边框 + 柔和阴影
+- 按钮:`bg-primary` 纯色
+- 输入框:`bg-surface` + 灰边框
+
+### 知识库(knowledgeList / knowledgeDetail)
+
+- 列表封面:去掉渐变,改为 `bg-primary-light` + `text-primary`
+- 详情封面:去掉渐变,改为 `bg-primary-light`
+- 摘要/内容/分类块:使用 `bg-primary-light`、`bg-success-light`、`bg-surface`
+
+### 通用组件
+
+| 组件 | 调整 |
+|------|------|
+| `uni-card` | 默认有 1px 灰边框,阴影默认关闭,标题 `#1f2937`,extra `#2563eb` |
+| `uni-list` | 边框颜色 `#e5e7eb` |
+| `uni-list-item` | 图标背景 `#f3f4f6`,图标颜色 `#6b7280`,hover 背景 `#f5f5f5`,文字色更新 |
+| `FilterTabs` | 默认 `bg-surface text-gray-500`,选中 `bg-primary text-white` |
+| `StatusTag` | 通过 `utils/index.ts` 统一状态色 |
+
+---
+
+## 五、状态色统一
+
+`src/utils/index.ts` 中状态颜色已统一:
+
+```js
+pending/inspecting    -> 警告色 #f59e0b / #fff7ed
+confirmed/scheduled/constructing -> 主色 #2563eb / #eff6ff
+departed              -> 紫色 #8b5cf6 / #f3e8ff
+arrived/completed     -> 成功色 #10b981 / #ecfdf5
+cancelled/rejected    -> 危险色 #ef4444 / #fef2f2
+```
+
+---
+
+## 六、注意事项
+
+1. **语义化颜色类使用规则**
+   - 可用:`bg-primary`、`bg-primary-light`、`bg-success-light`、`bg-warning-light`、`bg-danger-light`、`bg-surface`、`bg-background`、`text-primary`、`text-success`、`text-warning`、`text-danger`
+   - 避免:`text-text-*`、`bg-text-*`、`border-border-color` 等带连词的自定义类,Tailwind 可能无法正确解析
+
+2. **卡片阴影**
+   - 改造后默认使用 `is-shadow="false"`,依赖 `uni-card` 自带的 1px 边框分隔内容
+   - 如需强调可用 `shadow-card`
+
+3. **渐变保留**
+   - 本次改造后,除 `global.scss` 中原有少量样式外,业务页面不再使用渐变背景
+   - 后续如需品牌氛围,可在启动页或空状态插画中使用
+
+4. **图标**
+   - 快捷入口、列表项、状态标签统一使用 `uni-icons`
+   - 图标底色统一 `bg-primary-light`,图标颜色统一 `text-primary`
+
+---
+
+## 七、已修改文件清单
+
+- `src/styles/variables.scss`
+- `src/styles/global.scss`
+- `tailwind.config.js`
+- `src/pages.json`
+- `src/utils/index.ts`
+- `src/pages/login/login.vue`
+- `src/components/home/SalesHome.vue`
+- `src/components/home/DispatchHome.vue`
+- `src/components/home/ConstructionHome.vue`
+- `src/components/my/ProfileView.vue`
+- `src/components/task/TaskListView.vue`
+- `src/components/common/FilterTabs.vue`
+- `src/components/uni-card/uni-card.vue`
+- `src/components/uni-list/uni-list.vue`
+- `src/components/uni-list/uni-list-item.vue`
+- `src/subPackages/pages-common/knowledgeList.vue`
+- `src/subPackages/pages-common/knowledgeDetail.vue`
+
+---
+
+## 八、验证结果
+
+- `npm run build:mp-weixin` 编译通过
+- 主包体积 923K,各分包 62K–152K,均小于 2MB
+- 生成 WXSS 中无无效类名
+
+---
+
+## 九、后续建议
+
+1. 逐步将剩余页面中的硬编码 `#3b82f6` / `#f59e0b` 等替换为语义化类
+2. 检查分包页面中的表单、详情页是否还有未统一的彩色背景块
+3. 如果 UI 提供正式 tabBar 图标,替换 `src/static/tabbar/` 下占位图标
+4. 继续验证各角色首页、任务页、我的页在微信开发者工具中的实际效果

+ 46 - 0
docs/superpowers/reports/2026-06-24-fix-missing-custom-tabbar-verify.md

@@ -0,0 +1,46 @@
+# 验证报告:fix-missing-custom-tabbar
+
+**变更**: fix-missing-custom-tabbar
+**验证时间**: 2026-06-24
+**验证模式**: light
+**验证人**: Claude Opus 4.6
+
+## 变更摘要
+
+修复微信小程序端自定义 tabBar 不显示的问题。根因为 `src/custom-tab-bar/index.vue` 在编译到微信小程序时未被编译为原生组件格式,导致构建产物中只有 `.vue` 文件,微信小程序无法识别。
+
+修复方式:将 `src/custom-tab-bar/index.vue` 替换为微信小程序原生自定义 tabBar 组件:
+- `src/custom-tab-bar/index.wxml`
+- `src/custom-tab-bar/index.wxss`
+- `src/custom-tab-bar/index.js`
+- `src/custom-tab-bar/index.json`
+
+## 轻量验证检查项
+
+| # | 检查项 | 结果 | 说明 |
+|---|--------|------|------|
+| 1 | tasks.md 全部完成 | PASS | 4 项任务均已标记 `[x]` |
+| 2 | 改动文件与 tasks 一致 | PASS | 本次提交仅涉及 `src/custom-tab-bar/` 与 OpenSpec 变更产物 |
+| 3 | 编译通过 | PASS | `npm run build:mp-weixin` 成功 |
+| 4 | 相关测试通过 | PASS | `npm run type-check` 通过;项目无单元测试 |
+| 5 | 无明显安全问题 | PASS | 无硬编码密钥、无新增 unsafe 操作 |
+| 6 | 代码审查 | SKIP | `review_mode: off` |
+
+## 构建产物验证
+
+`dist/build/mp-weixin/custom-tab-bar/` 下已生成:
+- `index.wxml`
+- `index.wxss`
+- `index.js`
+- `index.json`
+
+符合微信小程序自定义 tabBar 要求。
+
+## 注意
+
+- `verification-before-completion` 与 `finishing-a-development-branch` Superpowers 技能在当前环境未安装,本次验证按 Comet 轻量验证清单手动执行。
+- 工作区仍存在大量与本次 hotfix 无关的未提交改动(角色化页面重构),这些改动不属于本变更,未纳入本次验证与提交。
+
+## 结论
+
+**验证通过** — 可进入归档阶段。

+ 147 - 0
docs/superpowers/specs/2026-06-24-weapp-capsule-safearea-design.md

@@ -0,0 +1,147 @@
+---
+comet_change: weapp-capsule-safearea
+role: technical-design
+canonical_spec: openspec
+---
+
+# 微信小程序胶囊安全区适配设计
+
+## 背景
+
+清道夫微信小程序所有页面均使用 `navigationStyle: custom` 自定义导航栏。微信小程序右上角存在系统胶囊按钮(菜单 + 关闭),在真机上会遮挡页面右上角的操作按钮(通知铃铛、新增/发布、今天、新增排班等)。
+
+本次 change 引入全局统一的胶囊安全区组件,让 `TopBar` 右侧操作区自动避让胶囊。
+
+## 目标
+
+- 提供可在任何页面/组件复用的胶囊安全区方案。
+- 改造 `TopBar.vue`,使其右侧插槽内容自动避让胶囊。
+- 修复首页、任务页、排班页的胶囊遮挡问题。
+- 保持 H5/App 等非微信环境下的正常显示(graceful fallback)。
+
+## 非目标
+
+- 不调整底部 tabBar 结构。
+- 不改页面整体布局风格。
+- 不处理胶囊以外的系统 UI(如灵动岛、底部横条)。
+- 不做横屏/旋转后的实时安全区重算(后续可单独优化)。
+
+## 组件设计
+
+### `CapsuleSafeArea.vue`
+
+新增 `src/components/common/CapsuleSafeArea.vue`。
+
+#### 职责
+
+计算微信小程序胶囊按钮所需的最小右侧安全宽度,并以 wrapper 形式给默认 slot 内容添加左侧间距。
+
+#### 算法
+
+```
+safeWidth = screenWidth - menuButton.left + 8px
+```
+
+- `screenWidth` 来自 `uni.getSystemInfoSync().screenWidth`
+- `menuButton.left` 来自 `uni.getMenuButtonBoundingClientRect().left`
+- `8px` 为额外缓冲,避免按钮紧贴胶囊
+
+#### 平台回退
+
+- 微信小程序:使用上述动态计算值。
+- H5 / App / 其他平台:返回 `0`,布局保持不变。
+- API 调用失败:使用基于 `screenWidth` 的保守估算值兜底(约为屏幕宽度的 28%,覆盖常见 iPhone/Android 胶囊宽度)。
+
+#### API
+
+```vue
+<template>
+  <CapsuleSafeArea>
+    <!-- 右侧操作按钮 -->
+  </CapsuleSafeArea>
+</template>
+```
+
+组件仅提供默认 slot,不暴露 props。内部使用 `:style="{ marginLeft: `${safeWidth}px` }"` 作用在包裹层上。
+
+### `TopBar.vue` 改造
+
+修改 `src/components/common/TopBar.vue`:
+
+- 右侧 `<slot name="right" />` 外层自动包裹 `<CapsuleSafeArea>`。
+- 页面只需正常填充 `<template #right>`,无需感知胶囊安全区。
+- 标题区使用 `flex-1 min-w-0 truncate`,避免左侧内容过长或右侧安全区挤压导致标题溢出。
+
+## 页面改造
+
+### 首页(`DispatchHome.vue` / `SalesHome.vue` / `ConstructionHome.vue`)
+
+当前三个首页使用自定义 header:左侧头像+欢迎语,右侧通知铃铛。
+
+改造为使用 `TopBar`:
+
+- 左侧头像和欢迎语放入 `left slot`。
+- 右侧通知铃铛放入 `right slot`。
+- 保持原有白底、下边框、内边距风格。
+- `TopBar` 的 `title` 可置空或使用页面标题(如「首页」),本次保持顶部以用户欢迎语为主,标题置空。
+
+### 任务页(`TaskListView.vue`)
+
+当前顶部是自定义标题栏 + 筛选标签,右上角是「发布/新增」按钮。
+
+改造:
+
+- 顶部标题栏改为 `<TopBar :title="pageTitle">`,右侧放入发布/新增按钮。
+- 筛选标签保留在 TopBar 下方,不受安全区影响。
+- 保留底部的悬浮「+」按钮(仅在非调度端角色显示)。
+
+### 排班页(`ScheduleView.vue`)
+
+当前已使用 `TopBar`,但「今天」按钮在月份切换行右侧,「新增排班」在列表标题右侧。
+
+改造:
+
+- TopBar 标题保持「排班管理」。
+- 右侧 `slot` 同时放入「今天」和「新增排班」两个操作按钮,中间保留 `gap-2` 间距。
+- 月份切换行只保留左侧月份导航,右侧清空。
+
+## 数据流与状态
+
+- `CapsuleSafeArea` 为纯展示组件,无外部状态依赖。
+- 安全宽度在 `onMounted` 中计算一次,不监听窗口尺寸变化。
+- 计算结果通过响应式变量绑定到 style,无全局 store 或事件总线。
+
+## 测试策略
+
+1. **类型检查**:`npm run type-check`,确保 TypeScript 无错误。
+2. **构建验证**:`npm run build:mp-weixin`,确保微信小程序构建通过。
+3. **真机/模拟器验证**:
+   - 检查首页右上角通知铃铛未被胶囊遮挡。
+   - 检查任务页右上角发布/新增按钮可点击。
+   - 检查排班页「今天」「新增排班」均未被遮挡。
+4. **H5 回归**:`npm run build:h5`,确认非微信环境下布局与之前一致。
+
+## 风险与回退
+
+| 风险 | 影响 | 缓解措施 |
+|------|------|----------|
+| `uni.getMenuButtonBoundingClientRect()` 获取失败 | 安全区宽度计算错误,可能仍遮挡 | 使用基于屏幕宽度的估算值兜底 |
+| 不同机型胶囊尺寸差异大 | 固定估算值可能不足 | 优先使用动态 API,仅在失败时回退 |
+| 顶部空间被压缩 | 标题可能显示不全 | 标题使用 `truncate` 截断 |
+| 横屏/旋转 | 胶囊位置变化后安全区失效 | 本次不处理,后续单独优化 |
+
+## 依赖
+
+- `uni.getMenuButtonBoundingClientRect()`
+- `uni.getSystemInfoSync()`
+- 无新增 npm 依赖,无后端接口变更。
+
+## 受影响文件
+
+- 新增:`src/components/common/CapsuleSafeArea.vue`
+- 修改:`src/components/common/TopBar.vue`
+- 修改:`src/components/home/DispatchHome.vue`
+- 修改:`src/components/home/SalesHome.vue`
+- 修改:`src/components/home/ConstructionHome.vue`
+- 修改:`src/components/task/TaskListView.vue`
+- 修改:`src/components/dispatch/ScheduleView.vue`

+ 7 - 6
openspec/changes/fix-missing-custom-tabbar/.comet.yaml → openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/.comet.yaml

@@ -1,5 +1,5 @@
 workflow: hotfix
-phase: build
+phase: archive
 context_compression: off
 build_mode: direct
 build_pause: null
@@ -12,9 +12,10 @@ auto_transition: true
 base_ref: 90d1562daef8961529c03735022dab7b906a46bd
 design_doc: null
 plan: null
-verify_result: pending
-verification_report: null
-branch_status: pending
+verify_result: pass
+verification_report: docs/superpowers/reports/2026-06-24-fix-missing-custom-tabbar-verify.md
+branch_status: handled
 created_at: 2026-06-24
-verified_at: null
-archived: false
+verified_at: 2026-06-24
+archived: true
+build_command: npm run build:mp-weixin

+ 0 - 0
openspec/changes/fix-missing-custom-tabbar/.openspec.yaml → openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/.openspec.yaml


+ 0 - 0
openspec/changes/fix-missing-custom-tabbar/design.md → openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/design.md


+ 0 - 0
openspec/changes/fix-missing-custom-tabbar/proposal.md → openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/proposal.md


+ 1 - 1
openspec/changes/fix-missing-custom-tabbar/tasks.md → openspec/changes/archive/2026-06-24-fix-missing-custom-tabbar/tasks.md

@@ -3,4 +3,4 @@
 - [x] 检查 `pages.json`、`manifest.json`、`custom-tab-bar/index.vue` 配置一致性
 - [x] 将 `src/custom-tab-bar/index.vue` 替换为微信小程序原生组件(wxml/wxss/js)
 - [x] 运行 `npm run type-check` 与 `npm run build:mp-weixin` 验证编译产物
-- [ ] 提交修复
+- [x] 提交修复

+ 22 - 0
openspec/changes/weapp-capsule-safearea/.comet.yaml

@@ -0,0 +1,22 @@
+workflow: full
+phase: build
+context_compression: off
+build_mode: subagent-driven-development
+build_pause: null
+subagent_dispatch: confirmed
+tdd_mode: tdd
+review_mode: standard
+isolation: branch
+verify_mode: null
+auto_transition: true
+base_ref: fe7ae882ea6905abf5f97f370efe8ffee9a7134f
+design_doc: docs/superpowers/specs/2026-06-24-weapp-capsule-safearea-design.md
+plan: docs/superpowers/plans/2026-06-24-weapp-capsule-safearea.md
+verify_result: pending
+verification_report: null
+branch_status: pending
+created_at: 2026-06-24
+verified_at: null
+archived: false
+handoff_context: openspec/changes/weapp-capsule-safearea/.comet/handoff/design-context.json
+handoff_hash: aa611532c57837edccdaa3c5221ab2e70b42ac50630cca269c20b0a2dbbd2af3

+ 34 - 0
openspec/changes/weapp-capsule-safearea/.comet/handoff/brainstorm-summary.md

@@ -0,0 +1,34 @@
+# Brainstorm Summary
+
+- Change: weapp-capsule-safearea
+- Date: 2026-06-24
+
+## 确认的技术方案
+
+采用「统一移入 TopBar + 自动包裹 right slot」的方案:
+
+1. 新增 `CapsuleSafeArea.vue` 组件,基于 `uni.getMenuButtonBoundingClientRect()` 计算胶囊安全宽度,以 wrapper 形式给 slot 内容加 `margin-left`。
+2. `TopBar.vue` 内部自动用 `CapsuleSafeArea` 包裹 right slot,页面无需感知安全区。
+3. 三个首页组件和 `TaskListView` 从自定义 header 重构为使用 `TopBar`;`ScheduleView` 的「今天」「新增排班」均移入 `TopBar` right slot。
+
+## 关键取舍与风险
+
+- **取舍**:选择 TopBar 自动包裹而非 spacer/composable,是因为最符合「统一移入 TopBar」的目标,页面改造成本最低。
+- **风险**:`uni.getMenuButtonBoundingClientRect()` 可能获取失败,使用基于屏幕宽度的估算值兜底。
+- **风险**:横屏/旋转时胶囊尺寸变化,当前不做实时监听,后续如需可单独提 change。
+
+## 测试策略
+
+- `npm run type-check`
+- `npm run build:mp-weixin`
+- 微信开发者工具真机检查
+- H5 构建确认布局无回归
+
+## Spec Patch
+
+将回写 `openspec/changes/weapp-capsule-safearea/specs/weapp-capsule-safearea/spec.md`:
+
+- 明确首页、任务列表需要从「自定义 header」重构为使用 `TopBar`。
+- 明确 `ScheduleView` 中「今天」和「新增排班」均移入 `TopBar` right slot。
+- 明确 `CapsuleSafeArea` 采用 wrapper 形式给 slot 内容加 `margin-left`。
+- 补充 API 失败时的回退行为验收场景。

+ 14 - 0
openspec/changes/weapp-capsule-safearea/.comet/handoff/design-context.json

@@ -0,0 +1,14 @@
+{
+  "change": "weapp-capsule-safearea",
+  "phase": "design",
+  "mode": "compact",
+  "canonical_spec": "openspec",
+  "generated_by": "comet-handoff.sh",
+  "context_hash": "aa611532c57837edccdaa3c5221ab2e70b42ac50630cca269c20b0a2dbbd2af3",
+  "files": [
+    { "path": "openspec/changes/weapp-capsule-safearea/proposal.md", "sha256": "ff88164caf99258a7df621a84b1c163d9e934971c2b80ea01f6fccedbb6e528c" },
+    { "path": "openspec/changes/weapp-capsule-safearea/design.md", "sha256": "acc12d11b7486d4edaac7949821ba1529b66b165023c1c0897993724dccfe889" },
+    { "path": "openspec/changes/weapp-capsule-safearea/tasks.md", "sha256": "03218da571a4b7615bbf048d47b0b4479114a6ee5485fec873c6b65a84415757" },
+    { "path": "openspec/changes/weapp-capsule-safearea/specs/weapp-capsule-safearea/spec.md", "sha256": "5983ff126f33775ed1825d40ff0fb8f36d74fa8e2e0e8f7d7874f05a2b3719bc" }
+  ]
+}

+ 210 - 0
openspec/changes/weapp-capsule-safearea/.comet/handoff/design-context.md

@@ -0,0 +1,210 @@
+# Comet Design Handoff
+
+- Change: weapp-capsule-safearea
+- Phase: design
+- Mode: compact
+- Context hash: aa611532c57837edccdaa3c5221ab2e70b42ac50630cca269c20b0a2dbbd2af3
+
+Generated-by: comet-handoff.sh
+
+OpenSpec remains the canonical capability spec. This handoff is a deterministic, source-traceable context pack, not an agent-authored summary.
+
+## openspec/changes/weapp-capsule-safearea/proposal.md
+
+- Source: openspec/changes/weapp-capsule-safearea/proposal.md
+- Lines: 1-30
+- SHA256: ff88164caf99258a7df621a84b1c163d9e934971c2b80ea01f6fccedbb6e528c
+
+```md
+## Why
+
+微信小程序右上角存在原生胶囊按钮(菜单/关闭),宽度在不同机型下约为 87px。当前项目所有页面均使用 `navigationStyle: custom` 自定义导航栏,并把部分操作按钮(通知、新增、今天等)放在右上角,导致部署到微信真机后胶囊遮挡功能按钮,影响使用。需要引入全局统一的胶囊安全区组件,让顶部栏右侧操作区自动避让胶囊。
+
+## What Changes
+
+- 新增全局胶囊安全区组件 `CapsuleSafeArea`,基于 `uni.getMenuButtonBoundingClientRect()` 计算右侧安全宽度。
+- 改造 `TopBar` 组件,右侧插槽自动包裹在安全区内,保证操作按钮不会被胶囊遮挡。
+- 调整三个已确认存在遮挡问题的页面顶部区域:
+  - `pages/home/home`(各角色首页右上角通知铃铛)
+  - `pages/schedule/schedule`(排班页右上角「今天」「新增排班」)
+  - `pages/task/task`(任务页右上角「新增/发布」按钮)
+- 胶囊安全区采用「右侧操作按钮整体左移」策略,改动最小且保持按钮可点。
+- 非微信环境(H5/App)回退到默认右侧间距,避免异常。
+
+## Capabilities
+
+### New Capabilities
+
+- `weapp-capsule-safearea`: 微信小程序胶囊安全区适配,提供全局组件和 TopBar 集成。
+
+### Modified Capabilities
+
+- 无
+
+## Impact
+
+- 受影响的文件:`src/components/common/TopBar.vue`、新增 `src/components/common/CapsuleSafeArea.vue`、`src/pages/home/home.vue`、`src/pages/schedule/schedule.vue`、`src/pages/task/task.vue`。
+- 依赖:仅使用 uni-app 内置 API `uni.getMenuButtonBoundingClientRect()` 与 `uni.getSystemInfoSync()`。
+- 不涉及后端接口、数据模型、API 变更。
+```
+
+## openspec/changes/weapp-capsule-safearea/design.md
+
+- Source: openspec/changes/weapp-capsule-safearea/design.md
+- Lines: 1-66
+- SHA256: acc12d11b7486d4edaac7949821ba1529b66b165023c1c0897993724dccfe889
+
+```md
+## Context
+
+清道夫微信小程序使用 `navigationStyle: custom` 实现自定义导航栏。所有页面顶部的标题、返回按钮、功能按钮(通知、新增、筛选等)都由组件自行渲染。然而微信小程序右上角存在系统胶囊按钮(菜单 + 关闭),在 iPhone 上宽度约 87px、安卓上约 96px,且距离屏幕右侧有一定边距。当前部分页面把操作按钮直接放在右上角,导致真机运行时胶囊遮挡按钮。
+
+已有组件:`StatusBar.vue` 负责状态栏占位,`TopBar.vue` 负责标题栏。两者均未考虑胶囊区域。
+
+## Goals / Non-Goals
+
+**Goals:**
+- 提供可在任何页面/组件复用的胶囊安全区方案。
+- 改造 `TopBar.vue`,使其右侧插槽内容自动避让胶囊。
+- 修复 `pages/home/home`、`pages/schedule/schedule`、`pages/task/task` 的遮挡问题。
+- 保持 H5/App 等非微信环境下的正常显示( graceful fallback)。
+
+**Non-Goals:**
+- 不调整底部 tabBar 结构。
+- 不改页面整体布局风格。
+- 不处理胶囊以外的系统 UI(如灵动岛、底部横条)。
+
+## Decisions
+
+### 1. 新增 `CapsuleSafeArea.vue` 组件
+- 通过 `uni.getMenuButtonBoundingClientRect()` 获取胶囊坐标与宽度。
+- 通过 `uni.getSystemInfoSync()` 获取屏幕宽度。
+- 计算右侧需要避让的最小宽度:`screenWidth - menuButton.left + 8px`(额外 8px 缓冲)。
+- 在微信小程序下渲染一个占位 view;在其他平台返回 0 或固定值,避免影响布局。
+- 提供 `width` 插槽 prop 或默认 slot,方便 `TopBar` 直接使用。
+
+### 2. `TopBar.vue` 集成安全区
+- 右侧 `<slot name="right" />` 外层用 `CapsuleSafeArea` 包裹,使右侧内容左侧平移。
+- 保持标题居左或根据左侧内容自适应,不改动标题样式。
+- 为避免标题被挤压过窄,标题区使用 `flex-1 min-w-0 truncate`。
+
+### 3. 页面顶部操作按钮统一使用 `TopBar`
+- `pages/home/home` 三个首页当前直接在页面内写 header,需把右上角通知铃铛移入 `TopBar` 的 right slot,或给按钮加 `mr-[safeWidth]`。
+- `pages/task/task` 的 `TaskListView` 顶部「新增」按钮纳入 `TopBar` right slot。
+- `pages/schedule/schedule` 的 `ScheduleView` 顶部「今天」「新增排班」按钮纳入 `TopBar` right slot。
+
+### 4. 策略选择:右侧按钮整体左移
+- 对比方案:
+  - 按钮换行:改动大,破坏视觉一致性。
+  - 右侧留空白:浪费空间且用户看不到原因。
+  - 按钮整体左移:改动最小,符合微信小程序主流做法,保留按钮可点击区域。
+- 选定「按钮整体左移」。
+
+## Risks / Trade-offs
+
+- **[Risk]** `uni.getMenuButtonBoundingClientRect()` 在小程序初始化后可能异步变化(如旋转屏幕)。
+  - **Mitigation:** 在 `onMounted` 获取并监听窗口尺寸变化;如获取失败则使用基于屏幕宽度的默认估算值。
+- **[Risk]** 不同机型胶囊尺寸差异大。
+  - **Mitigation:** 使用动态 API 获取真实尺寸,而非写死 px。
+- **[Risk]** 顶部标题栏空间被压缩,标题可能显示不全。
+  - **Mitigation:** 标题使用 `truncate` 截断,并确保左侧返回按钮只在需要时显示。
+- **[Trade-off]** H5/App 下无胶囊,安全区宽度为 0,布局与之前一致。
+
+## Migration Plan
+
+- 新增 `CapsuleSafeArea.vue`。
+- 修改 `TopBar.vue`,集成安全区。
+- 修改三个页面/视图组件,使右上角按钮通过 `TopBar` right slot 渲染。
+- 构建 `npm run build:mp-weixin` 验证无编译错误。
+- 在微信开发者工具真机调试中检查胶囊未遮挡按钮。
+
+## Open Questions
+
+- 是否需要在每个页面单独处理 `StatusBar + TopBar`,还是统一封装一个 `PageHeader`?本次保持最小改动,只改 `TopBar`。
+```
+
+## openspec/changes/weapp-capsule-safearea/tasks.md
+
+- Source: openspec/changes/weapp-capsule-safearea/tasks.md
+- Lines: 1-16
+- SHA256: 03218da571a4b7615bbf048d47b0b4479114a6ee5485fec873c6b65a84415757
+
+```md
+## 1. Core component
+
+- [ ] 1.1 Create `src/components/common/CapsuleSafeArea.vue` to compute and expose capsule safe width.
+- [ ] 1.2 Modify `src/components/common/TopBar.vue` to wrap the right slot with `CapsuleSafeArea`.
+
+## 2. Page header fixes
+
+- [ ] 2.1 Update `src/components/home/DispatchHome.vue`, `SalesHome.vue`, `ConstructionHome.vue` to place the notification bell inside `TopBar` right slot.
+- [ ] 2.2 Update `src/components/task/TaskListView.vue` to place the add/publish button inside `TopBar` right slot.
+- [ ] 2.3 Update `src/components/dispatch/ScheduleView.vue` to place "今天" and "新增排班" buttons inside `TopBar` right slot.
+
+## 3. Validation
+
+- [ ] 3.1 Run `npm run type-check` and fix any TypeScript errors.
+- [ ] 3.2 Run `npm run build:mp-weixin` and confirm the build succeeds.
+- [ ] 3.3 Commit each completed task with a clear message.
+```
+
+## openspec/changes/weapp-capsule-safearea/specs/weapp-capsule-safearea/spec.md
+
+- Source: openspec/changes/weapp-capsule-safearea/specs/weapp-capsule-safearea/spec.md
+- Lines: 1-51
+- SHA256: 5983ff126f33775ed1825d40ff0fb8f36d74fa8e2e0e8f7d7874f05a2b3719bc
+
+```md
+## ADDED Requirements
+
+### Requirement: CapsuleSafeArea component calculates safe margin
+The system SHALL provide a `CapsuleSafeArea` component that computes the minimum right-side margin needed to avoid the WeChat mini-program capsule button, and wraps its default slot with that left margin.
+
+#### Scenario: WeChat environment
+- **WHEN** the app runs in WeChat mini-program
+- **THEN** the component uses `uni.getMenuButtonBoundingClientRect()` and `uni.getSystemInfoSync()` to compute `safeWidth = screenWidth - menuButton.left + 8px`, and applies `margin-left: safeWidth` to the wrapped content
+
+#### Scenario: Non-WeChat environment
+- **WHEN** the app runs on H5 or App
+- **THEN** the component returns a safe width of `0` so the layout remains unchanged
+
+#### Scenario: API failure fallback
+- **WHEN** `uni.getMenuButtonBoundingClientRect()` fails or returns invalid data
+- **THEN** the component falls back to a conservative estimate based on `screenWidth` (approximately 28% of screen width) so the layout remains safe on common devices
+
+### Requirement: TopBar right slot is protected by capsule safe area
+The system SHALL wrap the `TopBar` right slot inside `CapsuleSafeArea` so any right-side action buttons are shifted left of the capsule automatically. Pages using `TopBar` shall not need to manually add capsule-safe spacing.
+
+#### Scenario: Right slot contains a button
+- **WHEN** a page places a button in the `TopBar` right slot
+- **THEN** the button is rendered with a left margin equal to the capsule safe width and remains fully clickable
+
+### Requirement: Home page notification bell avoids capsule
+The system SHALL ensure the notification bell on `pages/home/home` (all roles) does not overlap the WeChat capsule by rendering the home header with `TopBar` and placing the bell inside the `TopBar` right slot.
+
+#### Scenario: Dispatch home renders notification icon
+- **WHEN** `DispatchHome.vue` renders the top-right notification bell
+- **THEN** the header uses `TopBar`, the left slot keeps the existing avatar/greeting, and the bell is placed inside the `TopBar` right slot and is not covered by the capsule
+
+### Requirement: Task page add button avoids capsule
+The system SHALL ensure the "新增/发布" button on `pages/task/task` does not overlap the WeChat capsule by rendering the task list header with `TopBar` and placing the button inside the `TopBar` right slot.
+
+#### Scenario: TaskListView renders add button
+- **WHEN** `TaskListView.vue` renders the top-right add button
+- **THEN** the header uses `TopBar` with the page title, and the button is placed inside the `TopBar` right slot and is not covered by the capsule
+
+### Requirement: Schedule page actions avoid capsule
+The system SHALL ensure the "今天" and "新增排班" buttons on `pages/schedule/schedule` do not overlap the WeChat capsule by placing both buttons inside the `TopBar` right slot.
+
+#### Scenario: ScheduleView renders top actions
+- **WHEN** `ScheduleView.vue` renders the top-right action buttons
+- **THEN** both "今天" and "新增排班" buttons are placed inside the `TopBar` right slot and are not covered by the capsule, and the month-switching row below `TopBar` only keeps the month navigation on the left
+
+### Requirement: Build passes and layout remains valid on non-WeChat platforms
+The system SHALL keep `npm run build:mp-weixin` and `npm run type-check` passing, and non-WeChat layouts must not regress.
+
+#### Scenario: Build verification
+- **WHEN** the project is built for WeChat or type-checked
+- **THEN** no TypeScript or template errors are introduced
+```
+

+ 6 - 4
openspec/changes/weapp-capsule-safearea/.comet/subagent-progress.md

@@ -7,9 +7,9 @@
 
 ## Current Task
 
-- Task: Task 7: 改造排班页 ScheduleView.vue
-- Plan Task Text: "Task 7: 改造排班页 ScheduleView.vue"
-- OpenSpec Task Text: "2.3 Update `src/components/dispatch/ScheduleView.vue` to place "今天" and "新增排班" buttons inside `TopBar` right slot."
+- Task: Task 8: 类型检查与构建验证
+- Plan Task Text: "Task 8: 类型检查与构建验证"
+- OpenSpec Task Text: "3.1 Run `npm run type-check` and fix any TypeScript errors." / "3.2 Run `npm run build:mp-weixin` and confirm the build succeeds."
 - Stage: implementing
 - Review-Fix Round: 0
 
@@ -21,11 +21,13 @@
 - Task 4: 改造销售端首页 SalesHome.vue(commit 199700c)
 - Task 5: 改造施工端首页 ConstructionHome.vue(commit 4a57952)
 - Task 6: 改造任务列表页 TaskListView.vue(commit ffa0218)
+- Task 7: 改造排班页 ScheduleView.vue(commit 57fee7b)
 
 ## Pending Tasks
 
-- Task 8: 类型检查与构建验证
+
 
 ## Notes
 
 - 任务之间禁止暂停,连续执行。
+- Task 8 是最终验证任务。

+ 2 - 0
openspec/changes/weapp-capsule-safearea/.openspec.yaml

@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-06-24

+ 66 - 0
openspec/changes/weapp-capsule-safearea/design.md

@@ -0,0 +1,66 @@
+## Context
+
+清道夫微信小程序使用 `navigationStyle: custom` 实现自定义导航栏。所有页面顶部的标题、返回按钮、功能按钮(通知、新增、筛选等)都由组件自行渲染。然而微信小程序右上角存在系统胶囊按钮(菜单 + 关闭),在 iPhone 上宽度约 87px、安卓上约 96px,且距离屏幕右侧有一定边距。当前部分页面把操作按钮直接放在右上角,导致真机运行时胶囊遮挡按钮。
+
+已有组件:`StatusBar.vue` 负责状态栏占位,`TopBar.vue` 负责标题栏。两者均未考虑胶囊区域。
+
+## Goals / Non-Goals
+
+**Goals:**
+- 提供可在任何页面/组件复用的胶囊安全区方案。
+- 改造 `TopBar.vue`,使其右侧插槽内容自动避让胶囊。
+- 修复 `pages/home/home`、`pages/schedule/schedule`、`pages/task/task` 的遮挡问题。
+- 保持 H5/App 等非微信环境下的正常显示( graceful fallback)。
+
+**Non-Goals:**
+- 不调整底部 tabBar 结构。
+- 不改页面整体布局风格。
+- 不处理胶囊以外的系统 UI(如灵动岛、底部横条)。
+
+## Decisions
+
+### 1. 新增 `CapsuleSafeArea.vue` 组件
+- 通过 `uni.getMenuButtonBoundingClientRect()` 获取胶囊坐标与宽度。
+- 通过 `uni.getSystemInfoSync()` 获取屏幕宽度。
+- 计算右侧需要避让的最小宽度:`screenWidth - menuButton.left + 8px`(额外 8px 缓冲)。
+- 在微信小程序下渲染一个占位 view;在其他平台返回 0 或固定值,避免影响布局。
+- 提供 `width` 插槽 prop 或默认 slot,方便 `TopBar` 直接使用。
+
+### 2. `TopBar.vue` 集成安全区
+- 右侧 `<slot name="right" />` 外层用 `CapsuleSafeArea` 包裹,使右侧内容左侧平移。
+- 保持标题居左或根据左侧内容自适应,不改动标题样式。
+- 为避免标题被挤压过窄,标题区使用 `flex-1 min-w-0 truncate`。
+
+### 3. 页面顶部操作按钮统一使用 `TopBar`
+- `pages/home/home` 三个首页当前直接在页面内写 header,需把右上角通知铃铛移入 `TopBar` 的 right slot,或给按钮加 `mr-[safeWidth]`。
+- `pages/task/task` 的 `TaskListView` 顶部「新增」按钮纳入 `TopBar` right slot。
+- `pages/schedule/schedule` 的 `ScheduleView` 顶部「今天」「新增排班」按钮纳入 `TopBar` right slot。
+
+### 4. 策略选择:右侧按钮整体左移
+- 对比方案:
+  - 按钮换行:改动大,破坏视觉一致性。
+  - 右侧留空白:浪费空间且用户看不到原因。
+  - 按钮整体左移:改动最小,符合微信小程序主流做法,保留按钮可点击区域。
+- 选定「按钮整体左移」。
+
+## Risks / Trade-offs
+
+- **[Risk]** `uni.getMenuButtonBoundingClientRect()` 在小程序初始化后可能异步变化(如旋转屏幕)。
+  - **Mitigation:** 在 `onMounted` 获取并监听窗口尺寸变化;如获取失败则使用基于屏幕宽度的默认估算值。
+- **[Risk]** 不同机型胶囊尺寸差异大。
+  - **Mitigation:** 使用动态 API 获取真实尺寸,而非写死 px。
+- **[Risk]** 顶部标题栏空间被压缩,标题可能显示不全。
+  - **Mitigation:** 标题使用 `truncate` 截断,并确保左侧返回按钮只在需要时显示。
+- **[Trade-off]** H5/App 下无胶囊,安全区宽度为 0,布局与之前一致。
+
+## Migration Plan
+
+- 新增 `CapsuleSafeArea.vue`。
+- 修改 `TopBar.vue`,集成安全区。
+- 修改三个页面/视图组件,使右上角按钮通过 `TopBar` right slot 渲染。
+- 构建 `npm run build:mp-weixin` 验证无编译错误。
+- 在微信开发者工具真机调试中检查胶囊未遮挡按钮。
+
+## Open Questions
+
+- 是否需要在每个页面单独处理 `StatusBar + TopBar`,还是统一封装一个 `PageHeader`?本次保持最小改动,只改 `TopBar`。

+ 30 - 0
openspec/changes/weapp-capsule-safearea/proposal.md

@@ -0,0 +1,30 @@
+## Why
+
+微信小程序右上角存在原生胶囊按钮(菜单/关闭),宽度在不同机型下约为 87px。当前项目所有页面均使用 `navigationStyle: custom` 自定义导航栏,并把部分操作按钮(通知、新增、今天等)放在右上角,导致部署到微信真机后胶囊遮挡功能按钮,影响使用。需要引入全局统一的胶囊安全区组件,让顶部栏右侧操作区自动避让胶囊。
+
+## What Changes
+
+- 新增全局胶囊安全区组件 `CapsuleSafeArea`,基于 `uni.getMenuButtonBoundingClientRect()` 计算右侧安全宽度。
+- 改造 `TopBar` 组件,右侧插槽自动包裹在安全区内,保证操作按钮不会被胶囊遮挡。
+- 调整三个已确认存在遮挡问题的页面顶部区域:
+  - `pages/home/home`(各角色首页右上角通知铃铛)
+  - `pages/schedule/schedule`(排班页右上角「今天」「新增排班」)
+  - `pages/task/task`(任务页右上角「新增/发布」按钮)
+- 胶囊安全区采用「右侧操作按钮整体左移」策略,改动最小且保持按钮可点。
+- 非微信环境(H5/App)回退到默认右侧间距,避免异常。
+
+## Capabilities
+
+### New Capabilities
+
+- `weapp-capsule-safearea`: 微信小程序胶囊安全区适配,提供全局组件和 TopBar 集成。
+
+### Modified Capabilities
+
+- 无
+
+## Impact
+
+- 受影响的文件:`src/components/common/TopBar.vue`、新增 `src/components/common/CapsuleSafeArea.vue`、`src/pages/home/home.vue`、`src/pages/schedule/schedule.vue`、`src/pages/task/task.vue`。
+- 依赖:仅使用 uni-app 内置 API `uni.getMenuButtonBoundingClientRect()` 与 `uni.getSystemInfoSync()`。
+- 不涉及后端接口、数据模型、API 变更。

+ 51 - 0
openspec/changes/weapp-capsule-safearea/specs/weapp-capsule-safearea/spec.md

@@ -0,0 +1,51 @@
+## ADDED Requirements
+
+### Requirement: CapsuleSafeArea component calculates safe margin
+The system SHALL provide a `CapsuleSafeArea` component that computes the minimum right-side margin needed to avoid the WeChat mini-program capsule button, and wraps its default slot with that left margin.
+
+#### Scenario: WeChat environment
+- **WHEN** the app runs in WeChat mini-program
+- **THEN** the component uses `uni.getMenuButtonBoundingClientRect()` and `uni.getSystemInfoSync()` to compute `safeWidth = screenWidth - menuButton.left + 8px`, and applies `margin-left: safeWidth` to the wrapped content
+
+#### Scenario: Non-WeChat environment
+- **WHEN** the app runs on H5 or App
+- **THEN** the component returns a safe width of `0` so the layout remains unchanged
+
+#### Scenario: API failure fallback
+- **WHEN** `uni.getMenuButtonBoundingClientRect()` fails or returns invalid data
+- **THEN** the component falls back to a conservative estimate based on `screenWidth` (approximately 28% of screen width) so the layout remains safe on common devices
+
+### Requirement: TopBar right slot is protected by capsule safe area
+The system SHALL wrap the `TopBar` right slot inside `CapsuleSafeArea` so any right-side action buttons are shifted left of the capsule automatically. Pages using `TopBar` shall not need to manually add capsule-safe spacing.
+
+#### Scenario: Right slot contains a button
+- **WHEN** a page places a button in the `TopBar` right slot
+- **THEN** the button is rendered with a left margin equal to the capsule safe width and remains fully clickable
+
+### Requirement: Home page notification bell avoids capsule
+The system SHALL ensure the notification bell on `pages/home/home` (all roles) does not overlap the WeChat capsule by rendering the home header with `TopBar` and placing the bell inside the `TopBar` right slot.
+
+#### Scenario: Dispatch home renders notification icon
+- **WHEN** `DispatchHome.vue` renders the top-right notification bell
+- **THEN** the header uses `TopBar`, the left slot keeps the existing avatar/greeting, and the bell is placed inside the `TopBar` right slot and is not covered by the capsule
+
+### Requirement: Task page add button avoids capsule
+The system SHALL ensure the "新增/发布" button on `pages/task/task` does not overlap the WeChat capsule by rendering the task list header with `TopBar` and placing the button inside the `TopBar` right slot.
+
+#### Scenario: TaskListView renders add button
+- **WHEN** `TaskListView.vue` renders the top-right add button
+- **THEN** the header uses `TopBar` with the page title, and the button is placed inside the `TopBar` right slot and is not covered by the capsule
+
+### Requirement: Schedule page actions avoid capsule
+The system SHALL ensure the "今天" and "新增排班" buttons on `pages/schedule/schedule` do not overlap the WeChat capsule by placing both buttons inside the `TopBar` right slot.
+
+#### Scenario: ScheduleView renders top actions
+- **WHEN** `ScheduleView.vue` renders the top-right action buttons
+- **THEN** both "今天" and "新增排班" buttons are placed inside the `TopBar` right slot and are not covered by the capsule, and the month-switching row below `TopBar` only keeps the month navigation on the left
+
+### Requirement: Build passes and layout remains valid on non-WeChat platforms
+The system SHALL keep `npm run build:mp-weixin` and `npm run type-check` passing, and non-WeChat layouts must not regress.
+
+#### Scenario: Build verification
+- **WHEN** the project is built for WeChat or type-checked
+- **THEN** no TypeScript or template errors are introduced

Разлика између датотеке није приказан због своје велике величине
+ 625 - 216
package-lock.json


+ 7 - 4
package.json

@@ -9,11 +9,12 @@
     "build:custom": "uni build -p",
     "build:h5": "uni build",
     "build:h5:ssr": "uni build --ssr",
-    "build:mp-weixin": "uni build -p mp-weixin",
+    "build:mp-weixin": "uni build -p mp-weixin && npm run check:wxss",
+    "check:wxss": "node scripts/check-wxss-escapes.js",
     "type-check": "vue-tsc --noEmit"
   },
   "dependencies": {
-    "@dcloudio/uni-app": "3.0.0-4080420251103001",
+    "@dcloudio/uni-app": "^3.0.0-4080420251103001",
     "@dcloudio/uni-app-harmony": "3.0.0-4080420251103001",
     "@dcloudio/uni-app-plus": "3.0.0-4080420251103001",
     "@dcloudio/uni-components": "3.0.0-4080420251103001",
@@ -26,7 +27,7 @@
     "@dcloudio/uni-mp-lark": "3.0.0-4080420251103001",
     "@dcloudio/uni-mp-qq": "3.0.0-4080420251103001",
     "@dcloudio/uni-mp-toutiao": "3.0.0-4080420251103001",
-    "@dcloudio/uni-mp-weixin": "3.0.0-4080420251103001",
+    "@dcloudio/uni-mp-weixin": "^3.0.0-4080420251103001",
     "@dcloudio/uni-mp-xhs": "3.0.0-4080420251103001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-4080420251103001",
     "pinia": "^2.3.1",
@@ -39,9 +40,10 @@
     "@dcloudio/uni-automator": "3.0.0-4080420251103001",
     "@dcloudio/uni-cli-shared": "3.0.0-4080420251103001",
     "@dcloudio/uni-stacktracey": "3.0.0-4080420251103001",
-    "@dcloudio/vite-plugin-uni": "3.0.0-4080420251103001",
+    "@dcloudio/vite-plugin-uni": "^3.0.0-4080420251103001",
     "@types/node": "^26.0.0",
     "@vue/runtime-core": "^3.4.21",
+    "@vue/test-utils": "^2.4.11",
     "@vue/tsconfig": "^0.1.3",
     "autoprefixer": "^10.5.0",
     "postcss": "^8.5.15",
@@ -49,6 +51,7 @@
     "tailwindcss": "^3.4.19",
     "typescript": "^4.9.4",
     "vite": "5.2.8",
+    "vitest": "^4.1.9",
     "vue-tsc": "^1.0.24"
   },
   "description": "清道夫系统 - 环境服务管理平台"

+ 36 - 0
scripts/check-wxss-escapes.js

@@ -0,0 +1,36 @@
+/**
+ * 检查微信小程序生成的 app.wxss 中是否包含 WXSS 不兼容的 CSS 选择器转义。
+ * Tailwind 的小数点类(h-1.5)、分数类(w-1/2)、伪类变体(last:)等
+ * 会生成带反斜杠转义的选择器,微信 WXSS 解析会报错。
+ */
+const fs = require('fs')
+const path = require('path')
+
+const wxssPath = path.resolve(__dirname, '../dist/build/mp-weixin/app.wxss')
+
+if (!fs.existsSync(wxssPath)) {
+  console.error(`[check-wxss] 未找到 ${wxssPath},请先执行构建`)
+  process.exit(1)
+}
+
+const content = fs.readFileSync(wxssPath, 'utf-8')
+
+// 匹配以 . 开头、类名中包含反斜杠转义的选择器
+// 例如 .h-1\.5、.w-1\/2、.last\:border-b-0
+const escapedClassSelector = /\.[a-zA-Z0-9_-]*\\[.:]/g
+const matches = content.match(escapedClassSelector) || []
+
+// 排除常见的 base64/inline 数据中的反斜杠(它们通常不在选择器位置)
+const suspicious = matches.filter((m) => !m.startsWith('.data:') && !m.includes('\\/\\/'))
+
+if (suspicious.length > 0) {
+  console.error('[check-wxss] 发现 WXSS 不兼容的转义选择器:')
+  suspicious.slice(0, 20).forEach((m) => console.error('  -', m))
+  if (suspicious.length > 20) {
+    console.error(`  ... 还有 ${suspicious.length - 20} 处`)
+  }
+  console.error('[check-wxss] 请检查源码中是否使用了小数点类、分数类或 last:/hover: 等变体。')
+  process.exit(1)
+}
+
+console.log('[check-wxss] app.wxss 未发现 WXSS 不兼容的转义选择器')

+ 61 - 0
scripts/generate-tabbar-icons.py

@@ -0,0 +1,61 @@
+from PIL import Image, ImageDraw
+
+ICON_SIZE = 81
+OUTPUT_DIR = "D:/work/绿水青山/清道夫App/src/static/tabbar"
+
+# 颜色
+NORMAL_BG = "#f3f4f6"
+NORMAL_FG = "#9ca3af"
+ACTIVE_BG = "#eff6ff"
+ACTIVE_FG = "#3b82f6"
+
+def draw_rounded_rect(draw, xy, radius, fill):
+    x1, y1, x2, y2 = xy
+    draw.rounded_rectangle(xy, radius=radius, fill=fill)
+
+def draw_home(img, color):
+    draw = ImageDraw.Draw(img)
+    # 房子主体
+    draw.polygon([(40, 20), (20, 40), (60, 40)], fill=color)
+    draw.rectangle([25, 40, 55, 62], fill=color)
+    # 门
+    draw.rectangle([35, 50, 45, 62], fill="white")
+
+def draw_task(img, color):
+    draw = ImageDraw.Draw(img)
+    # 旗帜
+    draw.rectangle([38, 18, 42, 58], fill=color)
+    draw.polygon([(42, 18), (58, 26), (42, 34)], fill=color)
+    # 底座
+    draw.ellipse([32, 58, 48, 62], fill=color)
+
+def draw_my(img, color):
+    draw = ImageDraw.Draw(img)
+    # 头
+    draw.ellipse([30, 18, 50, 38], fill=color)
+    # 身体
+    draw.rounded_rectangle([24, 40, 56, 66], radius=8, fill=color)
+
+def create_icon(name, active=False):
+    bg = ACTIVE_BG if active else NORMAL_BG
+    fg = ACTIVE_FG if active else NORMAL_FG
+    img = Image.new("RGBA", (ICON_SIZE, ICON_SIZE), (255, 255, 255, 0))
+    draw = ImageDraw.Draw(img)
+    # 背景圆角方块
+    draw.rounded_rectangle([4, 4, ICON_SIZE-4, ICON_SIZE-4], radius=16, fill=bg)
+
+    if name == "home":
+        draw_home(img, fg)
+    elif name == "task":
+        draw_task(img, fg)
+    elif name == "my":
+        draw_my(img, fg)
+
+    suffix = "-active" if active else ""
+    img.save(f"{OUTPUT_DIR}/{name}{suffix}.png", "PNG")
+
+if __name__ == "__main__":
+    for name in ["home", "task", "my"]:
+        create_icon(name, active=False)
+        create_icon(name, active=True)
+    print("tabBar icons generated.")

+ 25 - 0
scripts/rename-subpackages-back.py

@@ -0,0 +1,25 @@
+import os
+
+ROOT = "D:/work/绿水青山/清道夫App/src"
+
+for dirpath, _, filenames in os.walk(ROOT):
+    for filename in filenames:
+        if filename.endswith((".vue", ".ts", ".js", ".json")):
+            filepath = os.path.join(dirpath, filename)
+            try:
+                with open(filepath, "r", encoding="utf-8") as f:
+                    content = f.read()
+                # 只把源码路径中的 subpackages/ 改回 subPackages/
+                # 但 pages.json 里的 root 和 preloadRule 也要改回 subPackages/
+                new_content = content.replace("/subpackages/", "/subPackages/")
+                # 避免把 pages.json 的 "subPackages": [ 字段名改掉(上面替换不影响,因为没有斜杠)
+                # 但 root 里的 subpackages/pages-common 需要改成 subPackages/pages-common
+                new_content = new_content.replace("subpackages/pages-", "subPackages/pages-")
+                if new_content != content:
+                    with open(filepath, "w", encoding="utf-8") as f:
+                        f.write(new_content)
+                    print(f"Updated: {filepath}")
+            except Exception as e:
+                print(f"Skip {filepath}: {e}")
+
+print("Done")

+ 42 - 0
scripts/rename-subpackages.py

@@ -0,0 +1,42 @@
+import os
+import json
+
+ROOT = "D:/work/绿水青山/清道夫App/src"
+
+# 1. 批量替换文件内容中的路径
+for dirpath, _, filenames in os.walk(ROOT):
+    for filename in filenames:
+        if filename.endswith((".vue", ".ts", ".js", ".json")):
+            filepath = os.path.join(dirpath, filename)
+            try:
+                with open(filepath, "r", encoding="utf-8") as f:
+                    content = f.read()
+                new_content = content.replace("subPackages/", "subpackages/")
+                new_content = new_content.replace("subPackages", "subpackages")
+                if new_content != content:
+                    with open(filepath, "w", encoding="utf-8") as f:
+                        f.write(new_content)
+                    print(f"Updated content: {filepath}")
+            except Exception as e:
+                print(f"Skip {filepath}: {e}")
+
+# 2. 单独处理 pages.json 的 subPackages 字段名需要保留(uni-app 配置字段名必须叫 subPackages)
+pages_json = "D:/work/绿水青山/清道夫App/src/pages.json"
+with open(pages_json, "r", encoding="utf-8") as f:
+    data = json.load(f)
+
+# root 路径改为 subpackages
+for pkg in data.get("subPackages", []):
+    if "root" in pkg:
+        pkg["root"] = pkg["root"].replace("subPackages", "subpackages")
+
+# preloadRule 里的 packages 也改
+for rule in data.get("preloadRule", {}).values():
+    if "packages" in rule:
+        rule["packages"] = [p.replace("subPackages", "subpackages") for p in rule["packages"]]
+
+with open(pages_json, "w", encoding="utf-8") as f:
+    json.dump(data, f, ensure_ascii=False, indent=2)
+print("Updated pages.json root paths")
+
+print("Done")

+ 128 - 0
scripts/update-paths.py

@@ -0,0 +1,128 @@
+import os
+import re
+
+ROOT = "D:/work/绿水青山/清道夫App/src"
+
+# 路径替换映射:旧路径 -> 新路径
+PATH_REPLACEMENTS = {
+    "/pages/common/projectList": "/subPackages/pages-common/projectList",
+    "/pages/common/projectDetail": "/subPackages/pages-common/projectDetail",
+    "/pages/common/addProject": "/subPackages/pages-common/addProject",
+    "/pages/common/publishTask": "/subPackages/pages-common/publishTask",
+    "/pages/common/taskDetail": "/subPackages/pages-common/taskDetail",
+    "/pages/common/changePassword": "/subPackages/pages-common/changePassword",
+    "/pages/common/noticeList": "/subPackages/pages-common/noticeList",
+    "/pages/common/noticeDetail": "/subPackages/pages-common/noticeDetail",
+    "/pages/common/messageList": "/subPackages/pages-common/messageList",
+    "/pages/common/messageDetail": "/subPackages/pages-common/messageDetail",
+    "/pages/common/helpCenter": "/subPackages/pages-common/helpCenter",
+    "/pages/common/knowledgeList": "/subPackages/pages-common/knowledgeList",
+    "/pages/common/knowledgeDetail": "/subPackages/pages-common/knowledgeDetail",
+    "/pages/sales/taskReminder": "/subPackages/pages-sales/taskReminder",
+    "/pages/sales/notification": "/subPackages/pages-sales/notification",
+    "/pages/sales/knowledgeList": "/subPackages/pages-sales/knowledgeList",
+    "/pages/sales/knowledgeDetail": "/subPackages/pages-sales/knowledgeDetail",
+    "/pages/sales/messageList": "/subPackages/pages-sales/messageList",
+    "/pages/sales/messageDetail": "/subPackages/pages-sales/messageDetail",
+    "/pages/dispatch/schedule": "/subPackages/pages-dispatch/schedule",
+    "/pages/dispatch/taskDetail": "/subPackages/pages-dispatch/taskDetail",
+    "/pages/dispatch/publishTask": "/subPackages/pages-dispatch/publishTask",
+    "/pages/dispatch/vehicleList": "/subPackages/pages-dispatch/vehicleList",
+    "/pages/dispatch/availableVehicles": "/subPackages/pages-dispatch/availableVehicles",
+    "/pages/dispatch/personnel": "/subPackages/pages-dispatch/personnel",
+    "/pages/dispatch/onDutyStaff": "/subPackages/pages-dispatch/onDutyStaff",
+    "/pages/dispatch/emergencyTeam": "/subPackages/pages-dispatch/emergencyTeam",
+    "/pages/dispatch/emergencyStandby": "/subPackages/pages-dispatch/emergencyStandby",
+    "/pages/dispatch/visualization": "/subPackages/pages-dispatch/visualization",
+    "/pages/dispatch/noticeList": "/subPackages/pages-dispatch/noticeList",
+    "/pages/dispatch/noticeDetail": "/subPackages/pages-dispatch/noticeDetail",
+    "/pages/dispatch/notification": "/subPackages/pages-dispatch/notification",
+    "/pages/construction/taskDetail": "/subPackages/pages-construction/taskDetail",
+    "/pages/construction/dailyTask": "/subPackages/pages-construction/dailyTask",
+    "/pages/construction/historyTask": "/subPackages/pages-construction/historyTask",
+    "/pages/construction/tomorrowTask": "/subPackages/pages-construction/tomorrowTask",
+    "/pages/construction/noticeList": "/subPackages/pages-construction/noticeList",
+    "/pages/construction/noticeDetail": "/subPackages/pages-construction/noticeDetail",
+    "/pages/construction/notification": "/subPackages/pages-construction/notification",
+    # 被删除/合并的页面统一指向 common
+    "/pages/sales/changePassword": "/subPackages/pages-common/changePassword",
+    "/pages/sales/helpCenter": "/subPackages/pages-common/helpCenter",
+    "/pages/dispatch/changePassword": "/subPackages/pages-common/changePassword",
+    "/pages/dispatch/helpCenter": "/subPackages/pages-common/helpCenter",
+    "/pages/construction/changePassword": "/subPackages/pages-common/changePassword",
+    "/pages/construction/helpCenter": "/subPackages/pages-common/helpCenter",
+    # tabBar 页
+    "/pages/common/taskList": "/pages/task/task",
+    "/pages/common/profile": "/pages/my/my",
+    "/pages/sales/home": "/pages/home/home",
+    "/pages/dispatch/home": "/pages/home/home",
+    "/pages/dispatch/taskList": "/pages/task/task",
+    "/pages/dispatch/myPage": "/pages/my/my",
+    "/pages/construction/home": "/pages/home/home",
+    "/pages/construction/taskList": "/pages/task/task",
+    "/pages/construction/myPage": "/pages/my/my",
+}
+
+# tabBar 目标路径,需要把 navigateTo/reLaunch 改为 switchTab
+TABBAR_PATHS = [
+    "/pages/home/home",
+    "/pages/task/task",
+    "/pages/my/my",
+]
+
+# 扫描的文件后缀
+EXTS = (".vue", ".ts", ".js")
+
+
+def replace_paths(content: str) -> str:
+    # 1. 替换路径字符串
+    for old, new in PATH_REPLACEMENTS.items():
+        content = content.replace(old, new)
+    return content
+
+
+def fix_tabbar_navigation(content: str) -> str:
+    # 把指向 tabBar 页面的 navigateTo/reLaunch 改成 switchTab
+    # 匹配 uni.navigateTo({ url: '...' }) 或 uni.reLaunch({ url: '...' })
+    pattern = re.compile(
+        r"uni\.(navigateTo|reLaunch)\(\{\s*url\s*:\s*['\"]([" + "".join(re.escape(p) for p in TABBAR_PATHS) + r"][^'\"]*)['\"]\s*\}\)"
+    )
+
+    def replacer(match):
+        method = match.group(1)
+        url = match.group(2)
+        return f"uni.switchTab({{ url: '{url}' }})"
+
+    # 由于 TABBAR_PATHS 是固定的简单字符串,直接用更简单的方式
+    for path in TABBAR_PATHS:
+        for method in ("navigateTo", "reLaunch"):
+            content = re.sub(
+                rf"uni\.{method}\(\{{\s*url\s*:\s*['\"]{re.escape(path)}[^'\"]*['\"]\s*\}}\)",
+                lambda m, p=path: f"uni.switchTab({{ url: '{p}' }})",
+                content,
+            )
+    return content
+
+
+def process_file(filepath: str):
+    with open(filepath, "r", encoding="utf-8") as f:
+        content = f.read()
+
+    new_content = replace_paths(content)
+    new_content = fix_tabbar_navigation(new_content)
+
+    if new_content != content:
+        with open(filepath, "w", encoding="utf-8") as f:
+            f.write(new_content)
+        print(f"Updated: {filepath}")
+
+
+def main():
+    for dirpath, _, filenames in os.walk(ROOT):
+        for filename in filenames:
+            if filename.endswith(EXTS):
+                process_file(os.path.join(dirpath, filename))
+
+
+if __name__ == "__main__":
+    main()

+ 9 - 0
src/api/auth.ts

@@ -13,6 +13,15 @@ export interface LoginResult {
   nickName: string
   roleType: string
   avatar: string
+  phonenumber?: string
+  phone?: string
+  email?: string
+  dept?: { deptName?: string; deptId?: number; name?: string }
+  position?: string
+  employeeNo?: string
+  hireDate?: string
+  team?: string
+  department?: string
 }
 
 export function login(params: LoginParams) {

+ 2 - 0
src/api/project.ts

@@ -6,6 +6,8 @@ export interface ProjectParams {
   customerName: string
   customerPhone: string
   address: string
+  servicePoints?: { address?: string; contactName?: string; contactPhone?: string }[]
+  contacts?: { name?: string; phone?: string; role?: string }[]
 }
 
 export function getProjectList(pageNum = 1, pageSize = 10) {

+ 0 - 69
src/components/common/BottomNav.vue

@@ -1,69 +0,0 @@
-<template>
-  <view class="bottom-nav fixed bottom-0 left-0 right-0 bg-white z-50 safe-bottom">
-    <view class="max-w-app mx-auto flex justify-around items-center h-14">
-      <view
-        v-for="item in items"
-        :key="item.path"
-        class="nav-item flex flex-col items-center justify-center flex-1 py-1 clickable"
-        :class="{ active: isActive(item.path) }"
-        @click="onNavClick(item.path)"
-      >
-        <view
-          class="nav-icon w-10 h-10 rounded-xl flex items-center justify-center mb-0.5"
-          :class="isActive(item.path) ? 'bg-primary-light' : ''"
-        >
-          <text
-            class="uni-icons text-xl"
-            :class="[
-              isActive(item.path) ? 'text-primary' : 'text-gray-400',
-              isActive(item.path) && item.activeIcon ? item.activeIcon : item.icon,
-            ]"
-          />
-        </view>
-        <text
-          class="text-xs"
-          :class="isActive(item.path) ? 'text-primary font-medium' : 'text-gray-400'"
-        >
-          {{ item.text || item.name }}
-        </text>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue'
-import type { NavItem } from '@/types'
-
-const props = defineProps<{
-  items: NavItem[]
-  currentPath: string
-}>()
-
-const emit = defineEmits<{
-  (e: 'navigate', path: string): void
-}>()
-
-const activeIndex = computed(() =>
-  props.items.findIndex((item) => isActive(item.path))
-)
-
-function isActive(path: string): boolean {
-  return props.currentPath === path || props.currentPath.startsWith(path + '/')
-}
-
-function onNavClick(path: string) {
-  if (isActive(path)) return
-  emit('navigate', path)
-}
-</script>
-
-<style scoped>
-.bottom-nav {
-  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
-}
-
-.nav-item.active .nav-icon {
-  background: #eff6ff;
-}
-</style>

+ 69 - 0
src/components/common/CapsuleSafeArea.test.ts

@@ -0,0 +1,69 @@
+export {}
+
+// Test for CapsuleSafeArea safeWidth calculation logic
+// This is a simple unit test to verify the calculation behavior
+
+interface MenuButtonRect {
+  left: number
+  top?: number
+  right?: number
+  bottom?: number
+  width?: number
+  height?: number
+}
+
+interface SystemInfo {
+  screenWidth: number
+  screenHeight?: number
+  statusBarHeight?: number
+}
+
+// Simulate the calculation logic from CapsuleSafeArea.vue
+function calculateSafeWidth(systemInfo: SystemInfo, menuButton: MenuButtonRect | null): number {
+  if (systemInfo.screenWidth && menuButton && menuButton.left != null) {
+    return systemInfo.screenWidth - menuButton.left + 8
+  } else {
+    return Math.round(systemInfo.screenWidth * 0.28)
+  }
+}
+
+// Test cases
+function runTests() {
+  console.log('Running CapsuleSafeArea calculation tests...\n')
+
+  // Test 1: WeChat environment with valid data
+  const test1 = calculateSafeWidth({ screenWidth: 375 }, { left: 280 })
+  const expected1 = 375 - 280 + 8 // 103
+  console.log(`Test 1 - WeChat environment: ${test1 === expected1 ? 'PASS' : 'FAIL'}`)
+  console.log(`  Expected: ${expected1}, Got: ${test1}`)
+
+  // Test 2: Non-WeChat environment (no menuButton)
+  const test2 = calculateSafeWidth({ screenWidth: 375 }, null)
+  const expected2 = Math.round(375 * 0.28) // 105
+  console.log(`Test 2 - Non-WeChat environment: ${test2 === expected2 ? 'PASS' : 'FAIL'}`)
+  console.log(`  Expected: ${expected2}, Got: ${test2}`)
+
+  // Test 3: API failure fallback
+  const test3 = calculateSafeWidth({ screenWidth: 414 }, null)
+  const expected3 = Math.round(414 * 0.28) // 116
+  console.log(`Test 3 - API failure fallback: ${test3 === expected3 ? 'PASS' : 'FAIL'}`)
+  console.log(`  Expected: ${expected3}, Got: ${test3}`)
+
+  // Test 4: Edge case - small screen
+  const test4 = calculateSafeWidth({ screenWidth: 320 }, { left: 240 })
+  const expected4 = 320 - 240 + 8 // 88
+  console.log(`Test 4 - Small screen: ${test4 === expected4 ? 'PASS' : 'FAIL'}`)
+  console.log(`  Expected: ${expected4}, Got: ${test4}`)
+
+  // Test 5: Edge case - menuButton at left edge
+  const test5 = calculateSafeWidth({ screenWidth: 375 }, { left: 0 })
+  const expected5 = 375 - 0 + 8 // 383
+  console.log(`Test 5 - Menu button at edge: ${test5 === expected5 ? 'PASS' : 'FAIL'}`)
+  console.log(`  Expected: ${expected5}, Got: ${test5}`)
+
+  const allPass = test1 === expected1 && test2 === expected2 && test3 === expected3 && test4 === expected4 && test5 === expected5
+  console.log(`\n${allPass ? 'All tests PASSED' : 'Some tests FAILED'}`)
+  return allPass
+}
+
+runTests()

+ 2 - 2
src/components/common/ChartView.vue

@@ -43,8 +43,8 @@ function drawChart() {
   ctx = uni.createCanvasContext(props.canvasId)
   if (!ctx) return
 
-  const { width } = uni.getSystemInfoSync()
-  const canvasWidth = width - 32 // 减去padding
+  const { windowWidth } = uni.getSystemInfoSync()
+  const canvasWidth = windowWidth - 32 // 减去padding
   const canvasHeight = props.height
 
   ctx.clearRect(0, 0, canvasWidth, canvasHeight)

+ 5 - 5
src/components/common/FilterTabs.vue

@@ -49,16 +49,16 @@ function onSelect(value: string) {
   padding: 6px 16px;
   border-radius: 20px;
   font-size: 13px;
-  color: #666;
-  background: #f5f5f5;
-  border: 1px solid #e5e7eb;
+  background: $surface;
+  color: $text-secondary;
+  border: 1px solid $border-color;
   transition: all 0.2s ease;
 }
 
 .filter-tab.active {
-  background: #3b82f6;
+  background: $primary;
   color: #fff;
-  border-color: #3b82f6;
+  border-color: $primary;
 }
 
 .filter-tab:active {

+ 2 - 2
src/components/common/Modal.vue

@@ -11,13 +11,13 @@
       <view class="modal-footer flex gap-3">
         <button
           v-if="showCancel"
-          class="flex-1 py-2.5 rounded-button border border-gray-200 text-gray-600 text-sm font-medium clickable"
+          class="flex-1 py-2 rounded-button border border-gray-200 text-gray-600 text-sm font-medium clickable"
           @click="onCancel"
         >
           {{ cancelText }}
         </button>
         <button
-          class="flex-1 py-2.5 rounded-button bg-primary text-white text-sm font-medium clickable"
+          class="flex-1 py-2 rounded-button bg-primary text-white text-sm font-medium clickable"
           @click="onConfirm"
         >
           {{ confirmText }}

+ 18 - 0
src/components/common/StatusBar.vue

@@ -0,0 +1,18 @@
+<template>
+  <view class="status-bar" :style="{ height: `${height}px` }"></view>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+
+const height = ref(0)
+
+onMounted(() => {
+  try {
+    const info = uni.getSystemInfoSync()
+    height.value = info.statusBarHeight || 0
+  } catch (e) {
+    height.value = 0
+  }
+})
+</script>

+ 57 - 0
src/components/dispatch/ScheduleView.test.ts

@@ -0,0 +1,57 @@
+import { describe, it, expect, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import ScheduleView from '../ScheduleView.vue'
+
+// Mock the API module
+vi.mock('@/api/schedule', () => ({
+  getScheduleList: vi.fn().mockResolvedValue({ rows: [] })
+}))
+
+describe('ScheduleView.vue - TDD Task 7', () => {
+  it('should have TopBar right slot with today and add-schedule buttons', () => {
+    const wrapper = mount(ScheduleView)
+    const topBar = wrapper.findComponent({ name: 'TopBar' })
+    expect(topBar.exists()).toBe(true)
+
+    // Check that TopBar has right slot content
+    const rightSlot = topBar.find('[class*="flex items-center gap-2"]')
+    expect(rightSlot.exists()).toBe(true)
+
+    // Verify "今天" button exists in right slot
+    const todayButton = rightSlot.findAll('[class*="text-blue-500"]').find(el => el.text().includes('今天'))
+    expect(todayButton).toBeDefined()
+    expect(todayButton!.exists()).toBe(true)
+
+    // Verify "新增排班" button exists in right slot
+    const addScheduleButton = rightSlot.findAll('[class*="text-blue-500"]').find(el => el.text().includes('新增排班'))
+    expect(addScheduleButton).toBeDefined()
+    expect(addScheduleButton!.exists()).toBe(true)
+  })
+
+  it('should have empty month navigation row right side (no today button)', () => {
+    const wrapper = mount(ScheduleView)
+
+    // Find the month navigation area
+    const monthNav = wrapper.findAll('[class*="bg-white px-4 py-3 flex items-center justify-between"]').at(0)
+    expect(monthNav).toBeDefined()
+    expect(monthNav!.exists()).toBe(true)
+
+    // The right side should only contain the left arrow, month text, and right arrow
+    // It should NOT contain "今天" button anymore
+    const todayButtonInMonthNav = monthNav!.findAll('[class*="text-blue-500"]').find(el => el.text().includes('今天'))
+    expect(todayButtonInMonthNav).toBeUndefined()
+  })
+
+  it('should have empty schedule list title right side (no add-schedule button)', () => {
+    const wrapper = mount(ScheduleView)
+
+    // Find the schedule list title area
+    const scheduleListTitle = wrapper.findAll('[class*="flex justify-between items-center mb-3"]').at(0)
+    expect(scheduleListTitle).toBeDefined()
+    expect(scheduleListTitle!.exists()).toBe(true)
+
+    // The right side should NOT contain "新增排班" button anymore
+    const addScheduleButtonInTitle = scheduleListTitle!.findAll('[class*="text-blue-500"]').find(el => el.text().includes('新增排班'))
+    expect(addScheduleButtonInTitle).toBeUndefined()
+  })
+})

+ 50 - 0
src/components/home/ConstructionHome.test.ts.bak

@@ -0,0 +1,50 @@
+// ConstructionHome template structure test
+// TDD: Verify TopBar usage with left/right slots
+
+import { describe, it, expect } from 'vitest'
+import { mount } from '@vue/test-utils'
+import ConstructionHome from './ConstructionHome.vue'
+
+// Since we don't have a full test runner setup, we'll create a simple
+// structural verification test that can be run manually
+
+function verifyTemplateStructure(): boolean {
+  const fs = require('fs')
+  const path = require('path')
+
+  const filePath = path.join(__dirname, 'ConstructionHome.vue')
+  const content = fs.readFileSync(filePath, 'utf-8')
+
+  const checks = {
+    hasTopBarImport: content.includes("import TopBar from '@/components/common/TopBar.vue'"),
+    usesTopBarComponent: content.includes('<TopBar'),
+    hasTitleProp: content.includes('title=""'),
+    hasLeftSlot: content.includes('<template #left>'),
+    hasRightSlot: content.includes('<template #right>'),
+    noStatusBarImport: !content.includes("import StatusBar from '@/components/common/StatusBar.vue'"),
+    noStatusBarUsage: !content.includes('<StatusBar />'),
+    hasNotificationButton: content.includes('goToNotification'),
+    hasUserInfo: content.includes('张师傅'),
+    hasPersonIcon: content.includes('uniui-person-filled'),
+    hasNotificationIcon: content.includes('uniui-notification-filled')
+  }
+
+  console.log('\n=== ConstructionHome Template Structure Verification ===\n')
+
+  let allPass = true
+  for (const [check, passed] of Object.entries(checks)) {
+    const status = passed ? 'PASS' : 'FAIL'
+    console.log(`  ${check}: ${status}`)
+    if (!passed) allPass = false
+  }
+
+  console.log(`\n${allPass ? '✓ All checks PASSED' : '✗ Some checks FAILED'}`)
+  return allPass
+}
+
+// Run the test
+if (require.main === module) {
+  verifyTemplateStructure()
+}
+
+export { verifyTemplateStructure }

+ 0 - 1
src/components/index.ts

@@ -1,5 +1,4 @@
 export { default as AppContainer } from './common/AppContainer.vue'
-export { default as BottomNav } from './common/BottomNav.vue'
 export { default as TopBar } from './common/TopBar.vue'
 export { default as Card } from './common/Card.vue'
 export { default as TaskCard } from './task/TaskCard.vue'

+ 136 - 0
src/components/my/ProfileView.vue

@@ -0,0 +1,136 @@
+<template>
+  <view class="app-container pb-20">
+    <!-- 顶部个人信息 -->
+    <view class="bg-white px-4 pb-6 border-b border-gray-200">
+      <StatusBar />
+      <view class="flex items-center pt-3">
+        <view class="w-16 h-16 bg-primary-light rounded-full flex items-center justify-center mr-4">
+          <text class="uni-icons uniui-person-filled text-primary text-2xl"></text>
+        </view>
+        <view class="flex-1 min-w-0">
+          <text class="text-gray-800 text-lg font-semibold block truncate">{{ displayName }}</text>
+          <view class="flex items-center flex-wrap mt-1">
+            <text v-if="primaryMeta" class="text-gray-500 text-sm mr-3">{{ primaryMeta }}</text>
+            <text v-if="secondaryMeta" class="text-gray-500 text-sm">{{ secondaryMeta }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 功能列表 -->
+    <view class="px-4 py-4 gap-y-4">
+      <!-- 账号信息 -->
+      <uni-card title="账号信息" icon="uniui-person-filled" :is-shadow="false">
+        <uni-list :border="false">
+          <uni-list-item title="姓名" :note="userInfo?.name || '-'" />
+          <template v-if="role === 'construction'">
+            <uni-list-item title="班组" :note="userInfo?.team || '-'" />
+            <uni-list-item title="职位" :note="userInfo?.position || '-'" />
+            <uni-list-item title="工号" :note="userInfo?.employeeNo || '-'" />
+            <uni-list-item title="联系电话" :note="userInfo?.phone || '-'" />
+          </template>
+          <template v-else>
+            <uni-list-item title="部门" :note="userInfo?.department || '-'" />
+            <uni-list-item title="职位" :note="userInfo?.position || '-'" />
+            <uni-list-item title="工号" :note="userInfo?.employeeNo || '-'" />
+            <uni-list-item title="电话" :note="userInfo?.phone || '-'" />
+            <uni-list-item title="邮箱" :note="userInfo?.email || '-'" />
+            <uni-list-item title="入职日期" :note="userInfo?.hireDate || '-'" />
+          </template>
+        </uni-list>
+      </uni-card>
+
+      <!-- 功能菜单 -->
+      <uni-card title="功能菜单" icon="uniui-gear-filled" :is-shadow="false">
+        <uni-list :border="false">
+          <uni-list-item
+            title="修改密码"
+            icon="uniui-locked-filled"
+            show-arrow
+            @click="goToChangePassword"
+          />
+          <uni-list-item
+            title="通知公告"
+            icon="uniui-notification-filled"
+            show-arrow
+            @click="goToNotices"
+          />
+          <uni-list-item
+            title="帮助中心"
+            icon="uniui-help-filled"
+            show-arrow
+            @click="goToHelp"
+          />
+        </uni-list>
+      </uni-card>
+
+      <!-- 退出登录 -->
+      <view class="mt-4">
+        <button class="w-full py-3 bg-white text-danger rounded-xl font-medium border border-gray-200" @click="handleLogout">
+          退出登录
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useAuthStore } from '@/stores/auth'
+import StatusBar from '@/components/common/StatusBar.vue'
+import UniCard from '@/components/uni-card/uni-card.vue'
+import UniList from '@/components/uni-list/uni-list.vue'
+import UniListItem from '@/components/uni-list/uni-list-item.vue'
+
+const authStore = useAuthStore()
+
+const userInfo = computed(() => authStore.user)
+const role = computed(() => userInfo.value?.role || 'sales')
+
+const displayName = computed(() => {
+  return userInfo.value?.name || '用户'
+})
+
+const primaryMeta = computed(() => {
+  if (role.value === 'construction') {
+    return userInfo.value?.team ? `${userInfo.value.team}` : ''
+  }
+  return userInfo.value?.department || ''
+})
+
+const secondaryMeta = computed(() => {
+  if (role.value === 'construction') {
+    return userInfo.value?.position ? `/${userInfo.value.position}` : ''
+  }
+  return userInfo.value?.position || ''
+})
+
+function goToChangePassword() {
+  uni.navigateTo({ url: '/subPackages/pages-common/changePassword' })
+}
+
+function goToNotices() {
+  const paths: Record<string, string> = {
+    sales: '/subPackages/pages-common/noticeList',
+    dispatch: '/subPackages/pages-dispatch/noticeList',
+    construction: '/subPackages/pages-construction/noticeList',
+  }
+  uni.navigateTo({ url: paths[role.value] || paths.sales })
+}
+
+function goToHelp() {
+  uni.navigateTo({ url: '/subPackages/pages-common/helpCenter' })
+}
+
+async function handleLogout() {
+  const res = await uni.showModal({
+    title: '确认退出',
+    content: '确定要退出登录吗?',
+  })
+  if (res.confirm) {
+    await authStore.doLogout()
+    uni.showToast({ title: '已退出登录', icon: 'none' })
+    uni.reLaunch({ url: '/pages/login/login' })
+  }
+}
+</script>

+ 1 - 1
src/components/project/ProjectCard.vue

@@ -7,7 +7,7 @@
       <StatusTag type="project" :status="project.status" />
     </view>
 
-    <view class="space-y-1 mb-3">
+    <view class="gap-y-1 mb-3">
       <view class="text-xs text-gray-500 flex items-center">
         <text class="mr-1">客</text>
         <text class="truncate">{{ project.customerName }} {{ project.customerPhone }}</text>

+ 81 - 0
src/components/task/ConstructionTaskCard.vue

@@ -0,0 +1,81 @@
+<template>
+  <view class="card p-4 mb-3 clickable" @click="$emit('click', task.id)">
+    <view class="flex justify-between items-start mb-2">
+      <view class="flex-1 min-w-0 mr-3">
+        <text class="text-sm font-semibold text-gray-800 truncate block">{{ task.projectName || '未命名任务' }}</text>
+      </view>
+      <StatusTag :status="task.status" />
+    </view>
+
+    <view class="flex flex-wrap gap-2 mb-3">
+      <view v-if="task.urgent === 'high'" class="px-2 py-1 bg-danger-light text-danger rounded text-xs font-medium">
+        加急
+      </view>
+      <view v-if="task.coopType" class="px-2 py-1 bg-primary-light text-primary rounded text-xs font-medium">
+        {{ task.coopType }}
+      </view>
+      <view v-if="task.serviceMode" class="px-2 py-1 bg-success-light text-success rounded text-xs font-medium">
+        {{ task.serviceMode }}
+      </view>
+    </view>
+
+    <view class="gap-y-2 mb-3">
+      <view class="flex items-center">
+        <text class="uni-icons uniui-tools text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs">{{ task.serviceType || '施工服务' }}</text>
+      </view>
+      <view class="flex items-center">
+        <text class="uni-icons uniui-clock-filled text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs">{{ task.serviceDate || task.scheduleDate || '待定' }} {{ task.serviceTime || task.scheduleTime || '' }}</text>
+      </view>
+      <view class="flex items-center">
+        <text class="uni-icons uniui-location-filled text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs truncate">{{ task.address || '暂无地址' }}</text>
+      </view>
+    </view>
+
+    <view class="pt-3 border-t border-gray-100">
+      <view class="flex items-center justify-between">
+        <view
+          v-for="step in steps"
+          :key="step.key"
+          class="flex-1 flex flex-col items-center"
+        >
+          <view
+            class="w-6 h-6 rounded-full flex items-center justify-center text-xs mb-1"
+            :class="step.active ? 'bg-primary text-white' : 'bg-gray-100 text-gray-400'"
+          >
+            {{ step.label }}
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import StatusTag from '../common/StatusTag.vue'
+import type { Task } from '@/types'
+
+const props = defineProps<{
+  task: Task
+}>()
+
+defineEmits<{
+  click: [taskId: string]
+}>()
+
+const steps = computed(() => {
+  const status = props.task.status
+  return [
+    { key: 'confirm', label: '确认', active: true },
+    { key: 'depart', label: '出车', active: ['departed', 'arrived', 'constructing', 'completed', 'inspecting'].includes(status) },
+    { key: 'arrive', label: '到达', active: ['arrived', 'constructing', 'completed', 'inspecting'].includes(status) },
+    { key: 'construct', label: '施工', active: ['constructing', 'completed', 'inspecting'].includes(status) },
+    { key: 'complete', label: '完成', active: ['completed', 'inspecting'].includes(status) },
+    { key: 'clean', label: '清洗', active: props.task.cleanTime != null || status === 'inspecting' || status === 'completed' },
+    { key: 'inspect', label: '验收', active: status === 'inspecting' || status === 'completed' },
+  ]
+})
+</script>

+ 115 - 0
src/components/task/DispatchTaskCard.vue

@@ -0,0 +1,115 @@
+<template>
+  <view class="card p-4 mb-3 clickable border-l-4 border-primary" :style="borderStyle" @click="$emit('click', task.id)">
+    <view class="flex justify-between items-start mb-2">
+      <view class="flex-1 min-w-0 mr-3">
+        <text class="text-sm font-semibold text-gray-800 truncate block">{{ task.projectName || '未命名任务' }}</text>
+      </view>
+      <StatusTag :status="task.status" />
+    </view>
+
+    <view class="gap-y-2 mb-3">
+      <view class="flex items-center">
+        <text class="uni-icons uniui-location-filled text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs truncate">{{ task.address || '暂无地址' }}</text>
+      </view>
+      <view class="flex items-center">
+        <text class="uni-icons uniui-tools text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs">
+          {{ task.serviceType || '施工服务' }}
+          <text v-if="task.facilityType" class="text-gray-400"> / {{ task.facilityType }}</text>
+        </text>
+      </view>
+      <view class="flex items-center">
+        <text class="uni-icons uniui-clock-filled text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs">{{ formatTime }}</text>
+      </view>
+      <view v-if="task.applicant" class="flex items-center">
+        <text class="uni-icons uniui-person-filled text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs">申请人:{{ task.applicant }}</text>
+      </view>
+    </view>
+
+    <view class="pt-2 border-t border-gray-100 flex justify-between items-center">
+      <text class="text-xs text-gray-400">{{ statusHint }}</text>
+      <text class="text-xs font-medium" :style="{ color: actionColor }">{{ actionText }}</text>
+    </view>
+
+    <view v-if="task.rejectReason" class="pt-2 border-t border-gray-100">
+      <text class="text-xs text-danger">驳回原因:{{ task.rejectReason }}</text>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import StatusTag from '../common/StatusTag.vue'
+import type { Task } from '@/types'
+import { formatDate } from '@/utils'
+
+const props = defineProps<{
+  task: Task
+}>()
+
+defineEmits<{
+  click: [taskId: string]
+}>()
+
+const borderStyle = computed(() => {
+  const color = {
+    pending: '#f59e0b',
+    confirmed: '#2563eb',
+    scheduled: '#2563eb',
+    departed: '#8b5cf6',
+    arrived: '#10b981',
+    constructing: '#2563eb',
+    inspecting: '#f59e0b',
+    completed: '#10b981',
+    cancelled: '#ef4444',
+    rejected: '#ef4444',
+  }[props.task.status] || '#2563eb'
+  return { borderLeftColor: color }
+})
+
+const formatTime = computed(() => {
+  if (props.task.applyTime) return formatDate(props.task.applyTime, 'MM-DD HH:mm')
+  if (props.task.createTime) return formatDate(props.task.createTime, 'MM-DD HH:mm')
+  return '待定'
+})
+
+const statusHint = computed(() => {
+  const map: Record<string, string> = {
+    pending: '需调度安排',
+    confirmed: '待安排班组',
+    scheduled: '已安排,待出车',
+    departed: '已出车',
+    arrived: '已到达现场',
+    constructing: '正在施工',
+    inspecting: '待验收',
+    completed: '已完成',
+    rejected: '已驳回',
+    cancelled: '已取消',
+  }
+  return map[props.task.status] || '待处理'
+})
+
+const actionText = computed(() => {
+  const map: Record<string, string> = {
+    pending: '去审核',
+    confirmed: '去排班',
+    scheduled: '查看',
+    rejected: '查看',
+  }
+  return map[props.task.status] || '查看'
+})
+
+const actionColor = computed(() => {
+  if (['pending', 'confirmed'].includes(props.task.status)) return '#2563eb'
+  return '#9ca3af'
+})
+</script>
+
+<style scoped>
+.border-l-4 {
+  border-left-width: 4px;
+}
+</style>

+ 77 - 0
src/components/task/SalesTaskCard.vue

@@ -0,0 +1,77 @@
+<template>
+  <view class="card p-4 mb-3 clickable" @click="$emit('click', task.id)">
+    <view class="flex justify-between items-start mb-2">
+      <view class="flex-1 min-w-0 mr-3">
+        <text class="text-sm font-semibold text-gray-800 truncate block">{{ task.projectName || '未命名任务' }}</text>
+      </view>
+      <StatusTag :status="task.status" />
+    </view>
+
+    <view class="flex flex-wrap gap-2 mb-3">
+      <view v-if="task.urgent === 'high'" class="px-2 py-1 bg-danger-light text-danger rounded text-xs font-medium">
+        加急
+      </view>
+      <view v-if="task.type === 'emergency'" class="px-2 py-1 bg-warning-light text-warning rounded text-xs font-medium">
+        应急
+      </view>
+      <view v-if="task.coopType" class="px-2 py-1 bg-primary-light text-primary rounded text-xs font-medium">
+        {{ task.coopType }}
+      </view>
+    </view>
+
+    <view class="gap-y-1 mb-3">
+      <view class="flex items-center">
+        <text class="uni-icons uniui-location-filled text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs truncate">{{ task.address || '暂无地址' }}</text>
+      </view>
+      <view v-if="task.team" class="flex items-center">
+        <text class="uni-icons uniui-person-filled text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs">施工班组:{{ task.team }}</text>
+      </view>
+      <view v-if="task.acceptType" class="flex items-center">
+        <text class="uni-icons uniui-checkmarkempty text-gray-400 text-xs mr-1"></text>
+        <text class="text-gray-500 text-xs">验收方式:{{ task.acceptType }}</text>
+      </view>
+    </view>
+
+    <view class="pt-3 border-t border-gray-100">
+      <view class="flex justify-between text-xs text-gray-400 mb-1">
+        <text>进度</text>
+        <text>{{ progressPercent }}%</text>
+      </view>
+      <view class="h-2 bg-gray-100 rounded-full overflow-hidden">
+        <view class="h-full bg-primary rounded-full" :style="{ width: `${progressPercent}%` }"></view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import StatusTag from '../common/StatusTag.vue'
+import type { Task } from '@/types'
+
+const props = defineProps<{
+  task: Task
+}>()
+
+defineEmits<{
+  click: [taskId: string]
+}>()
+
+const progressPercent = computed(() => {
+  const map: Record<string, number> = {
+    pending: 0,
+    rejected: 0,
+    confirmed: 25,
+    scheduled: 50,
+    departed: 60,
+    arrived: 70,
+    constructing: 75,
+    inspecting: 90,
+    completed: 100,
+    cancelled: 0,
+  }
+  return map[props.task.status] ?? 0
+})
+</script>

+ 7 - 17
src/components/task/TaskCard.vue

@@ -1,25 +1,25 @@
 
 <template>
   <view class="card p-4 mb-3 clickable" @click="$emit('click', task.id)">
-    <view class="flex justify-between items-start mb-2.5">
+    <view class="flex justify-between items-start mb-2">
       <view class="flex-1 min-w-0 mr-3">
-        <text class="text-[15px] font-semibold text-gray-800 truncate block">{{ task.projectName || '未命名任务' }}</text>
+        <text class="text-sm font-semibold text-gray-800 truncate block">{{ task.projectName || '未命名任务' }}</text>
       </view>
       <StatusTag :status="task.status" />
     </view>
     
-    <view class="space-y-1.5 mb-3">
+    <view class="gap-y-1 mb-3">
       <view class="flex items-center">
-        <text class="text-gray-400 text-xs mr-1.5"></text>
+        <text class="text-gray-400 text-xs mr-1"></text>
         <text class="text-gray-500 text-xs truncate">{{ task.address || '暂无地址' }}</text>
       </view>
       <view class="flex items-center">
-        <text class="text-gray-400 text-xs mr-1.5">⏰</text>
+        <text class="text-gray-400 text-xs mr-1">⏰</text>
         <text class="text-gray-500 text-xs">{{ task.scheduleDate || '待定' }} {{ task.scheduleTime || '' }}</text>
       </view>
     </view>
     
-    <view class="flex items-center justify-between pt-2.5 border-t border-gray-100">
+    <view class="flex items-center justify-between pt-2 border-t border-gray-100">
       <view class="flex items-center">
         <view v-if="task.vehicleNo" class="flex items-center mr-3">
           <text class="text-gray-400 text-xs mr-1"></text>
@@ -37,17 +37,7 @@
 
 <script setup lang="ts">
 import StatusTag from '../common/StatusTag.vue'
-
-interface Task {
-  id: string
-  projectName: string
-  status: string
-  address: string
-  scheduleDate: string
-  scheduleTime: string
-  vehicleNo?: string
-  staffName?: string
-}
+import type { Task } from '@/types'
 
 defineProps<{
   task: Task

+ 23 - 8
src/components/uni-card/uni-card.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="uni-card" :class="{ 'uni-card--full': isFull, 'uni-card--shadow': isShadow }">
     <!-- 卡片头部 -->
-    <view v-if="title || $slots.title" class="uni-card__header">
+    <view v-show="showHeader" class="uni-card__header">
       <slot name="title">
         <view class="uni-card__header-content">
           <text v-if="icon" class="uni-icons mr-2" :class="icon"></text>
@@ -17,13 +17,25 @@
     </view>
 
     <!-- 卡片底部 -->
-    <view v-if="$slots.footer" class="uni-card__footer">
+    <view v-show="hasFooter" class="uni-card__footer">
       <slot name="footer" />
     </view>
   </view>
 </template>
 
 <script setup lang="ts">
+import { ref, onMounted, getCurrentInstance } from 'vue'
+
+const instance = getCurrentInstance()
+const showHeader = ref(true)
+const hasFooter = ref(false)
+
+onMounted(() => {
+  const slots = instance?.slots || {}
+  showHeader.value = !!(props.title || slots.title)
+  hasFooter.value = !!slots.footer
+})
+
 interface Props {
   title?: string
   extra?: string
@@ -33,8 +45,8 @@ interface Props {
   noPadding?: boolean
 }
 
-withDefaults(defineProps<Props>(), {
-  isShadow: true,
+const props = withDefaults(defineProps<Props>(), {
+  isShadow: false,
   noPadding: false
 })
 </script>
@@ -44,14 +56,17 @@ withDefaults(defineProps<Props>(), {
   background-color: #fff;
   border-radius: 12px;
   overflow: hidden;
-  margin: 12px 16px;
+  margin: 12px 0;
+  border: 1px solid #e5e7eb;
 }
 .uni-card--full {
   margin: 0;
   border-radius: 0;
+  border-left: none;
+  border-right: none;
 }
 .uni-card--shadow {
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 2px 8px rgba(0, 0, 0, 0.04);
 }
 .uni-card__header {
   display: flex;
@@ -67,11 +82,11 @@ withDefaults(defineProps<Props>(), {
 .uni-card__title {
   font-size: 15px;
   font-weight: 600;
-  color: #333;
+  color: #1f2937;
 }
 .uni-card__extra {
   font-size: 13px;
-  color: #999;
+  color: #2563eb;
 }
 .uni-card__content {
   padding: 16px;

+ 21 - 13
src/components/uni-list/uni-list-item.vue

@@ -1,18 +1,18 @@
 <template>
-  <view 
-    class="uni-list-item" 
+  <view
+    class="uni-list-item"
     :class="{ 'uni-list-item--hover': hoverClass, 'uni-list-item--disabled': disabled }"
     @click="onClick"
   >
     <view class="uni-list-item__container">
       <!-- 左侧图标/插槽 -->
-      <view v-if="showArrow || $slots.header" class="uni-list-item__header">
+      <view v-show="showHeader" class="uni-list-item__header">
         <slot name="header">
           <view v-if="thumb" class="uni-list-item__icon">
             <image :src="thumb" class="uni-list-item__thumb" mode="aspectFill" />
           </view>
           <view v-else-if="icon" class="uni-list-item__icon">
-            <text class="uni-icons" :class="icon"></text>
+            <text class="uni-icons text-gray-500" :class="icon"></text>
           </view>
         </slot>
       </view>
@@ -41,8 +41,8 @@
     <view v-if="showSwipe" class="uni-list-item__swipe">
       <slot name="swipe">
         <view class="uni-list-item__swipe-actions">
-          <view 
-            v-for="(action, index) in swipeActions" 
+          <view
+            v-for="(action, index) in swipeActions"
             :key="index"
             class="uni-list-item__swipe-btn"
             :style="{ backgroundColor: action.color }"
@@ -57,7 +57,15 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { ref, onMounted, getCurrentInstance } from 'vue'
+
+const instance = getCurrentInstance()
+const showHeader = ref(true)
+
+onMounted(() => {
+  const slots = instance?.slots || {}
+  showHeader.value = !!(props.showArrow || props.thumb || props.icon || slots.header)
+})
 
 interface SwipeAction {
   text: string
@@ -142,8 +150,8 @@ function onSwipeClick(action: SwipeAction) {
   display: flex;
   align-items: center;
   justify-content: center;
-  background-color: #f5f5f5;
-  border-radius: 8px;
+  background-color: #f3f4f6;
+  border-radius: 10px;
 }
 .uni-list-item__content {
   flex: 1;
@@ -153,12 +161,12 @@ function onSwipeClick(action: SwipeAction) {
 }
 .uni-list-item__title {
   font-size: 15px;
-  color: #333;
+  color: #1f2937;
   line-height: 1.5;
 }
 .uni-list-item__note {
   font-size: 12px;
-  color: #999;
+  color: #9ca3af;
   margin-top: 4px;
   line-height: 1.4;
 }
@@ -169,7 +177,7 @@ function onSwipeClick(action: SwipeAction) {
 }
 .uni-list-item__right-text {
   font-size: 14px;
-  color: #999;
+  color: #6b7280;
 }
 .uni-list-item__border {
   position: absolute;
@@ -177,7 +185,7 @@ function onSwipeClick(action: SwipeAction) {
   right: 0;
   left: 15px;
   height: 1px;
-  background-color: #e5e5e5;
+  background-color: #e5e7eb;
   transform: scaleY(0.5);
 }
 .uni-list-item__swipe {

+ 2 - 2
src/components/uni-list/uni-list.vue

@@ -28,7 +28,7 @@ withDefaults(defineProps<Props>(), {
   left: 0;
   height: 1px;
   content: '';
-  background-color: #e5e5e5;
+  background-color: #e5e7eb;
   transform: scaleY(0.5);
 }
 .uni-list--border::before {
@@ -38,7 +38,7 @@ withDefaults(defineProps<Props>(), {
   left: 0;
   height: 1px;
   content: '';
-  background-color: #e5e5e5;
+  background-color: #e5e7eb;
   transform: scaleY(0.5);
 }
 </style>

+ 25 - 0
src/composables/useSystemInfo.ts

@@ -0,0 +1,25 @@
+import { ref, onMounted } from 'vue'
+
+export function useSystemInfo() {
+  const statusBarHeight = ref(0)
+  const safeAreaBottom = ref(0)
+  const screenWidth = ref(375)
+
+  onMounted(() => {
+    try {
+      const info = uni.getSystemInfoSync()
+      statusBarHeight.value = info.statusBarHeight || 0
+      safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
+      screenWidth.value = info.screenWidth || 375
+    } catch (e) {
+      statusBarHeight.value = 0
+      safeAreaBottom.value = 0
+    }
+  })
+
+  return {
+    statusBarHeight,
+    safeAreaBottom,
+    screenWidth,
+  }
+}

+ 11 - 1
src/manifest.json

@@ -48,13 +48,23 @@
     },
     /* 快应用特有相关 */
     "quickapp" : {},
+    "h5" : {
+        "sdkConfigs" : {
+            "map" : {
+                "key" : "BPCBZ-MGHKC-46F2P-AZMJV-YPOTK-5VBBD"
+            }
+        }
+    },
     /* 小程序特有相关 */
     "mp-weixin" : {
         "appid" : "",
         "setting" : {
             "urlCheck" : false
         },
-        "usingComponents" : true
+        "usingComponents" : true,
+        "optimization" : {
+            "subPackages" : true
+        }
     },
     "mp-alipay" : {
         "usingComponents" : true

+ 106 - 33
src/mock/index.ts

@@ -50,14 +50,23 @@ export const mockProjects: Project[] = [
     customerName: '王经理',
     customerPhone: '13900139001',
     address: '杭州市西湖区绿城小区1-10栋',
+    province: '浙江省',
+    city: '杭州市',
+    district: '西湖区',
     contacts: [
       { name: '王经理', phone: '13900139001', role: '物业经理' },
       { name: '李主任', phone: '13900139002', role: '工程主任' }
     ],
     servicePoints: [
-      { id: 'sp1', name: '1栋地下车库', location: '绿城小区1栋', serviceType: '开荒保洁' },
-      { id: 'sp2', name: '中心花园', location: '绿城小区中心', serviceType: '日常保洁' }
+      { id: 'sp1', name: '1栋地下车库', location: '绿城小区1栋', address: '绿城小区1栋地下车库', serviceType: '开荒保洁' },
+      { id: 'sp2', name: '中心花园', location: '绿城小区中心', address: '绿城小区中心花园', serviceType: '日常保洁' }
     ],
+    contractAmount: 120000,
+    contractStartDate: '2026-01-01',
+    contractEndDate: '2026-12-31',
+    serviceCycle: '每周2次',
+    remark: '包含地下车库、中心花园及公共区域清洁',
+    location: { lat: 30.2741, lng: 120.1551 },
     createTime: '2026-01-15T09:30:00',
     updateTime: '2026-06-20T14:20:00'
   },
@@ -69,12 +78,18 @@ export const mockProjects: Project[] = [
     customerName: '赵主管',
     customerPhone: '13900139003',
     address: '杭州市余杭区文一西路969号',
+    province: '浙江省',
+    city: '杭州市',
+    district: '余杭区',
     contacts: [
       { name: '赵主管', phone: '13900139003', role: '行政主管' }
     ],
     servicePoints: [
-      { id: 'sp3', name: 'A区办公楼', location: '园区A区', serviceType: '深度清洁' }
+      { id: 'sp3', name: 'A区办公楼', location: '园区A区', address: '阿里巴巴园区A区办公楼', serviceType: '深度清洁' }
     ],
+    contractAmount: 8000,
+    serviceCycle: '单次',
+    remark: '周末作业,避开办公高峰',
     createTime: '2026-06-18T10:00:00',
     updateTime: '2026-06-21T09:15:00'
   },
@@ -86,13 +101,20 @@ export const mockProjects: Project[] = [
     customerName: '陈总',
     customerPhone: '13900139005',
     address: '杭州市余杭区仓前街道',
+    province: '浙江省',
+    city: '杭州市',
+    district: '余杭区',
     contacts: [
       { name: '陈总', phone: '13900139005', role: '总经理' },
       { name: '小刘', phone: '13900139006', role: '对接人' }
     ],
     servicePoints: [
-      { id: 'sp4', name: '科技展厅', location: '未来科技城A座', serviceType: '展厅保洁' }
+      { id: 'sp4', name: '科技展厅', location: '未来科技城A座', address: '未来科技城A座展厅', serviceType: '展厅保洁' }
     ],
+    contractAmount: 35000,
+    contractStartDate: '2026-04-01',
+    contractEndDate: '2026-09-30',
+    serviceCycle: '每季度1次',
     createTime: '2026-06-20T16:00:00',
     updateTime: '2026-06-20T16:00:00'
   },
@@ -104,12 +126,19 @@ export const mockProjects: Project[] = [
     customerName: '孙经理',
     customerPhone: '13900139007',
     address: '杭州市滨江区江南大道',
+    province: '浙江省',
+    city: '杭州市',
+    district: '滨江区',
     contacts: [
       { name: '孙经理', phone: '13900139007', role: '楼宇经理' }
     ],
     servicePoints: [
-      { id: 'sp5', name: '1-5层办公区', location: '滨江写字楼', serviceType: '日常保洁' }
+      { id: 'sp5', name: '1-5层办公区', location: '滨江写字楼', address: '滨江写字楼1-5层', serviceType: '日常保洁' }
     ],
+    contractAmount: 18000,
+    contractStartDate: '2026-03-01',
+    contractEndDate: '2026-05-31',
+    serviceCycle: '每月4次',
     createTime: '2026-03-01T08:00:00',
     updateTime: '2026-05-31T18:00:00'
   },
@@ -121,13 +150,21 @@ export const mockProjects: Project[] = [
     customerName: '周主任',
     customerPhone: '13900139008',
     address: '杭州市西湖区龙井路',
+    province: '浙江省',
+    city: '杭州市',
+    district: '西湖区',
     contacts: [
       { name: '周主任', phone: '13900139008', role: '景区主任' }
     ],
     servicePoints: [
-      { id: 'sp6', name: '断桥区域', location: '西湖断桥', serviceType: '环卫清洁' },
-      { id: 'sp7', name: '苏堤区域', location: '西湖苏堤', serviceType: '环卫清洁' }
+      { id: 'sp6', name: '断桥区域', location: '西湖断桥', address: '西湖断桥区域', serviceType: '环卫清洁' },
+      { id: 'sp7', name: '苏堤区域', location: '西湖苏堤', address: '西湖苏堤区域', serviceType: '环卫清洁' }
     ],
+    contractAmount: 200000,
+    contractStartDate: '2026-02-01',
+    contractEndDate: '2026-12-31',
+    serviceCycle: '每日1次',
+    remark: '景区节假日需加强保洁',
     createTime: '2026-02-10T10:30:00',
     updateTime: '2026-06-18T11:00:00'
   }
@@ -224,22 +261,22 @@ export const mockSchedules: Schedule[] = [
 ]
 
 export const mockVehicles: Vehicle[] = [
-  { id: 'v1', plateNumber: '浙A·12345', type: '清洁车', status: 'in_use', driver: '张师傅' },
-  { id: 'v2', plateNumber: '浙A·67890', type: '垃圾清运车', status: 'in_use', driver: '王师傅' },
-  { id: 'v3', plateNumber: '浙A·11111', type: '高压清洗车', status: 'available', driver: '李师傅' },
-  { id: 'v4', plateNumber: '浙A·22222', type: '洒水车', status: 'maintenance', driver: '' },
-  { id: 'v5', plateNumber: '浙A·33333', type: '清洁车', status: 'available', driver: '赵师傅' }
+  { id: 'v1', plateNumber: '浙A·12345', type: '清洁车', status: 'in_use', driver: '张师傅', driverPhone: '13800138003', size: '中型', loadCapacity: '5吨', capacity: '5m³', purchaseDate: '2022-03-15', mileage: 32000 },
+  { id: 'v2', plateNumber: '浙A·67890', type: '垃圾清运车', status: 'in_use', driver: '王师傅', driverPhone: '13800138004', size: '大型', loadCapacity: '8吨', capacity: '8m³', purchaseDate: '2021-08-20', mileage: 48000 },
+  { id: 'v3', plateNumber: '浙A·11111', type: '高压清洗车', status: 'available', driver: '李师傅', driverPhone: '13800138005', size: '中型', loadCapacity: '6吨', capacity: '6m³', purchaseDate: '2023-01-10', mileage: 15000 },
+  { id: 'v4', plateNumber: '浙A·22222', type: '洒水车', status: 'maintenance', driver: '', size: '大型', loadCapacity: '10吨', capacity: '10m³', purchaseDate: '2020-11-05', mileage: 65000 },
+  { id: 'v5', plateNumber: '浙A·33333', type: '清洁车', status: 'available', driver: '赵师傅', driverPhone: '13800138006', size: '小型', loadCapacity: '3吨', capacity: '3m³', purchaseDate: '2023-06-18', mileage: 8000 }
 ]
 
 export const mockPersonnel: Personnel[] = [
-  { id: 'pe1', name: '张师傅', phone: '13800138003', team: '一班组', status: 'working' },
-  { id: 'pe2', name: '王师傅', phone: '13800138004', team: '二班组', status: 'working' },
-  { id: 'pe3', name: '李师傅', phone: '13800138005', team: '三班组', status: 'idle' },
-  { id: 'pe4', name: '赵师傅', phone: '13800138006', team: '三班组', status: 'idle' },
-  { id: 'pe5', name: '刘师傅', phone: '13800138007', team: '应急组', status: 'standby' },
-  { id: 'pe6', name: '陈师傅', phone: '13800138008', team: '应急组', status: 'standby' },
-  { id: 'pe7', name: '杨师傅', phone: '13800138009', team: '一班组', status: 'rest' },
-  { id: 'pe8', name: '黄师傅', phone: '13800138010', team: '二班组', status: 'rest' }
+  { id: 'pe1', name: '张师傅', phone: '13800138003', team: '一班组', status: 'working', position: '班长', skillLevel: '高级', qualifications: ['高空作业', '密闭空间'], workYears: 8 },
+  { id: 'pe2', name: '王师傅', phone: '13800138004', team: '二班组', status: 'working', position: '驾驶员', skillLevel: '中级', qualifications: ['大型车辆', '高压清洗'], workYears: 5 },
+  { id: 'pe3', name: '李师傅', phone: '13800138005', team: '三班组', status: 'idle', position: '操作手', skillLevel: '中级', qualifications: ['高压清洗'], workYears: 4 },
+  { id: 'pe4', name: '赵师傅', phone: '13800138006', team: '三班组', status: 'idle', position: '驾驶员', skillLevel: '初级', qualifications: ['清洁车'], workYears: 2 },
+  { id: 'pe5', name: '刘师傅', phone: '13800138007', team: '应急组', status: 'standby', position: '应急队长', skillLevel: '高级', qualifications: ['应急救援', '高空作业', '密闭空间'], workYears: 10 },
+  { id: 'pe6', name: '陈师傅', phone: '13800138008', team: '应急组', status: 'standby', position: '应急队员', skillLevel: '中级', qualifications: ['应急救援', '高压清洗'], workYears: 6 },
+  { id: 'pe7', name: '杨师傅', phone: '13800138009', team: '一班组', status: 'rest', position: '操作手', skillLevel: '初级', qualifications: ['清洁车'], workYears: 1 },
+  { id: 'pe8', name: '黄师傅', phone: '13800138010', team: '二班组', status: 'rest', position: '操作手', skillLevel: '中级', qualifications: ['垃圾清运'], workYears: 3 }
 ]
 
 export const mockNotifications: Notification[] = [
@@ -316,31 +353,67 @@ export const mockFAQs: FAQ[] = [
   {
     id: 'f1',
     question: '如何新增一个项目?',
-    answer: '进入销售端首页,点击"项目库",再点击右上角"新增项目"按钮,填写项目信息、服务点和联系人后保存。',
-    category: '销售端'
+    answer: '进入销售端首页,点击"项目库",再点击右下角"+"按钮,填写项目信息、服务点和联系人后保存。',
+    category: 'sales'
   },
   {
     id: 'f2',
-    question: '任务状态有哪些?',
-    answer: '任务状态包括:待确认、待排班、待出车、施工中、待验收、已完成、已取消、已驳回。',
-    category: '任务管理'
+    question: '任务发布后可以修改吗?',
+    answer: '任务在"待确认"状态前,可在任务详情中取消后重新发布;已确认任务请联系调度中心处理。',
+    category: 'sales'
   },
   {
     id: 'f3',
-    question: '如何进行排班?',
-    answer: '调度端进入"排班管理",选择日期和班组,点击"添加排班"并关联任务。',
-    category: '调度端'
+    question: '如何查看任务进度?',
+    answer: '销售端进入"任务"页,点击对应任务卡片即可查看当前进度、施工班组与车辆信息。',
+    category: 'sales'
   },
   {
     id: 'f4',
-    question: '施工人员如何上报任务进度?',
-    answer: '施工端进入"我的任务",选择任务后依次点击"出发"、"到达"、"开始施工"、"完成",并可上传现场照片。',
-    category: '施工端'
+    question: '如何审核任务?',
+    answer: '调度端首页"待审核任务"或任务列表"待审核"标签进入详情,确认信息后安排班组与车辆,或填写驳回原因后驳回。',
+    category: 'dispatch'
   },
   {
     id: 'f5',
+    question: '排班时如何选择合适的车辆与班组?',
+    answer: '可参考"在岗人员"查看人员技能与状态,"可用车辆"查看车辆载重与尺寸,确保资源匹配任务需求。',
+    category: 'dispatch'
+  },
+  {
+    id: 'f6',
+    question: '应急任务如何快速响应?',
+    answer: '调度端进入"应急待命"查看待命班组与车辆,点击电话联系后快速派单。',
+    category: 'dispatch'
+  },
+  {
+    id: 'f7',
+    question: '施工人员如何确认任务?',
+    answer: '施工端"任务"页进入待确认任务详情,查看项目与故障信息后点击"确认任务",也可反馈问题或拒绝。',
+    category: 'construction'
+  },
+  {
+    id: 'f8',
+    question: '每日车辆检查必须完成哪些项目?',
+    answer: '每日任务页包含常规检查、设备检查、三大系统检查,所有项勾选并提交后才算完成。',
+    category: 'construction'
+  },
+  {
+    id: 'f9',
+    question: '施工流程包含哪些步骤?',
+    answer: '标准流程为:确认→出车→到达→勘验→施工→完成→清洗→验收,请在每个节点及时点击上报。',
+    category: 'construction'
+  },
+  {
+    id: 'f10',
     question: '忘记密码怎么办?',
     answer: '请联系管理员重置密码,或在个人中心"修改密码"中按提示操作。',
-    category: '账号安全'
-  }
+    category: 'common'
+  },
+  {
+    id: 'f11',
+    question: '任务状态有哪些?',
+    answer: '任务状态包括:待确认、待排班、待出车、已出车、施工中、待验收、已完成、已取消、已驳回。',
+    category: 'common'
+  },
 ]

+ 349 - 362
src/pages.json

@@ -21,394 +21,381 @@
       }
     },
     {
-      "path": "pages/sales/home",
+      "path": "pages/home/home",
       "style": {
-        "navigationBarTitleText": "销售端首页",
+        "navigationBarTitleText": "首页",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/dispatch/home",
+      "path": "pages/task/task",
       "style": {
-        "navigationBarTitleText": "调度端首页",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/home",
-      "style": {
-        "navigationBarTitleText": "施工端首页",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/projectList",
-      "style": {
-        "navigationBarTitleText": "项目库",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/common/projectDetail",
-      "style": {
-        "navigationBarTitleText": "项目详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/addProject",
-      "style": {
-        "navigationBarTitleText": "新增项目",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/publishTask",
-      "style": {
-        "navigationBarTitleText": "发布任务",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/taskList",
-      "style": {
-        "navigationBarTitleText": "任务列表",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/common/taskDetail",
-      "style": {
-        "navigationBarTitleText": "任务详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/profile",
-      "style": {
-        "navigationBarTitleText": "个人中心",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/changePassword",
-      "style": {
-        "navigationBarTitleText": "修改密码",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/noticeList",
-      "style": {
-        "navigationBarTitleText": "通知公告",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/common/noticeDetail",
-      "style": {
-        "navigationBarTitleText": "通知详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/messageList",
-      "style": {
-        "navigationBarTitleText": "消息列表",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/messageDetail",
-      "style": {
-        "navigationBarTitleText": "消息详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/helpCenter",
-      "style": {
-        "navigationBarTitleText": "帮助中心",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/knowledgeList",
-      "style": {
-        "navigationBarTitleText": "知识库",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/common/knowledgeDetail",
-      "style": {
-        "navigationBarTitleText": "知识详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/sales/basicInfo",
-      "style": {
-        "navigationBarTitleText": "基本信息",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/sales/changePassword",
-      "style": {
-        "navigationBarTitleText": "修改密码",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/sales/helpCenter",
-      "style": {
-        "navigationBarTitleText": "帮助中心",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/sales/notification",
-      "style": {
-        "navigationBarTitleText": "通知",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/sales/taskReminder",
-      "style": {
-        "navigationBarTitleText": "任务提醒",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/schedule",
-      "style": {
-        "navigationBarTitleText": "排班管理",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/taskList",
-      "style": {
-        "navigationBarTitleText": "任务列表",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/dispatch/taskDetail",
-      "style": {
-        "navigationBarTitleText": "任务详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/publishTask",
-      "style": {
-        "navigationBarTitleText": "发布任务",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/vehicleList",
-      "style": {
-        "navigationBarTitleText": "车辆列表",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/dispatch/availableVehicles",
-      "style": {
-        "navigationBarTitleText": "可用车辆",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/personnel",
-      "style": {
-        "navigationBarTitleText": "人员管理",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/dispatch/onDutyStaff",
-      "style": {
-        "navigationBarTitleText": "当班人员",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/emergencyTeam",
-      "style": {
-        "navigationBarTitleText": "应急班组",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/emergencyStandby",
-      "style": {
-        "navigationBarTitleText": "应急待命",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/visualization",
-      "style": {
-        "navigationBarTitleText": "数据可视化",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/noticeList",
-      "style": {
-        "navigationBarTitleText": "通知列表",
+        "navigationBarTitleText": "任务",
         "navigationStyle": "custom",
         "enablePullDownRefresh": true
       }
     },
     {
-      "path": "pages/dispatch/noticeDetail",
-      "style": {
-        "navigationBarTitleText": "通知详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/notification",
-      "style": {
-        "navigationBarTitleText": "通知消息",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/basicInfo",
-      "style": {
-        "navigationBarTitleText": "基本信息",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/changePassword",
-      "style": {
-        "navigationBarTitleText": "修改密码",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/helpCenter",
-      "style": {
-        "navigationBarTitleText": "帮助中心",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/dispatch/myPage",
+      "path": "pages/my/my",
       "style": {
         "navigationBarTitleText": "我的",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/construction/taskList",
+      "path": "pages/schedule/schedule",
       "style": {
-        "navigationBarTitleText": "任务列表",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/construction/taskDetail",
-      "style": {
-        "navigationBarTitleText": "任务详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/dailyTask",
-      "style": {
-        "navigationBarTitleText": "每日任务",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/historyTask",
-      "style": {
-        "navigationBarTitleText": "历史任务",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/tomorrowTask",
-      "style": {
-        "navigationBarTitleText": "明日任务",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/noticeList",
-      "style": {
-        "navigationBarTitleText": "通知列表",
-        "navigationStyle": "custom",
-        "enablePullDownRefresh": true
-      }
-    },
-    {
-      "path": "pages/construction/noticeDetail",
-      "style": {
-        "navigationBarTitleText": "通知详情",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/notification",
-      "style": {
-        "navigationBarTitleText": "通知消息",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/basicInfo",
-      "style": {
-        "navigationBarTitleText": "基本信息",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/changePassword",
-      "style": {
-        "navigationBarTitleText": "修改密码",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/helpCenter",
-      "style": {
-        "navigationBarTitleText": "帮助中心",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/construction/myPage",
-      "style": {
-        "navigationBarTitleText": "我的",
+        "navigationBarTitleText": "排班",
         "navigationStyle": "custom"
       }
     }
   ],
+  "tabBar": {
+    "custom": true,
+    "color": "#9ca3af",
+    "selectedColor": "#2563eb",
+    "backgroundColor": "#ffffff",
+    "borderStyle": "black",
+    "list": [
+      {
+        "pagePath": "pages/home/home",
+        "text": "首页"
+      },
+      {
+        "pagePath": "pages/schedule/schedule",
+        "text": "排班"
+      },
+      {
+        "pagePath": "pages/task/task",
+        "text": "任务"
+      },
+      {
+        "pagePath": "pages/my/my",
+        "text": "我的"
+      }
+    ]
+  },
+  "subpackages": [
+    {
+      "root": "subPackages/pages-common",
+      "pages": [
+        {
+          "path": "projectList",
+          "style": {
+            "navigationBarTitleText": "项目库",
+            "navigationStyle": "custom",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "projectDetail",
+          "style": {
+            "navigationBarTitleText": "项目详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "addProject",
+          "style": {
+            "navigationBarTitleText": "新增项目",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "publishTask",
+          "style": {
+            "navigationBarTitleText": "发布任务",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "taskDetail",
+          "style": {
+            "navigationBarTitleText": "任务详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "changePassword",
+          "style": {
+            "navigationBarTitleText": "修改密码",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "noticeList",
+          "style": {
+            "navigationBarTitleText": "通知公告",
+            "navigationStyle": "custom",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "noticeDetail",
+          "style": {
+            "navigationBarTitleText": "通知详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "messageList",
+          "style": {
+            "navigationBarTitleText": "消息列表",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "messageDetail",
+          "style": {
+            "navigationBarTitleText": "消息详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "helpCenter",
+          "style": {
+            "navigationBarTitleText": "帮助中心",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "knowledgeList",
+          "style": {
+            "navigationBarTitleText": "知识库",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "knowledgeDetail",
+          "style": {
+            "navigationBarTitleText": "知识详情",
+            "navigationStyle": "custom"
+          }
+        }
+      ]
+    },
+    {
+      "root": "subPackages/pages-sales",
+      "pages": [
+        {
+          "path": "taskReminder",
+          "style": {
+            "navigationBarTitleText": "任务提醒",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "notification",
+          "style": {
+            "navigationBarTitleText": "通知",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "knowledgeList",
+          "style": {
+            "navigationBarTitleText": "知识库",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "knowledgeDetail",
+          "style": {
+            "navigationBarTitleText": "知识详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "messageList",
+          "style": {
+            "navigationBarTitleText": "消息列表",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "messageDetail",
+          "style": {
+            "navigationBarTitleText": "消息详情",
+            "navigationStyle": "custom"
+          }
+        }
+      ]
+    },
+    {
+      "root": "subPackages/pages-dispatch",
+      "pages": [
+        {
+          "path": "schedule",
+          "style": {
+            "navigationBarTitleText": "排班管理",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "taskDetail",
+          "style": {
+            "navigationBarTitleText": "任务详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "publishTask",
+          "style": {
+            "navigationBarTitleText": "发布任务",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "vehicleList",
+          "style": {
+            "navigationBarTitleText": "车辆列表",
+            "navigationStyle": "custom",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "availableVehicles",
+          "style": {
+            "navigationBarTitleText": "可用车辆",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "personnel",
+          "style": {
+            "navigationBarTitleText": "人员管理",
+            "navigationStyle": "custom",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "dutyPersonnel",
+          "style": {
+            "navigationBarTitleText": "执勤人员",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "onDutyStaff",
+          "style": {
+            "navigationBarTitleText": "当班人员",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "emergencyTeam",
+          "style": {
+            "navigationBarTitleText": "应急班组",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "emergencyStandby",
+          "style": {
+            "navigationBarTitleText": "应急待命",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "visualization",
+          "style": {
+            "navigationBarTitleText": "数据可视化",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "noticeList",
+          "style": {
+            "navigationBarTitleText": "通知列表",
+            "navigationStyle": "custom",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "noticeDetail",
+          "style": {
+            "navigationBarTitleText": "通知详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "notification",
+          "style": {
+            "navigationBarTitleText": "通知消息",
+            "navigationStyle": "custom"
+          }
+        }
+      ]
+    },
+    {
+      "root": "subPackages/pages-construction",
+      "pages": [
+        {
+          "path": "taskDetail",
+          "style": {
+            "navigationBarTitleText": "任务详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "dailyTask",
+          "style": {
+            "navigationBarTitleText": "每日任务",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "historyTask",
+          "style": {
+            "navigationBarTitleText": "历史任务",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "tomorrowTask",
+          "style": {
+            "navigationBarTitleText": "明日任务",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "noticeList",
+          "style": {
+            "navigationBarTitleText": "通知列表",
+            "navigationStyle": "custom",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "noticeDetail",
+          "style": {
+            "navigationBarTitleText": "通知详情",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "notification",
+          "style": {
+            "navigationBarTitleText": "通知消息",
+            "navigationStyle": "custom"
+          }
+        }
+      ]
+    }
+  ],
+  "preloadRule": {
+    "pages/home/home": {
+      "network": "all",
+      "packages": [
+        "subPackages/pages-common"
+      ]
+    }
+  },
   "globalStyle": {
     "navigationBarTextStyle": "black",
     "navigationBarTitleText": "清道夫系统",
     "navigationBarBackgroundColor": "#F8F8F8",
     "backgroundColor": "#f5f5f5"
   },
-  "tabBar": {
-    "list": []
-  },
   "condition": {}
-}
+}

+ 0 - 237
src/pages/common/addProject.vue

@@ -1,237 +0,0 @@
-<template>
-  <view class="app-container pb-24">
-    <TopBar title="新增项目" show-back />
-    
-    <view class="px-4 py-4 space-y-4">
-      <!-- 基本信息 -->
-      <uni-card title="基本信息" :is-shadow="true">
-        <view class="space-y-4">
-          <view>
-            <text class="form-label">项目名称 <text class="text-red-500">*</text></text>
-            <input 
-              class="form-input"
-              placeholder="请输入项目名称"
-              v-model="form.name"
-              @blur="validateField('name')"
-            />
-            <text v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</text>
-          </view>
-          <view>
-            <text class="form-label">项目类型 <text class="text-red-500">*</text></text>
-            <picker mode="selector" :range="types" :value="typeIndex" @change="onTypeChange">
-              <view class="form-input flex items-center" :class="{ 'text-gray-400': !form.type }">
-                <text class="uni-icons uniui-arrowdown text-gray-400 mr-2"></text>
-                {{ form.type || '请选择项目类型' }}
-              </view>
-            </picker>
-            <text v-if="errors.type" class="text-red-500 text-xs mt-1">{{ errors.type }}</text>
-          </view>
-          <view>
-            <text class="form-label">项目地址 <text class="text-red-500">*</text></text>
-            <input 
-              class="form-input"
-              placeholder="请输入项目地址"
-              v-model="form.address"
-              @blur="validateField('address')"
-            />
-            <text v-if="errors.address" class="text-red-500 text-xs mt-1">{{ errors.address }}</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 客户信息 -->
-      <uni-card title="客户信息" :is-shadow="true">
-        <view class="space-y-4">
-          <view>
-            <text class="form-label">客户名称 <text class="text-red-500">*</text></text>
-            <input 
-              class="form-input"
-              placeholder="请输入客户名称"
-              v-model="form.customerName"
-              @blur="validateField('customerName')"
-            />
-            <text v-if="errors.customerName" class="text-red-500 text-xs mt-1">{{ errors.customerName }}</text>
-          </view>
-          <view>
-            <text class="form-label">联系电话 <text class="text-red-500">*</text></text>
-            <input 
-              class="form-input"
-              placeholder="请输入联系电话"
-              v-model="form.customerPhone"
-              type="number"
-              maxlength="11"
-              @blur="validateField('customerPhone')"
-            />
-            <text v-if="errors.customerPhone" class="text-red-500 text-xs mt-1">{{ errors.customerPhone }}</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 服务点信息 -->
-      <uni-card title="服务点信息" :is-shadow="true">
-        <template #footer>
-          <button class="flex items-center justify-center text-blue-500" @click="addServicePoint">
-            <text class="uni-icons uniui-plusempty mr-1"></text>
-            添加服务点
-          </button>
-        </template>
-        <view class="space-y-4">
-          <view 
-            v-for="(point, index) in form.servicePoints" 
-            :key="index"
-            class="border border-gray-200 rounded-xl p-4"
-          >
-            <view class="flex justify-between items-center mb-3">
-              <text class="text-sm font-semibold text-gray-700">服务点 {{ index + 1 }}</text>
-              <text class="text-red-500 text-sm flex items-center" @click="removeServicePoint(index)">
-                <text class="uni-icons uniui-trash text-sm mr-1"></text>
-                删除
-              </text>
-            </view>
-            <view class="space-y-3">
-              <input 
-                class="form-input"
-                placeholder="服务点地址"
-                v-model="point.address"
-              />
-              <input 
-                class="form-input"
-                placeholder="联系人"
-                v-model="point.contactName"
-              />
-              <input 
-                class="form-input"
-                placeholder="联系电话"
-                v-model="point.contactPhone"
-                type="number"
-                maxlength="11"
-              />
-            </view>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 提交按钮 -->
-      <view class="px-4 pt-2">
-        <button 
-          class="w-full py-4 btn-primary rounded-2xl text-base font-semibold flex items-center justify-center"
-          @click="submitForm"
-          :disabled="submitting"
-        >
-          <text v-if="submitting" class="uni-icons uniui-spinner-cycle mr-2 animate-spin"></text>
-          <text class="uni-icons uniui-checkmarkempty mr-2" v-else></text>
-          {{ submitting ? '提交中...' : '提交项目' }}
-        </button>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, reactive } from 'vue'
-import { useProjectStore } from '../../stores/project'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import { createProject } from '../../api/project'
-
-const projectStore = useProjectStore()
-
-const types = ['包年', '单次', '季度', '月付']
-const typeIndex = ref(0)
-
-const form = ref({
-  name: '',
-  type: '',
-  address: '',
-  customerName: '',
-  customerPhone: '',
-  servicePoints: [] as any[],
-})
-
-const errors = reactive<Record<string, string>>({
-  name: '',
-  type: '',
-  address: '',
-  customerName: '',
-  customerPhone: '',
-})
-
-const submitting = ref(false)
-
-function onTypeChange(e: any) {
-  typeIndex.value = e.detail.value
-  form.value.type = types[typeIndex.value]
-  errors.type = ''
-}
-
-function validateField(field: string) {
-  const value = form.value[field as keyof typeof form.value] as string
-  errors[field] = ''
-
-  if (!value || !value.trim()) {
-    const fieldNames: Record<string, string> = {
-      name: '项目名称',
-      address: '项目地址',
-      customerName: '客户名称',
-      customerPhone: '联系电话',
-    }
-    errors[field] = `请输入${fieldNames[field] || field}`
-    return false
-  }
-
-  if (field === 'customerPhone') {
-    const phoneRegex = /^1[3-9]\d{9}$/
-    if (!phoneRegex.test(value)) {
-      errors[field] = '请输入正确的手机号'
-      return false
-    }
-  }
-
-  return true
-}
-
-function validateForm() {
-  let isValid = true
-  const requiredFields = ['name', 'type', 'address', 'customerName', 'customerPhone']
-  
-  for (const field of requiredFields) {
-    if (!validateField(field)) {
-      isValid = false
-    }
-  }
-  
-  return isValid
-}
-
-function addServicePoint() {
-  form.value.servicePoints.push({
-    address: '',
-    contactName: '',
-    contactPhone: '',
-  })
-}
-
-function removeServicePoint(index: number) {
-  form.value.servicePoints.splice(index, 1)
-}
-
-async function submitForm() {
-  if (!validateForm()) {
-    uni.showToast({ title: '请完善表单信息', icon: 'none' })
-    return
-  }
-  
-  submitting.value = true
-  try {
-    await createProject(form.value)
-    uni.showToast({ title: '创建成功', icon: 'success' })
-    setTimeout(() => {
-      uni.navigateBack()
-    }, 1500)
-  } catch (error) {
-    uni.showToast({ title: '创建失败', icon: 'none' })
-  } finally {
-    submitting.value = false
-  }
-}
-</script>

+ 0 - 137
src/pages/common/profile.vue

@@ -1,137 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="个人中心" />
-    
-    <!-- 顶部个人信息 -->
-    <view class="bg-gradient-to-r from-blue-500 to-blue-600 px-4 pt-8 pb-6">
-      <view class="flex items-center">
-        <view class="w-16 h-16 bg-white rounded-full flex items-center justify-center mr-4">
-          <text class="uni-icons uniui-person-filled text-blue-500 text-2xl"></text>
-        </view>
-        <view>
-          <text class="text-white text-lg font-semibold block">{{ userInfo?.nickName || userInfo?.userName || '用户' }}</text>
-          <text class="text-blue-100 text-sm">{{ roleText }}</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- 功能列表 -->
-    <view class="px-4 -mt-4 space-y-4">
-      <!-- 基本信息 -->
-      <uni-card title="账号信息" icon="uniui-person-filled" :is-shadow="true">
-        <uni-list border="false">
-          <uni-list-item title="用户名" :note="userInfo?.userName || '-'" />
-          <uni-list-item title="昵称" :note="userInfo?.nickName || '-'" />
-          <uni-list-item title="角色" :note="roleText" />
-          <uni-list-item title="手机号" :note="userInfo?.phonenumber || '-'" />
-        </uni-list>
-      </uni-card>
-
-      <!-- 功能菜单 -->
-      <uni-card title="功能菜单" icon="uniui-gear-filled" :is-shadow="true">
-        <uni-list border="false">
-          <uni-list-item 
-            title="基本信息" 
-            icon="uniui-person-filled" 
-            show-arrow 
-            @click="goToBasicInfo"
-          />
-          <uni-list-item 
-            title="修改密码" 
-            icon="uniui-locked-filled" 
-            show-arrow 
-            @click="goToChangePassword"
-          />
-          <uni-list-item 
-            title="通知公告" 
-            icon="uniui-notification-filled" 
-            show-arrow 
-            @click="goToNotices"
-          />
-          <uni-list-item 
-            title="帮助中心" 
-            icon="uniui-help-filled" 
-            show-arrow 
-            @click="goToHelp"
-          />
-        </uni-list>
-      </uni-card>
-
-      <!-- 退出登录 -->
-      <view class="card p-4">
-        <view class="flex items-center justify-center py-2 cursor-pointer" @click="handleLogout">
-          <text class="uni-icons uniui-close text-red-500 mr-2"></text>
-          <text class="text-red-500 text-sm">退出登录</text>
-        </view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue'
-import { useAuthStore } from '../../stores/auth'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-
-const authStore = useAuthStore()
-
-const userInfo = computed(() => authStore.user)
-
-const roleText = computed(() => {
-  const role = userInfo.value?.role
-  const map: Record<string, string> = {
-    sales: '销售端',
-    dispatch: '调度端',
-    construction: '施工端',
-  }
-  return map[role] || role || '未知'
-})
-
-const role = computed(() => userInfo.value?.role || 'sales')
-
-function goToBasicInfo() {
-  const paths: Record<string, string> = {
-    sales: '/pages/sales/basicInfo',
-    dispatch: '/pages/dispatch/basicInfo',
-    construction: '/pages/construction/basicInfo',
-  }
-  uni.navigateTo({ url: paths[role.value] || paths.sales })
-}
-
-function goToChangePassword() {
-  const paths: Record<string, string> = {
-    sales: '/pages/sales/changePassword',
-    dispatch: '/pages/dispatch/changePassword',
-    construction: '/pages/construction/changePassword',
-  }
-  uni.navigateTo({ url: paths[role.value] || paths.sales })
-}
-
-function goToNotices() {
-  const paths: Record<string, string> = {
-    sales: '/pages/common/noticeList',
-    dispatch: '/pages/dispatch/noticeList',
-    construction: '/pages/construction/noticeList',
-  }
-  uni.navigateTo({ url: paths[role.value] || paths.sales })
-}
-
-function goToHelp() {
-  uni.navigateTo({ url: '/pages/construction/helpCenter' })
-}
-
-async function handleLogout() {
-  const res = await uni.showModal({
-    title: '确认退出',
-    content: '确定要退出登录吗?',
-  })
-  if (res.confirm) {
-    await authStore.doLogout()
-    uni.showToast({ title: '已退出登录', icon: 'none' })
-    uni.reLaunch({ url: '/pages/login/login' })
-  }
-}
-</script>

+ 0 - 136
src/pages/common/projectDetail.vue

@@ -1,136 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="项目详情" show-back />
-    
-    <view v-if="projectStore.loading" class="py-20 text-center">
-      <text class="text-gray-400">加载中...</text>
-    </view>
-    
-    <view v-else-if="!project" class="px-4 py-6">
-      <EmptyState message="项目不存在" />
-    </view>
-    
-    <view v-else class="px-4 py-4 space-y-4">
-      <!-- 项目基本信息 -->
-      <uni-card :title="project.name" :is-shadow="true">
-        <view class="flex justify-between items-start mb-3">
-          <StatusTag :status="project.status" />
-          <text class="text-xs text-gray-400">{{ project.createTime }}</text>
-        </view>
-        
-        <uni-list border="false">
-          <uni-list-item 
-            title="地址" 
-            :note="project.address" 
-            icon="uniui-location-filled"
-          />
-          <uni-list-item 
-            title="客户" 
-            :note="project.customerName" 
-            icon="uniui-person-filled"
-          />
-          <uni-list-item 
-            title="电话" 
-            :note="project.customerPhone" 
-            icon="uniui-phone-filled"
-            @click="callPhone(project.customerPhone)"
-          />
-          <uni-list-item 
-            title="类型" 
-            :note="project.type" 
-            icon="uniui-folder-add-filled"
-          />
-        </uni-list>
-      </uni-card>
-
-      <!-- 服务点信息 -->
-      <uni-card title="服务点信息" :is-shadow="true">
-        <view v-if="project.servicePoints && project.servicePoints.length > 0">
-          <uni-list border="false">
-            <uni-list-item 
-              v-for="(point, index) in project.servicePoints" 
-              :key="index"
-              :title="'服务点 ' + (index + 1)"
-              :note="point.address"
-              icon="uniui-location-filled"
-            >
-              <template #footer>
-                <text class="text-xs text-gray-400">{{ point.contactName }} | {{ point.contactPhone }}</text>
-              </template>
-            </uni-list-item>
-          </uni-list>
-        </view>
-        <view v-else class="text-center py-4">
-          <text class="text-sm text-gray-400">暂无服务点信息</text>
-        </view>
-      </uni-card>
-
-      <!-- 关联任务 -->
-      <uni-card title="关联任务" :is-shadow="true">
-        <view v-if="projectTasks.length > 0">
-          <uni-list border="false">
-            <uni-list-item 
-              v-for="task in projectTasks" 
-              :key="task.id"
-              :title="task.projectName"
-              :note="task.scheduleDate + ' ' + task.scheduleTime"
-              show-arrow
-              @click="goToTask(task.id)"
-            >
-              <template #footer>
-                <StatusTag :status="task.status" />
-              </template>
-            </uni-list-item>
-          </uni-list>
-        </view>
-        <view v-else class="text-center py-4">
-          <text class="text-sm text-gray-400">暂无关联任务</text>
-        </view>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import { useProjectStore } from '../../stores/project'
-import { useTaskStore } from '../../stores/task'
-import TopBar from '../../components/common/TopBar.vue'
-import StatusTag from '../../components/common/StatusTag.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-
-const projectStore = useProjectStore()
-const taskStore = useTaskStore()
-
-const project = computed(() => projectStore.currentProject)
-const projectTasks = ref<any[]>([])
-
-function callPhone(phone: string) {
-  if (!phone) return
-  uni.makePhoneCall({ phoneNumber: phone })
-}
-
-function goToTask(taskId: string) {
-  taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/pages/common/taskDetail' })
-}
-
-onMounted(async () => {
-  if (!project.value) {
-    const pages = getCurrentPages()
-    const currentPage = pages[pages.length - 1]
-    const projectId = currentPage.options?.id
-    if (projectId) {
-      projectStore.setCurrentProject(projectId)
-    }
-  }
-  
-  if (project.value) {
-    await taskStore.fetchTasks()
-    projectTasks.value = taskStore.tasks.filter(t => t.projectId === project.value?.id)
-  }
-})
-</script>

+ 0 - 265
src/pages/common/taskDetail.vue

@@ -1,265 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar :title="'任务详情'" show-back />
-    
-    <view v-if="taskStore.loading" class="py-20 text-center">
-      <text class="text-gray-400">加载中...</text>
-    </view>
-    
-    <view v-else-if="!task" class="px-4 py-6">
-      <EmptyState message="任务不存在" />
-    </view>
-    
-    <view v-else class="px-4 py-4 space-y-4">
-      <!-- 任务基本信息 -->
-      <uni-card :title="task.projectName" :is-shadow="true">
-        <view class="flex justify-between items-start mb-3">
-          <StatusTag :status="task.status" />
-          <text class="text-xs text-gray-400">{{ task.createTime }}</text>
-        </view>
-        
-        <uni-list border="false">
-          <uni-list-item 
-            title="地址" 
-            :note="task.address" 
-            icon="uniui-location-filled"
-          />
-          <uni-list-item 
-            title="客户" 
-            :note="task.customerName" 
-            icon="uniui-person-filled"
-          />
-          <uni-list-item 
-            title="电话" 
-            :note="task.customerPhone" 
-            icon="uniui-phone-filled"
-            @click="callPhone(task.customerPhone)"
-          />
-          <uni-list-item 
-            title="时间" 
-            :note="task.scheduleDate + ' ' + task.scheduleTime" 
-            icon="uniui-calendar-filled"
-          />
-          <uni-list-item 
-            title="车辆" 
-            :note="task.vehicleNo || '未分配'" 
-            icon="uniui-cart-filled"
-          />
-          <uni-list-item 
-            title="人员" 
-            :note="task.staffName || '未分配'" 
-            icon="uniui-personadd-filled"
-          />
-        </uni-list>
-      </uni-card>
-
-      <!-- 任务进度 -->
-      <uni-card title="任务进度" :is-shadow="true">
-        <view class="relative">
-          <view class="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-200"></view>
-          <view class="space-y-4">
-            <view v-for="(step, index) in taskSteps" :key="index" class="flex items-center relative">
-              <view 
-                class="w-8 h-8 rounded-full flex items-center justify-center z-10 mr-4"
-                :class="step.completed ? 'bg-blue-500' : 'bg-gray-200'"
-              >
-                <text class="uni-icons text-white text-xs" :class="step.completed ? 'uniui-checkmarkempty' : ''"></text>
-              </view>
-              <view>
-                <text class="text-sm" :class="step.completed ? 'text-gray-800' : 'text-gray-400'">{{ step.name }}</text>
-                <text v-if="step.time" class="text-xs text-gray-400 block">{{ step.time }}</text>
-              </view>
-            </view>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 施工照片 -->
-      <uni-card title="施工照片" icon="uniui-camera-filled" :is-shadow="true">
-        <ImageUploader v-model="taskImages" tip="上传施工过程照片" />
-      </uni-card>
-
-      <!-- 电子签名 -->
-      <uni-card v-if="canComplete" title="客户签名" icon="uniui-compose" :is-shadow="true">
-        <SignaturePad ref="signatureRef" title="请客户签字确认" @save="onSignatureSave" />
-      </uni-card>
-
-      <!-- 地图位置 -->
-      <uni-card title="项目位置" icon="uniui-location-filled" :is-shadow="true">
-        <MapView
-          :latitude="taskLatitude"
-          :longitude="taskLongitude"
-          :title="task.projectName"
-          :address="task.address"
-          :height="180"
-        />
-      </uni-card>
-
-      <!-- 操作按钮 -->
-      <uni-card :is-shadow="true">
-        <view class="grid grid-cols-2 gap-3">
-          <button 
-            v-if="canDepart"
-            class="py-3 bg-blue-500 text-white rounded-xl text-sm flex items-center justify-center"
-            @click="advanceStep(3)"
-          >
-            <text class="uni-icons uniui-cart-filled mr-1"></text>
-            确认出车
-          </button>
-          <button 
-            v-if="canArrive"
-            class="py-3 bg-blue-500 text-white rounded-xl text-sm flex items-center justify-center"
-            @click="advanceStep(4)"
-          >
-            <text class="uni-icons uniui-location-filled mr-1"></text>
-            到达现场
-          </button>
-          <button 
-            v-if="canStart"
-            class="py-3 bg-blue-500 text-white rounded-xl text-sm flex items-center justify-center"
-            @click="advanceStep(5)"
-          >
-            <text class="uni-icons uniui-gear-filled mr-1"></text>
-            开始施工
-          </button>
-          <button 
-            v-if="canComplete"
-            class="py-3 bg-green-500 text-white rounded-xl text-sm flex items-center justify-center"
-            @click="advanceStep(6)"
-          >
-            <text class="uni-icons uniui-checkmarkempty mr-1"></text>
-            完成任务
-          </button>
-          <button 
-            v-if="canUrge"
-            class="py-3 bg-orange-500 text-white rounded-xl text-sm flex items-center justify-center"
-            @click="urgeTask"
-          >
-            <text class="uni-icons uniui-notification-filled mr-1"></text>
-            催单
-          </button>
-          <button 
-            v-if="canCancel"
-            class="py-3 bg-red-500 text-white rounded-xl text-sm flex items-center justify-center"
-            @click="cancelTask"
-          >
-            <text class="uni-icons uniui-closeempty mr-1"></text>
-            取消任务
-          </button>
-        </view>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted, onUnmounted } from 'vue'
-import { useTaskStore } from '../../stores/task'
-import TopBar from '../../components/common/TopBar.vue'
-import StatusTag from '../../components/common/StatusTag.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-import { advanceTaskStep, urgeTask as apiUrgeTask, cancelTask as apiCancelTask } from '../../api/task'
-
-import ImageUploader from '../../components/common/ImageUploader.vue'
-import SignaturePad from '../../components/common/SignaturePad.vue'
-import MapView from '../../components/common/MapView.vue'
-
-const taskStore = useTaskStore()
-const task = computed(() => taskStore.currentTask)
-const taskImages = ref<string[]>([])
-const signatureRef = ref()
-
-// 模拟位置数据(实际应从后端获取)
-const taskLatitude = computed(() => task.value?.location?.lat || 39.9)
-const taskLongitude = computed(() => task.value?.location?.lng || 116.4)
-
-const taskSteps = computed(() => {
-  if (!task.value) return []
-  const steps = [
-    { name: '任务创建', completed: true, time: task.value.createTime },
-    { name: '任务确认', completed: ['confirmed', 'scheduled', 'departed', 'arrived', 'constructing', 'completed'].includes(task.value.status), time: '' },
-    { name: '任务排班', completed: ['scheduled', 'departed', 'arrived', 'constructing', 'completed'].includes(task.value.status), time: '' },
-    { name: '出车', completed: ['departed', 'arrived', 'constructing', 'completed'].includes(task.value.status), time: task.value.departTime },
-    { name: '到达现场', completed: ['arrived', 'constructing', 'completed'].includes(task.value.status), time: task.value.arriveTime },
-    { name: '开始施工', completed: ['constructing', 'completed'].includes(task.value.status), time: task.value.constructTime },
-    { name: '任务完成', completed: task.value.status === 'completed', time: task.value.completeTime },
-  ]
-  return steps
-})
-
-const canDepart = computed(() => task.value?.status === 'scheduled')
-const canArrive = computed(() => task.value?.status === 'departed')
-const canStart = computed(() => task.value?.status === 'arrived')
-const canComplete = computed(() => task.value?.status === 'constructing')
-const canUrge = computed(() => ['pending', 'confirmed', 'scheduled'].includes(task.value?.status || ''))
-const canCancel = computed(() => ['pending', 'confirmed', 'scheduled'].includes(task.value?.status || ''))
-
-function onSignatureSave(imagePath: string) {
-  console.log('签名已保存:', imagePath)
-  // 这里可以上传到后端
-  uni.showToast({ title: '签名已保存', icon: 'success' })
-}
-
-function callPhone(phone: string) {
-  if (!phone) return
-  uni.makePhoneCall({ phoneNumber: phone })
-}
-
-async function advanceStep(step: number) {
-  if (!task.value) return
-  try {
-    await advanceTaskStep(Number(task.value.id), step)
-    uni.showToast({ title: '操作成功', icon: 'success' })
-    await taskStore.fetchTaskDetail(Number(task.value.id))
-  } catch (error) {
-    uni.showToast({ title: '操作失败', icon: 'none' })
-  }
-}
-
-async function urgeTask() {
-  if (!task.value) return
-  try {
-    await apiUrgeTask(Number(task.value.id))
-    uni.showToast({ title: '催单成功', icon: 'success' })
-  } catch (error) {
-    uni.showToast({ title: '催单失败', icon: 'none' })
-  }
-}
-
-async function cancelTask() {
-  if (!task.value) return
-  uni.showModal({
-    title: '确认取消',
-    content: '确定要取消此任务吗?',
-    success: async (res) => {
-      if (res.confirm) {
-        try {
-          await apiCancelTask(Number(task.value!.id))
-          uni.showToast({ title: '取消成功', icon: 'success' })
-          await taskStore.fetchTaskDetail(Number(task.value!.id))
-        } catch (error) {
-          uni.showToast({ title: '取消失败', icon: 'none' })
-        }
-      }
-    },
-  })
-}
-
-onMounted(() => {
-  if (!task.value) {
-    const pages = getCurrentPages()
-    const currentPage = pages[pages.length - 1]
-    const taskId = currentPage.options?.id
-    if (taskId) {
-      taskStore.fetchTaskDetail(Number(taskId))
-    }
-  }
-})
-
-onUnmounted(() => {
-  taskStore.currentTask = null
-})
-</script>

+ 0 - 163
src/pages/common/taskList.vue

@@ -1,163 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="任务列表" />
-    
-    <!-- 筛选标签 - 使用 uni-segmented-control -->
-    <uni-segmented-control 
-      :current="currentFilterIndex" 
-      :values="filterLabels"
-      style-type="text"
-      active-color="#4f8ef7"
-      @clickItem="onFilterChange"
-    />
-
-    <!-- 任务列表 -->
-    <view class="mt-2">
-      <view v-if="taskStore.loading" class="py-10 text-center">
-        <text class="text-gray-400">加载中...</text>
-      </view>
-      
-      <view v-else-if="filteredTasks.length === 0">
-        <EmptyState message="暂无任务" />
-      </view>
-      
-      <uni-list v-else border>
-        <uni-swipe-action 
-          v-for="task in filteredTasks" 
-          :key="task.id"
-          :actions="getSwipeActions(task)"
-          @click="(action: any) => onSwipeAction(task, action)"
-        >
-          <uni-list-item 
-            :title="task.name"
-            :note="task.address"
-            :right-text="formatStatus(task.status)"
-            show-arrow
-            @click="goToDetail(task.id)"
-          >
-            <template #header>
-              <view class="w-10 h-10 rounded-xl flex items-center justify-center mr-3" :class="getStatusBgClass(task.status)">
-                <text class="uni-icons text-lg" :class="getStatusIconClass(task.status)"></text>
-              </view>
-            </template>
-          </uni-list-item>
-        </uni-swipe-action>
-      </uni-list>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import { useTaskStore } from '../../stores/task'
-import TopBar from '../../components/common/TopBar.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-import UniSwipeAction from '../../components/uni-swipe-action/uni-swipe-action.vue'
-import UniSegmentedControl from '../../components/uni-segmented-control/uni-segmented-control.vue'
-
-const taskStore = useTaskStore()
-const currentFilterIndex = ref(0)
-
-const filterStatuses = [
-  { value: 'all', label: '全部' },
-  { value: 'pending', label: '待确认' },
-  { value: 'confirmed', label: '待排班' },
-  { value: 'scheduled', label: '待出车' },
-  { value: 'constructing', label: '施工中' },
-  { value: 'completed', label: '已完成' },
-]
-
-const filterLabels = filterStatuses.map(s => s.label)
-
-const filteredTasks = computed(() => {
-  const filterValue = filterStatuses[currentFilterIndex.value].value
-  if (filterValue === 'all') return taskStore.tasks
-  return taskStore.tasks.filter(t => t.status === filterValue)
-})
-
-function formatStatus(status: string) {
-  const map: Record<string, string> = {
-    pending: '待确认',
-    confirmed: '待排班',
-    scheduled: '待出车',
-    constructing: '施工中',
-    completed: '已完成',
-    cancelled: '已取消',
-  }
-  return map[status] || status
-}
-
-function getStatusBgClass(status: string) {
-  const map: Record<string, string> = {
-    pending: 'bg-yellow-50',
-    confirmed: 'bg-blue-50',
-    scheduled: 'bg-purple-50',
-    constructing: 'bg-orange-50',
-    completed: 'bg-green-50',
-    cancelled: 'bg-gray-50',
-  }
-  return map[status] || 'bg-gray-50'
-}
-
-function getStatusIconClass(status: string) {
-  const map: Record<string, string> = {
-    pending: 'uniui-compose text-yellow-500',
-    confirmed: 'uniui-flag text-blue-500',
-    scheduled: 'uniui-cart-filled text-purple-500',
-    constructing: 'uniui-gear-filled text-orange-500',
-    completed: 'uniui-checkmarkempty text-green-500',
-    cancelled: 'uniui-close text-gray-400',
-  }
-  return map[status] || 'uniui-flag text-gray-400'
-}
-
-function getSwipeActions(task: any) {
-  const actions = []
-  if (task.status === 'pending') {
-    actions.push({ text: '催办', color: '#ff9500', key: 'urge' })
-  }
-  if (task.status !== 'completed' && task.status !== 'cancelled') {
-    actions.push({ text: '取消', color: '#ff3b30', key: 'cancel' })
-  }
-  return actions
-}
-
-function onFilterChange(index: number) {
-  currentFilterIndex.value = index
-}
-
-function onSwipeAction(task: any, action: { key: string; text: string }) {
-  if (action.key === 'cancel') {
-    uni.showModal({
-      title: '确认取消',
-      content: '取消后无法恢复,是否继续?',
-      success: (res) => {
-        if (res.confirm) {
-          taskStore.cancelTask(task.id)
-          uni.showToast({ title: '已取消', icon: 'success' })
-        }
-      }
-    })
-  } else if (action.key === 'urge') {
-    taskStore.urgeTask(task.id)
-    uni.showToast({ title: '已催办', icon: 'success' })
-  }
-}
-
-function goToDetail(taskId: string) {
-  taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/pages/common/taskDetail' })
-}
-
-onMounted(() => {
-  taskStore.fetchTasks()
-})
-
-// 下拉刷新
-onPullDownRefresh(async () => {
-  await taskStore.fetchTasks()
-  uni.stopPullDownRefresh()
-})
-</script>

+ 0 - 86
src/pages/construction/basicInfo.vue

@@ -1,86 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="个人信息" show-back />
-
-    <view class="px-4 py-4 space-y-4">
-      <!-- 头像区域 -->
-      <uni-card :is-shadow="true">
-        <view class="flex items-center py-2">
-          <view class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mr-4">
-            <text class="uni-icons uniui-person-filled text-blue-500 text-2xl"></text>
-          </view>
-          <view>
-            <text class="text-lg font-semibold text-gray-800 block">{{ form.name }}</text>
-            <text class="text-sm text-gray-500">{{ form.team }} · 施工员</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 可编辑信息 -->
-      <uni-card title="个人信息" icon="uniui-person-filled" :is-shadow="true">
-        <view class="space-y-4 pt-2">
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">姓名</text>
-            <input
-              v-model="form.name"
-              type="text"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">工号</text>
-            <input
-              v-model="form.id"
-              type="text"
-              disabled
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-400 bg-gray-50"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">班组</text>
-            <input
-              v-model="form.team"
-              type="text"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">联系电话</text>
-            <input
-              v-model="form.phone"
-              type="tel"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-            />
-          </view>
-        </view>
-
-        <view class="pt-4">
-          <button class="w-full py-3 bg-blue-500 text-white rounded-xl font-medium" @click="saveInfo">
-            <text class="uni-icons uniui-checkmarkempty mr-1"></text>
-            保存修改
-          </button>
-        </view>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import { useAuthStore } from '../../stores/auth'
-
-const authStore = useAuthStore()
-
-const form = ref({
-  name: authStore.userName || '张师傅',
-  id: authStore.userInfo?.id || 'u3',
-  team: authStore.userInfo?.team || '一班组',
-  phone: authStore.userInfo?.phone || '13800138003',
-})
-
-function saveInfo() {
-  uni.showToast({ title: '保存成功', icon: 'success' })
-}
-</script>

+ 0 - 85
src/pages/construction/changePassword.vue

@@ -1,85 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="修改密码" show-back />
-
-    <view class="px-4 py-4">
-      <uni-card title="密码修改" icon="uniui-locked-filled" :is-shadow="true">
-        <view class="space-y-4 pt-2">
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 当前密码
-            </text>
-            <input
-              v-model="form.oldPassword"
-              type="password"
-              placeholder="请输入当前密码"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 新密码
-            </text>
-            <input
-              v-model="form.newPassword"
-              type="password"
-              placeholder="请输入新密码"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 确认新密码
-            </text>
-            <input
-              v-model="form.confirmPassword"
-              type="password"
-              placeholder="请再次输入新密码"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-            />
-          </view>
-        </view>
-
-        <view class="pt-4">
-          <button class="w-full py-3 bg-blue-500 text-white rounded-xl font-medium" @click="changePassword">
-            <text class="uni-icons uniui-checkmarkempty mr-1"></text>
-            确认修改
-          </button>
-        </view>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-
-const form = ref({
-  oldPassword: '',
-  newPassword: '',
-  confirmPassword: '',
-})
-
-function changePassword() {
-  if (!form.value.oldPassword) {
-    uni.showToast({ title: '请输入当前密码', icon: 'none' })
-    return
-  }
-  if (!form.value.newPassword) {
-    uni.showToast({ title: '请输入新密码', icon: 'none' })
-    return
-  }
-  if (form.value.newPassword.length < 6 || form.value.newPassword.length > 20) {
-    uni.showToast({ title: '密码长度应为6-20位', icon: 'none' })
-    return
-  }
-  if (form.value.newPassword !== form.value.confirmPassword) {
-    uni.showToast({ title: '两次输入的密码不一致', icon: 'none' })
-    return
-  }
-  uni.showToast({ title: '密码修改成功', icon: 'success' })
-  form.value = { oldPassword: '', newPassword: '', confirmPassword: '' }
-}
-</script>

+ 0 - 143
src/pages/construction/dailyTask.vue

@@ -1,143 +0,0 @@
-<template>
-  <view class="app-container pb-24">
-    <TopBar title="每日任务" show-back />
-    
-    <view class="px-4 py-4 space-y-4">
-      <!-- 今日任务概览 -->
-      <uni-card title="今日概览" icon="uniui-calendar-filled">
-        <view class="grid grid-cols-2 gap-3">
-          <view class="bg-blue-50 rounded-xl p-4 text-center">
-            <text class="text-2xl font-bold text-blue-600 block">{{ todayTasks.length }}</text>
-            <text class="text-xs text-gray-500 mt-1 block">今日任务</text>
-          </view>
-          <view class="bg-green-50 rounded-xl p-4 text-center">
-            <text class="text-2xl font-bold text-green-500 block">{{ completedTasks }}</text>
-            <text class="text-xs text-gray-500 mt-1 block">已完成</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 车辆检查 -->
-      <uni-card 
-        title="车辆检查" 
-        icon="uniui-cart-filled"
-        :extra="vehicleCheckStatus ? '已完成' : '待检查'"
-        :extra-color="vehicleCheckStatus ? '#34c759' : '#ff9500'"
-      >
-        <view v-if="!vehicleCheckStatus">
-          <uni-list border>
-            <uni-list-item 
-              v-for="item in checkItems" 
-              :key="item.key"
-              :title="item.label"
-              clickable
-              @click="toggleCheck(item.key)"
-            >
-              <template #header>
-                <view 
-                  class="w-6 h-6 rounded-full border-2 flex items-center justify-center mr-3"
-                  :class="checks[item.key] ? 'bg-blue-500 border-blue-500' : 'border-gray-300'"
-                >
-                  <text v-if="checks[item.key]" class="uni-icons uniui-checkmarkempty text-white text-xs"></text>
-                </view>
-              </template>
-            </uni-list-item>
-          </uni-list>
-        </view>
-        
-        <view v-else class="text-center py-8">
-          <view class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
-            <text class="uni-icons uniui-checkmarkempty text-green-500 text-2xl"></text>
-          </view>
-          <text class="text-sm text-gray-600 font-medium">今日车辆检查已完成</text>
-          <text class="text-xs text-gray-400 mt-1 block">{{ today }}</text>
-        </view>
-      </uni-card>
-
-      <!-- 今日任务列表 -->
-      <uni-card title="今日任务列表" icon="uniui-flag-filled">
-        <view v-if="taskStore.loading" class="py-10 text-center">
-          <text class="text-gray-400">加载中...</text>
-        </view>
-        <view v-else-if="todayTasks.length === 0">
-          <EmptyState message="今日无任务" />
-        </view>
-        <view v-else class="space-y-3">
-          <TaskCard 
-            v-for="task in todayTasks" 
-            :key="task.id"
-            :task="task"
-            @click="goToDetail"
-          />
-        </view>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import { useTaskStore } from '../../stores/task'
-import TopBar from '../../components/common/TopBar.vue'
-import TaskCard from '../../components/task/TaskCard.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-
-const taskStore = useTaskStore()
-
-const today = new Date().toISOString().split('T')[0]
-const storageKey = `vehicle_check_${today}`
-
-const checkItems = [
-  { key: 'exterior', label: '车辆外观检查' },
-  { key: 'tire', label: '轮胎气压检查' },
-  { key: 'fluid', label: '油液检查' },
-  { key: 'equipment', label: '设备功能检查' },
-  { key: 'safety', label: '安全设备检查' },
-]
-
-const checks = ref<Record<string, boolean>>({
-  exterior: false,
-  tire: false,
-  fluid: false,
-  equipment: false,
-  safety: false,
-})
-
-const vehicleCheckStatus = computed(() => {
-  return Object.values(checks.value).every(v => v)
-})
-
-const completedTasks = computed(() => {
-  return todayTasks.value.filter(t => t.status === 'completed').length
-})
-
-const todayTasks = computed(() => {
-  return taskStore.tasks.filter(t => t.scheduleDate === today)
-})
-
-function toggleCheck(type: string) {
-  checks.value[type] = !checks.value[type]
-  uni.setStorageSync(storageKey, JSON.stringify(checks.value))
-}
-
-function goToDetail(taskId: string) {
-  taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/pages/construction/taskDetail' })
-}
-
-onMounted(() => {
-  const saved = uni.getStorageSync(storageKey)
-  if (saved) {
-    try {
-      checks.value = JSON.parse(saved)
-    } catch {
-      // 解析失败则使用默认值
-    }
-  }
-  
-  taskStore.fetchTasks()
-})
-</script>

+ 0 - 139
src/pages/construction/helpCenter.vue

@@ -1,139 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="帮助中心" show-back />
-    
-    <view class="px-4 py-4 space-y-4">
-      <!-- 常见问题 -->
-      <uni-card title="常见问题" icon="uniui-help-filled">
-        <view class="space-y-0">
-          <view 
-            v-for="(faq, index) in faqs" 
-            :key="index"
-            class="border-b border-gray-100 py-3 last:border-0"
-            @click="toggleFaq(index)"
-          >
-            <view class="flex justify-between items-center">
-              <text class="text-sm text-gray-800">{{ faq.question }}</text>
-              <text class="uni-icons text-gray-400" :class="faq.open ? 'uniui-arrowup' : 'uniui-arrowdown'"></text>
-            </view>
-            <view v-if="faq.open" class="mt-2 bg-gray-50 rounded-lg p-3">
-              <text class="text-xs text-gray-500 leading-relaxed">{{ faq.answer }}</text>
-            </view>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 联系方式 -->
-      <uni-card title="联系方式" icon="uniui-phone-filled">
-        <uni-list border>
-          <uni-list-item 
-            title="调度中心" 
-            note="400-888-9999"
-          >
-            <template #header>
-              <text class="uni-icons uniui-phone-filled text-blue-500 mr-3"></text>
-            </template>
-          </uni-list-item>
-          <uni-list-item 
-            title="邮箱" 
-            note="support@aquagreen.com"
-          >
-            <template #header>
-              <text class="uni-icons uniui-email-filled text-blue-500 mr-3"></text>
-            </template>
-          </uni-list-item>
-          <uni-list-item 
-            title="服务时间" 
-            note="7x24小时"
-          >
-            <template #header>
-              <text class="uni-icons uniui-calendar-filled text-blue-500 mr-3"></text>
-            </template>
-          </uni-list-item>
-        </uni-list>
-      </uni-card>
-
-      <!-- 操作指南 -->
-      <uni-card title="操作指南" icon="uniui-compose">
-        <uni-list border>
-          <uni-list-item 
-            title="任务处理流程" 
-            show-arrow
-            @click="showGuide('task')"
-          >
-            <template #header>
-              <text class="uni-icons uniui-flag-filled text-blue-500 mr-3"></text>
-            </template>
-          </uni-list-item>
-          <uni-list-item 
-            title="车辆检查规范" 
-            show-arrow
-            @click="showGuide('vehicle')"
-          >
-            <template #header>
-              <text class="uni-icons uniui-cart-filled text-blue-500 mr-3"></text>
-            </template>
-          </uni-list-item>
-          <uni-list-item 
-            title="安全操作规范" 
-            show-arrow
-            @click="showGuide('safety')"
-          >
-            <template #header>
-              <text class="uni-icons uniui-locked-filled text-blue-500 mr-3"></text>
-            </template>
-          </uni-list-item>
-        </uni-list>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-
-const faqs = ref([
-  {
-    question: '如何查看今日任务?',
-    answer: '在首页点击"每日任务"卡片或在底部导航切换到"任务"页面,即可查看今日所有任务。',
-    open: false,
-  },
-  {
-    question: '车辆检查未完成怎么办?',
-    answer: '请在每日出车前完成所有检查项目,如有异常请及时上报调度中心。',
-    open: false,
-  },
-  {
-    question: '如何上报任务异常?',
-    answer: '在任务详情页面点击"异常上报"按钮,填写异常描述并提交。',
-    open: false,
-  },
-  {
-    question: '任务状态如何推进?',
-    answer: '按照SOP流程,完成任务后点击"确认完成"按钮,系统会自动推进到下一步。',
-    open: false,
-  },
-])
-
-function toggleFaq(index: number) {
-  faqs.value[index].open = !faqs.value[index].open
-}
-
-function showGuide(type: string) {
-  const guides: Record<string, string> = {
-    task: '任务处理流程:\n1. 查看任务详情\n2. 确认出车\n3. 到达现场\n4. 开始施工\n5. 完成任务\n6. 确认完成',
-    vehicle: '车辆检查规范:\n1. 检查车辆外观\n2. 检查轮胎气压\n3. 检查油液液位\n4. 检查设备功能\n5. 检查安全设备',
-    safety: '安全操作规范:\n1. 佩戴安全装备\n2. 遵守交通规则\n3. 现场安全警示\n4. 设备安全操作\n5. 紧急情况处理',
-  }
-  
-  uni.showModal({
-    title: '操作指南',
-    content: guides[type],
-    showCancel: false,
-  })
-}
-</script>

+ 0 - 199
src/pages/construction/home.vue

@@ -1,199 +0,0 @@
-<template>
-  <view class="app-container">
-    <!-- 顶部区域 -->
-    <view class="relative overflow-hidden">
-      <view class="absolute inset-0 bg-gradient-to-br from-emerald-500 to-teal-600"></view>
-      <view class="absolute -top-20 -right-20 w-60 h-60 bg-white opacity-5 rounded-full"></view>
-      <view class="absolute -bottom-10 -left-10 w-40 h-40 bg-white opacity-5 rounded-full"></view>
-      
-      <view class="relative px-5 pt-12 pb-8">
-        <view class="flex items-center justify-between">
-          <view class="flex items-center">
-            <view class="w-12 h-12 bg-white/20 backdrop-blur rounded-2xl flex items-center justify-center mr-3">
-              <text class="uni-icons uniui-person-filled text-white text-xl"></text>
-            </view>
-            <view>
-              <text class="text-white/70 text-xs block">工程一班</text>
-              <text class="text-white text-lg font-bold">张师傅</text>
-            </view>
-          </view>
-          <button class="w-10 h-10 bg-white/15 backdrop-blur rounded-full flex items-center justify-center relative" @click="goToNotification">
-            <text class="uni-icons uniui-notification-filled text-white text-lg"></text>
-          </button>
-        </view>
-      </view>
-    </view>
-
-    <!-- 主内容区域 -->
-    <view class="px-4 -mt-5 space-y-4 pb-24">
-      <!-- 统计卡片 -->
-      <uni-card title="工作统计" :is-shadow="true">
-        <view class="grid grid-cols-3 gap-3">
-          <view class="bg-emerald-50 rounded-2xl p-4 text-center" @click="goToHistoryTask">
-            <text class="stat-number text-emerald-600 block">{{ completedCount }}</text>
-            <text class="text-desc mt-1 block">已完成</text>
-          </view>
-          <view class="bg-blue-50 rounded-2xl p-4 text-center" @click="goToTaskList">
-            <text class="stat-number text-blue-500 block">{{ currentTaskCount }}</text>
-            <text class="text-desc mt-1 block">进行中</text>
-          </view>
-          <view class="bg-purple-50 rounded-2xl p-4 text-center" @click="goToTomorrowTask">
-            <text class="stat-number text-purple-500 block">{{ tomorrowCount }}</text>
-            <text class="text-desc mt-1 block">明日任务</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 每日任务入口 -->
-      <uni-card :is-shadow="true" @click="goToDailyTask">
-        <view class="flex items-center justify-between cursor-pointer">
-          <view class="flex items-center">
-            <view class="w-12 h-12 bg-emerald-100 rounded-2xl flex items-center justify-center mr-4">
-              <text class="uni-icons uniui-gear-filled text-emerald-500 text-2xl"></text>
-            </view>
-            <view>
-              <text class="text-base font-bold text-gray-800 block">每日任务</text>
-              <text class="text-sm text-gray-400 mt-0.5 block">今日检查待完成</text>
-            </view>
-          </view>
-          <view class="flex items-center">
-            <view class="w-2.5 h-2.5 bg-red-400 rounded-full mr-2"></view>
-            <text class="uni-icons uniui-arrowright text-gray-300 text-lg"></text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 快捷功能 -->
-      <uni-card title="快捷功能" :is-shadow="true">
-        <view class="grid grid-cols-4 gap-4">
-          <view class="flex flex-col items-center" @click="goToTaskList">
-            <view class="w-14 h-14 bg-blue-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-flag-filled text-blue-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">我的任务</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToDailyTask">
-            <view class="w-14 h-14 bg-emerald-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-gear-filled text-emerald-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">车辆检查</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToHistoryTask">
-            <view class="w-14 h-14 bg-purple-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-calendar-filled text-purple-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">历史任务</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToHelpCenter">
-            <view class="w-14 h-14 bg-orange-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-help-filled text-orange-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">帮助中心</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 通知公告 -->
-      <uni-card 
-        title="通知公告" 
-        extra="查看全部" 
-        :is-shadow="true"
-        @click-extra="goToNoticeList"
-      >
-        <uni-list border="false">
-          <uni-list-item 
-            v-for="(notice, index) in notices" 
-            :key="index"
-            :title="notice.title"
-            :note="notice.date"
-            show-arrow
-            @click="goToNoticeDetail"
-          >
-            <template #header>
-              <view v-if="notice.important" class="px-2 py-0.5 bg-red-50 text-red-500 text-[10px] rounded-lg font-bold mr-2 flex-shrink-0">
-                重要
-              </view>
-            </template>
-          </uni-list-item>
-        </uni-list>
-      </uni-card>
-    </view>
-
-    <!-- 底部导航 -->
-    <BottomNav :items="navItems" :currentPath="currentPath" @navigate="onNavChange" />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import { useTaskStore } from '../../stores/task'
-import BottomNav from '../../components/common/BottomNav.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-
-const taskStore = useTaskStore()
-const currentPath = ref('/pages/construction/home')
-
-const completedCount = computed(() => taskStore.tasks.filter(t => t.status === 'completed').length)
-const currentTaskCount = computed(() => taskStore.tasks.filter(t => ['scheduled', 'departed', 'arrived', 'constructing'].includes(t.status)).length)
-const tomorrowCount = computed(() => 2)
-
-const navItems = [
-  { name: '首页', path: '/pages/construction/home', icon: 'uniui-home-filled' },
-  { name: '任务', path: '/pages/construction/taskList', icon: 'uniui-flag-filled' },
-  { name: '我的', path: '/pages/construction/myPage', icon: 'uniui-person-filled' },
-]
-
-const notices = [
-  { title: '关于规范施工现场安全管理的通知', date: '2026-04-20', important: true },
-  { title: '下周安全培训会议安排', date: '2026-04-18', important: false },
-]
-
-function onNavChange(path: string) {
-  uni.reLaunch({ url: path })
-}
-
-function goToTaskList() {
-  uni.navigateTo({ url: '/pages/construction/taskList' })
-}
-
-function goToDailyTask() {
-  uni.navigateTo({ url: '/pages/construction/dailyTask' })
-}
-
-function goToHistoryTask() {
-  uni.navigateTo({ url: '/pages/construction/historyTask' })
-}
-
-function goToTomorrowTask() {
-  uni.navigateTo({ url: '/pages/construction/tomorrowTask' })
-}
-
-function goToHelpCenter() {
-  uni.navigateTo({ url: '/pages/construction/helpCenter' })
-}
-
-function goToNotification() {
-  uni.navigateTo({ url: '/pages/construction/notification' })
-}
-
-function goToNoticeList() {
-  uni.navigateTo({ url: '/pages/construction/noticeList' })
-}
-
-function goToNoticeDetail() {
-  uni.navigateTo({ url: '/pages/construction/noticeDetail' })
-}
-
-onMounted(() => {
-  taskStore.fetchTasks()
-})
-</script>
-
-<style scoped>
-.stat-number {
-  font-size: 24px;
-  font-weight: 800;
-}
-</style>

+ 0 - 150
src/pages/construction/myPage.vue

@@ -1,150 +0,0 @@
-<template>
-  <view class="app-container">
-    <!-- 顶部用户信息区域 -->
-    <view class="bg-gradient-to-br from-blue-900 to-blue-800 text-white pt-10 pb-6 px-5">
-      <view class="flex items-center gap-4">
-        <view class="w-14 h-14 bg-white/20 rounded-full flex items-center justify-center text-2xl">
-          <text></text>
-        </view>
-        <view>
-          <text class="text-lg font-bold block">{{ userName }}</text>
-          <text class="text-blue-200 text-xs">{{ userTeam }} · 工号 {{ userId }}</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- 功能列表 -->
-    <view class="px-4 -mt-3 space-y-4 pb-4">
-      <!-- 基本信息卡片 -->
-      <view class="bg-white rounded-xl p-4 shadow-sm">
-        <view class="flex justify-between items-center py-2 border-b border-gray-100">
-          <view class="flex items-center">
-            <text class="text-blue-400 mr-3 w-5 text-center"></text>
-            <text class="text-gray-500 text-sm">班组</text>
-          </view>
-          <text class="text-sm">{{ userTeam }}</text>
-        </view>
-        <view class="flex justify-between items-center py-2 border-b border-gray-100">
-          <view class="flex items-center">
-            <text class="text-blue-400 mr-3 w-5 text-center">🆔</text>
-            <text class="text-gray-500 text-sm">职位</text>
-          </view>
-          <text class="text-sm">施工员</text>
-        </view>
-        <view class="flex justify-between items-center py-2">
-          <view class="flex items-center">
-            <text class="text-blue-400 mr-3 w-5 text-center"></text>
-            <text class="text-gray-500 text-sm">电话</text>
-          </view>
-          <text class="text-sm">{{ userPhone }}</text>
-        </view>
-      </view>
-
-      <!-- 功能入口卡片 -->
-      <view class="bg-white rounded-xl p-4 shadow-sm">
-        <view class="flex justify-between items-center py-3 border-b border-gray-100 cursor-pointer" @click="goToBasicInfo">
-          <view class="flex items-center">
-            <text class="text-gray-400 mr-3 w-5 text-center"></text>
-            <text class="text-sm text-gray-700">基本信息</text>
-          </view>
-          <text class="text-gray-300 text-xs">></text>
-        </view>
-        <view class="flex justify-between items-center py-3 border-b border-gray-100 cursor-pointer" @click="goToChangePassword">
-          <view class="flex items-center">
-            <text class="text-gray-400 mr-3 w-5 text-center"></text>
-            <text class="text-sm text-gray-700">修改密码</text>
-          </view>
-          <text class="text-gray-300 text-xs">></text>
-        </view>
-        <view class="flex justify-between items-center py-3 border-b border-gray-100 cursor-pointer" @click="goToNotification">
-          <view class="flex items-center">
-            <text class="text-gray-400 mr-3 w-5 text-center"></text>
-            <text class="text-sm text-gray-700">消息通知</text>
-          </view>
-          <view class="flex items-center">
-            <view v-if="unreadCount > 0" class="w-2 h-2 bg-red-500 rounded-full mr-2"></view>
-            <text class="text-gray-300 text-xs">></text>
-          </view>
-        </view>
-        <view class="flex justify-between items-center py-3 cursor-pointer" @click="goToHelpCenter">
-          <view class="flex items-center">
-            <text class="text-gray-400 mr-3 w-5 text-center"></text>
-            <text class="text-sm text-gray-700">帮助中心</text>
-          </view>
-          <text class="text-gray-300 text-xs">></text>
-        </view>
-      </view>
-
-      <!-- 退出登录按钮 -->
-      <button class="w-full py-3 mt-4 bg-red-500 text-white rounded-xl font-medium" @click="logout">
-        退出登录
-      </button>
-    </view>
-
-    <!-- 底部导航 -->
-    <BottomNav :items="navItems" :currentPath="currentPath" @navigate="onNavChange" />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed } from 'vue'
-import BottomNav from '../../components/common/BottomNav.vue'
-import { useAuthStore } from '../../stores/auth'
-import { useNotificationStore } from '../../stores/notification'
-import { navigateTo, showModal, showToast } from '../../utils'
-
-const authStore = useAuthStore()
-const notificationStore = useNotificationStore()
-const currentPath = ref('/pages/construction/myPage')
-
-const userName = computed(() => authStore.userName || '张师傅')
-const userTeam = computed(() => authStore.userInfo?.team || '一班组')
-const userId = computed(() => authStore.userInfo?.id || 'u3')
-const userPhone = computed(() => {
-  const phone = authStore.userInfo?.phone || '13800138003'
-  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
-})
-const unreadCount = computed(() => notificationStore.unreadCount)
-
-const navItems = [
-  { text: '首页', path: '/pages/construction/home', icon: 'uniui-home-filled' },
-  { text: '任务', path: '/pages/construction/taskList', icon: 'uniui-flag-filled' },
-  { text: '我的', path: '/pages/construction/myPage', icon: 'uniui-person-filled' },
-]
-
-function onNavChange(path: string) {
-  uni.reLaunch({ url: path })
-}
-
-function goToBasicInfo() {
-  navigateTo('/pages/construction/basicInfo')
-}
-
-function goToChangePassword() {
-  navigateTo('/pages/construction/changePassword')
-}
-
-function goToNotification() {
-  navigateTo('/pages/construction/notification')
-}
-
-function goToHelpCenter() {
-  navigateTo('/pages/construction/helpCenter')
-}
-
-function logout() {
-  showModal({
-    title: '确认退出',
-    content: '确定要退出登录吗?',
-    showCancel: true,
-    confirmText: '退出',
-    success: async (res: any) => {
-      if (res.confirm) {
-        await authStore.doLogout()
-        showToast('已退出登录')
-        uni.reLaunch({ url: '/pages/login/login' })
-      }
-    }
-  })
-}
-</script>

+ 0 - 78
src/pages/construction/taskList.vue

@@ -1,78 +0,0 @@
-
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="我的任务" />
-    
-    <!-- 筛选标签 -->
-    <view class="bg-white px-4 py-3 flex gap-2 overflow-x-auto hide-scrollbar">
-      <view 
-        v-for="status in filterStatuses" 
-        :key="status.value"
-        class="filter-tab"
-        :class="{ active: currentFilter === status.value }"
-        @click="currentFilter = status.value"
-      >
-        {{ status.label }}
-      </view>
-    </view>
-
-    <!-- 任务列表 -->
-    <view class="px-4 mt-3">
-      <view v-if="taskStore.loading" class="py-10 text-center">
-        <text class="text-gray-400">加载中...</text>
-      </view>
-      
-      <view v-else-if="filteredTasks.length === 0">
-        <EmptyState message="暂无任务" />
-      </view>
-      
-      <view v-else class="space-y-3">
-        <TaskCard 
-          v-for="task in filteredTasks" 
-          :key="task.id"
-          :task="task"
-          @click="goToDetail"
-        />
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import { useTaskStore } from '../../stores/task'
-import TopBar from '../../components/common/TopBar.vue'
-import TaskCard from '../../components/task/TaskCard.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-
-const taskStore = useTaskStore()
-const currentFilter = ref('all')
-
-const filterStatuses = [
-  { value: 'all', label: '全部' },
-  { value: 'scheduled', label: '待出车' },
-  { value: 'departed', label: '已出车' },
-  { value: 'constructing', label: '施工中' },
-  { value: 'completed', label: '已完成' },
-]
-
-const filteredTasks = computed(() => {
-  if (currentFilter.value === 'all') return taskStore.tasks
-  return taskStore.tasks.filter(t => t.status === currentFilter.value)
-})
-
-function goToDetail(taskId: string) {
-  taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/pages/construction/taskDetail' })
-}
-
-onMounted(() => {
-  taskStore.fetchTasks()
-})
-
-// 下拉刷新
-onPullDownRefresh(async () => {
-  await taskStore.fetchTasks()
-  uni.stopPullDownRefresh()
-})
-</script>

+ 0 - 138
src/pages/dispatch/availableVehicles.vue

@@ -1,138 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="可用车辆" show-back />
-    
-    <!-- 统计卡片 -->
-    <view class="px-4 py-4">
-      <view class="grid grid-cols-3 gap-3">
-        <view class="bg-green-50 rounded-xl p-3 text-center">
-          <text class="text-xl font-bold text-green-600 block">{{ availableCount }}</text>
-          <text class="text-xs text-gray-500">可用</text>
-        </view>
-        <view class="bg-blue-50 rounded-xl p-3 text-center">
-          <text class="text-xl font-bold text-blue-600 block">{{ inUseCount }}</text>
-          <text class="text-xs text-gray-500">使用中</text>
-        </view>
-        <view class="bg-yellow-50 rounded-xl p-3 text-center">
-          <text class="text-xl font-bold text-yellow-600 block">{{ maintenanceCount }}</text>
-          <text class="text-xs text-gray-500">维修中</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- 筛选 -->
-    <view class="px-4 pb-2">
-      <uni-segmented-control 
-        :current="currentFilterIndex" 
-        :values="filterLabels"
-        style-type="text"
-        active-color="#4f8ef7"
-        @clickItem="onFilterChange"
-      />
-    </view>
-
-    <!-- 车辆列表 -->
-    <view class="px-4 mt-2">
-      <view v-if="loading" class="py-10 text-center">
-        <text class="text-gray-400">加载中...</text>
-      </view>
-      
-      <view v-else-if="filteredVehicles.length === 0">
-        <EmptyState message="暂无车辆" />
-      </view>
-      
-      <uni-list v-else border>
-        <uni-list-item 
-          v-for="vehicle in filteredVehicles" 
-          :key="vehicle.vehicleId"
-          :title="vehicle.vehicleNo"
-          :note="(vehicle.type || '未分类') + ' | ' + (vehicle.driver || '未分配司机')"
-          :right-text="getStatusText(vehicle.status)"
-        >
-          <template #header>
-            <view class="w-10 h-10 rounded-xl flex items-center justify-center mr-3" :class="getStatusBgClass(vehicle.status)">
-              <text class="uni-icons text-lg" :class="getStatusIconClass(vehicle.status)"></text>
-            </view>
-          </template>
-        </uni-list-item>
-      </uni-list>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-import UniSegmentedControl from '../../components/uni-segmented-control/uni-segmented-control.vue'
-
-const loading = ref(false)
-const vehicles = ref<any[]>([])
-const currentFilterIndex = ref(0)
-
-const filterTypes = [
-  { value: 'all', label: '全部' },
-  { value: 'available', label: '可用' },
-  { value: 'in_use', label: '使用中' },
-  { value: 'maintenance', label: '维修中' },
-]
-
-const filterLabels = filterTypes.map(t => t.label)
-
-const filteredVehicles = computed(() => {
-  const filterValue = filterTypes[currentFilterIndex.value].value
-  if (filterValue === 'all') return vehicles.value
-  return vehicles.value.filter(v => v.status === filterValue)
-})
-
-const availableCount = computed(() => vehicles.value.filter(v => v.status === 'available').length)
-const inUseCount = computed(() => vehicles.value.filter(v => v.status === 'in_use').length)
-const maintenanceCount = computed(() => vehicles.value.filter(v => v.status === 'maintenance').length)
-
-function getStatusText(status: string) {
-  const map: Record<string, string> = {
-    available: '可用',
-    in_use: '使用中',
-    maintenance: '维修中',
-    offline: '离线',
-  }
-  return map[status] || status
-}
-
-function getStatusBgClass(status: string) {
-  const map: Record<string, string> = {
-    available: 'bg-green-50',
-    in_use: 'bg-blue-50',
-    maintenance: 'bg-yellow-50',
-    offline: 'bg-gray-50',
-  }
-  return map[status] || 'bg-gray-50'
-}
-
-function getStatusIconClass(status: string) {
-  const map: Record<string, string> = {
-    available: 'uniui-checkmarkempty text-green-500',
-    in_use: 'uniui-cart-filled text-blue-500',
-    maintenance: 'uniui-gear-filled text-yellow-500',
-    offline: 'uniui-close text-gray-400',
-  }
-  return map[status] || 'uniui-cart text-gray-400'
-}
-
-function onFilterChange(index: number) {
-  currentFilterIndex.value = index
-}
-
-onMounted(() => {
-  // 模拟数据
-  vehicles.value = [
-    { vehicleId: '1', vehicleNo: '云A12345', type: '清洁车', status: 'available', driver: '张师傅' },
-    { vehicleId: '2', vehicleNo: '云A67890', type: '高压清洗车', status: 'available', driver: '李师傅' },
-    { vehicleId: '3', vehicleNo: '云A11111', type: '垃圾清运车', status: 'in_use', driver: '王师傅' },
-    { vehicleId: '4', vehicleNo: '云A22222', type: '洒水车', status: 'maintenance', driver: '' },
-    { vehicleId: '5', vehicleNo: '云A33333', type: '清洁车', status: 'available', driver: '赵师傅' },
-  ]
-})
-</script>

+ 0 - 59
src/pages/dispatch/basicInfo.vue

@@ -1,59 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="基本信息" show-back />
-
-    <view class="px-4 py-4 space-y-4">
-      <!-- 头像区域 -->
-      <uni-card :is-shadow="true">
-        <view class="flex items-center py-2">
-          <view class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mr-4">
-            <text class="uni-icons uniui-person-filled text-blue-500 text-2xl"></text>
-          </view>
-          <view>
-            <text class="text-lg font-semibold text-gray-800 block">{{ userInfo?.name || '李调度' }}</text>
-            <text class="text-sm text-gray-500">{{ userInfo?.department || '调度中心' }} · 调度主管</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 基本信息 -->
-      <uni-card title="基本信息" icon="uniui-person-filled" :is-shadow="true">
-        <uni-list border="false">
-          <uni-list-item title="姓名" :note="userInfo?.name || '李调度'" />
-          <uni-list-item title="工号" note="DIS001" />
-          <uni-list-item title="部门" :note="userInfo?.department || '调度中心'" />
-          <uni-list-item title="职位" note="调度主管" />
-          <uni-list-item title="电话" :note="userInfo?.phone || '13800138002'" />
-          <uni-list-item title="状态" :right-text="'在职'" right-color="#34c759" />
-        </uni-list>
-      </uni-card>
-
-      <!-- 操作按钮 -->
-      <view class="pt-2">
-        <button
-          class="w-full py-3 bg-blue-500 text-white rounded-xl font-medium flex items-center justify-center"
-          @click="handleEdit"
-        >
-          <text class="uni-icons uniui-compose mr-2"></text>
-          编辑信息
-        </button>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-import { useAuthStore } from '../../stores/auth'
-
-const authStore = useAuthStore()
-const userInfo = computed(() => authStore.userInfo)
-
-function handleEdit() {
-  uni.showToast({ title: '编辑功能开发中', icon: 'none' })
-}
-</script>

+ 0 - 120
src/pages/dispatch/changePassword.vue

@@ -1,120 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="修改密码" show-back />
-
-    <view class="px-4 py-4">
-      <uni-card title="密码修改" icon="uniui-locked-filled" :is-shadow="true">
-        <view class="space-y-4 pt-2">
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 当前密码
-            </text>
-            <input
-              v-model="form.currentPassword"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-              type="password"
-              placeholder="请输入当前密码"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 新密码
-            </text>
-            <input
-              v-model="form.newPassword"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-              type="password"
-              placeholder="请输入新密码(6-20位)"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 确认新密码
-            </text>
-            <input
-              v-model="form.confirmPassword"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-              type="password"
-              placeholder="请再次输入新密码"
-            />
-          </view>
-        </view>
-
-        <view class="mt-4">
-          <view class="bg-blue-50 rounded-lg p-3 mb-4">
-            <text class="text-blue-600 text-sm block font-medium">密码要求:</text>
-            <view class="text-gray-500 text-xs mt-2 space-y-1">
-              <text class="block">• 长度6-20位</text>
-              <text class="block">• 必须包含字母和数字</text>
-              <text class="block">• 不能与当前密码相同</text>
-            </view>
-          </view>
-
-          <button
-            class="w-full py-3 bg-blue-500 text-white rounded-xl font-medium"
-            @click="handleSubmit"
-          >
-            确认修改
-          </button>
-        </view>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { reactive } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-
-const form = reactive({
-  currentPassword: '',
-  newPassword: '',
-  confirmPassword: ''
-})
-
-function validateForm(): boolean {
-  if (!form.currentPassword) {
-    uni.showToast({ title: '请输入当前密码', icon: 'none' })
-    return false
-  }
-  if (!form.newPassword) {
-    uni.showToast({ title: '请输入新密码', icon: 'none' })
-    return false
-  }
-  if (form.newPassword.length < 6 || form.newPassword.length > 20) {
-    uni.showToast({ title: '密码长度应为6-20位', icon: 'none' })
-    return false
-  }
-  if (!/^(?=.*[a-zA-Z])(?=.*\d).+$/.test(form.newPassword)) {
-    uni.showToast({ title: '密码必须包含字母和数字', icon: 'none' })
-    return false
-  }
-  if (form.newPassword === form.currentPassword) {
-    uni.showToast({ title: '新密码不能与当前密码相同', icon: 'none' })
-    return false
-  }
-  if (form.newPassword !== form.confirmPassword) {
-    uni.showToast({ title: '两次输入的新密码不一致', icon: 'none' })
-    return false
-  }
-  return true
-}
-
-async function handleSubmit() {
-  if (!validateForm()) return
-
-  uni.showModal({
-    title: '确认修改',
-    content: '确定要修改密码吗?修改后需要重新登录。',
-    success: (res) => {
-      if (res.confirm) {
-        uni.showToast({ title: '密码修改成功', icon: 'success' })
-        form.currentPassword = ''
-        form.newPassword = ''
-        form.confirmPassword = ''
-      }
-    }
-  })
-}
-</script>

+ 0 - 66
src/pages/dispatch/emergencyStandby.vue

@@ -1,66 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="应急待命" show-back />
-
-    <!-- 应急状态说明 -->
-    <view class="px-4 mt-4">
-      <view class="p-3 bg-red-50 rounded-lg flex items-center mb-4">
-        <text class="text-red-500 mr-2"></text>
-        <text class="text-sm text-red-600">应急待命人员保持通讯畅通,随时准备响应紧急任务</text>
-      </view>
-    </view>
-
-    <view class="px-4 py-4">
-      <view v-if="emergencyTeam.length > 0" class="space-y-3">
-        <view
-          v-for="person in emergencyTeam"
-          :key="person.id"
-          class="card p-4"
-        >
-          <view class="flex items-center justify-between">
-            <view class="flex items-center">
-              <view class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-3">
-                <text class="text-xl"></text>
-              </view>
-              <view>
-                <text class="font-medium text-gray-800 block">{{ person.name }}</text>
-                <text class="text-sm text-gray-500">{{ person.team }} · {{ person.phone }}</text>
-              </view>
-            </view>
-            <text class="tag tag-red">{{ getPersonnelStatusText(person.status) }}</text>
-          </view>
-          <view class="mt-3 pt-3 border-t border-gray-100">
-            <text class="text-xs text-gray-400"> 联系电话:{{ person.phone }}</text>
-          </view>
-        </view>
-      </view>
-      <EmptyState v-else message="暂无应急待命人员" />
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import { usePersonnelStore } from '../../stores/personnel'
-import { getPersonnelStatusText } from '../../utils'
-
-const personnelStore = usePersonnelStore()
-const emergencyTeam = computed(() => personnelStore.emergencyTeam)
-</script>
-
-<style scoped>
-.tag {
-  display: inline-block;
-  padding: 2px 8px;
-  border-radius: 4px;
-  font-size: 12px;
-  font-weight: 500;
-}
-
-.tag-red {
-  background: #fef2f2;
-  color: #ef4444;
-}
-</style>

+ 0 - 76
src/pages/dispatch/helpCenter.vue

@@ -1,76 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="帮助中心" show-back />
-
-    <view class="px-4 py-4">
-      <!-- 常见问题 -->
-      <view class="card p-4 mb-4">
-        <text class="text-base font-semibold text-gray-800 mb-4 block"> 常见问题</text>
-        <view v-if="faqs.length > 0" class="space-y-3">
-          <view
-            v-for="faq in faqs"
-            :key="faq.id"
-            class="pb-3 border-b border-gray-100 last:border-b-0 last:pb-0"
-          >
-            <view class="flex items-center justify-between" @click="toggleFaq(faq.id)">
-              <text class="text-sm text-gray-800 flex-1">{{ faq.question }}</text>
-              <text class="text-gray-400 ml-2">{{ expandedFaq === faq.id ? '' : '' }}</text>
-            </view>
-            <view v-if="expandedFaq === faq.id" class="mt-2 bg-gray-50 rounded-lg p-3">
-              <text class="text-sm text-gray-600">{{ faq.answer }}</text>
-            </view>
-          </view>
-        </view>
-        <EmptyState v-else message="暂无常见问题" />
-      </view>
-
-      <!-- 知识库 -->
-      <view class="card p-4">
-        <text class="text-base font-semibold text-gray-800 mb-4 block"> 知识库</text>
-        <view v-if="knowledges.length > 0" class="space-y-3">
-          <view
-            v-for="knowledge in knowledges"
-            :key="knowledge.id"
-            class="pb-3 border-b border-gray-100 last:border-b-0 last:pb-0"
-            @click="goToKnowledgeDetail(knowledge.id)"
-          >
-            <view class="flex items-start">
-              <view class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center mr-3 flex-shrink-0">
-                <text class="text-blue-500 text-sm"></text>
-              </view>
-              <view class="flex-1 min-w-0">
-                <text class="text-sm font-medium text-gray-800 block">{{ knowledge.title }}</text>
-                <text class="text-xs text-gray-500 mt-1 block">{{ knowledge.summary }}</text>
-                <text class="text-xs text-blue-500 mt-1 block">{{ knowledge.category }}</text>
-              </view>
-            </view>
-          </view>
-        </view>
-        <EmptyState v-else message="暂无知识库内容" />
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed, ref } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import { useCommonStore } from '../../stores/common'
-import { navigateTo, showToast } from '../../utils'
-
-const commonStore = useCommonStore()
-const expandedFaq = ref<string | null>(null)
-
-const faqs = computed(() => commonStore.faqs)
-const knowledges = computed(() => commonStore.knowledges)
-
-function toggleFaq(id: string) {
-  expandedFaq.value = expandedFaq.value === id ? null : id
-}
-
-function goToKnowledgeDetail(id: string) {
-  commonStore.setCurrentKnowledge(id)
-  showToast('知识详情:' + commonStore.currentKnowledge?.title)
-}
-</script>

+ 0 - 177
src/pages/dispatch/home.vue

@@ -1,177 +0,0 @@
-<template>
-  <view class="app-container">
-    <!-- 顶部区域 -->
-    <view class="relative overflow-hidden">
-      <view class="absolute inset-0 bg-gradient-to-br from-indigo-500 to-purple-600"></view>
-      <view class="absolute -top-20 -right-20 w-60 h-60 bg-white opacity-5 rounded-full"></view>
-      <view class="absolute -bottom-10 -left-10 w-40 h-40 bg-white opacity-5 rounded-full"></view>
-      
-      <view class="relative px-5 pt-12 pb-8">
-        <view class="flex items-center justify-between">
-          <view class="flex items-center">
-            <view class="w-12 h-12 bg-white/20 backdrop-blur rounded-2xl flex items-center justify-center mr-3">
-              <text class="uni-icons uniui-person-filled text-white text-xl"></text>
-            </view>
-            <view>
-              <text class="text-white/70 text-xs block">调度中心</text>
-              <text class="text-white text-lg font-bold">控制台</text>
-            </view>
-          </view>
-          <button class="w-10 h-10 bg-white/15 backdrop-blur rounded-full flex items-center justify-center relative" @click="goToNotification">
-            <text class="uni-icons uniui-notification-filled text-white text-lg"></text>
-          </button>
-        </view>
-      </view>
-    </view>
-
-    <!-- 主内容区域 -->
-    <view class="px-4 -mt-5 space-y-4 pb-24">
-      <!-- 统计卡片 -->
-      <uni-card title="今日概览" :is-shadow="true">
-        <view class="grid grid-cols-3 gap-3">
-          <view class="bg-indigo-50 rounded-2xl p-4 text-center" @click="goToTaskList">
-            <text class="stat-number text-indigo-600 block">{{ taskStore.total }}</text>
-            <text class="text-desc mt-1 block">今日任务</text>
-          </view>
-          <view class="bg-orange-50 rounded-2xl p-4 text-center" @click="goToTaskList">
-            <text class="stat-number text-orange-500 block">{{ taskStore.pendingCount }}</text>
-            <text class="text-desc mt-1 block">待排班</text>
-          </view>
-          <view class="bg-red-50 rounded-2xl p-4 text-center" @click="goToEmergencyTeam">
-            <text class="stat-number text-red-500 block">{{ emergencyCount }}</text>
-            <text class="text-desc mt-1 block">应急中</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 快捷功能 -->
-      <uni-card title="快捷功能" :is-shadow="true">
-        <view class="grid grid-cols-4 gap-4">
-          <view class="flex flex-col items-center" @click="goToSchedule">
-            <view class="w-14 h-14 bg-indigo-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-calendar-filled text-indigo-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">排班</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToTaskList">
-            <view class="w-14 h-14 bg-green-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-flag-filled text-green-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">任务</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToVehicleList">
-            <view class="w-14 h-14 bg-blue-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-cart-filled text-blue-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">车辆</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToVisualization">
-            <view class="w-14 h-14 bg-purple-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-map-filled text-purple-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">数据</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 最新任务 -->
-      <uni-card 
-        title="最新任务" 
-        extra="查看全部" 
-        :is-shadow="true"
-        @click-extra="goToTaskList"
-      >
-        <view v-if="taskStore.tasks.length === 0" class="py-6 text-center">
-          <text class="text-desc">暂无任务</text>
-        </view>
-        <uni-list v-else border="false">
-          <uni-list-item 
-            v-for="task in taskStore.tasks.slice(0, 3)" 
-            :key="task.id"
-            :title="task.projectName"
-            :note="task.scheduleDate"
-            show-arrow
-            @click="goToTaskDetail(task.id)"
-          >
-            <template #header>
-              <view class="w-10 h-10 bg-indigo-50 rounded-xl flex items-center justify-center mr-3">
-                <text class="uni-icons uniui-flag text-indigo-500"></text>
-              </view>
-            </template>
-            <template #footer>
-              <StatusTag :status="task.status" />
-            </template>
-          </uni-list-item>
-        </uni-list>
-      </uni-card>
-    </view>
-
-    <!-- 底部导航 -->
-    <BottomNav :items="navItems" :currentPath="currentPath" @navigate="onNavChange" />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import { useTaskStore } from '../../stores/task'
-import BottomNav from '../../components/common/BottomNav.vue'
-import StatusTag from '../../components/common/StatusTag.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-
-const taskStore = useTaskStore()
-const currentPath = ref('/pages/dispatch/home')
-
-const emergencyCount = computed(() => taskStore.tasks.filter(t => t.type === 'emergency').length)
-
-const navItems = [
-  { name: '首页', path: '/pages/dispatch/home', icon: 'uniui-home-filled' },
-  { name: '任务', path: '/pages/dispatch/taskList', icon: 'uniui-flag-filled' },
-  { name: '我的', path: '/pages/dispatch/myPage', icon: 'uniui-person-filled' },
-]
-
-function onNavChange(path: string) {
-  uni.reLaunch({ url: path })
-}
-
-function goToTaskList() {
-  uni.navigateTo({ url: '/pages/dispatch/taskList' })
-}
-
-function goToTaskDetail(taskId: string) {
-  taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/pages/dispatch/taskDetail' })
-}
-
-function goToSchedule() {
-  uni.navigateTo({ url: '/pages/dispatch/schedule' })
-}
-
-function goToVehicleList() {
-  uni.navigateTo({ url: '/pages/dispatch/vehicleList' })
-}
-
-function goToVisualization() {
-  uni.navigateTo({ url: '/pages/dispatch/visualization' })
-}
-
-function goToEmergencyTeam() {
-  uni.navigateTo({ url: '/pages/dispatch/emergencyTeam' })
-}
-
-function goToNotification() {
-  uni.navigateTo({ url: '/pages/dispatch/notification' })
-}
-
-onMounted(() => {
-  taskStore.fetchTasks()
-})
-</script>
-
-<style scoped>
-.stat-number {
-  font-size: 24px;
-  font-weight: 800;
-}
-</style>

+ 0 - 120
src/pages/dispatch/myPage.vue

@@ -1,120 +0,0 @@
-<template>
-  <view class="app-container">
-    <!-- 顶部用户信息区域 -->
-    <view class="bg-gradient-to-br from-blue-900 to-blue-800 text-white pt-10 pb-6 px-5">
-      <view class="flex items-center gap-4">
-        <view class="w-14 h-14 bg-white/20 rounded-full flex items-center justify-center text-2xl">
-          <text></text>
-        </view>
-        <view>
-          <text class="text-lg font-bold block">{{ userName || '李调度' }}</text>
-          <text class="text-blue-200 text-xs">调度中心 · 工号 DIS001</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- 功能列表 -->
-    <view class="px-4 -mt-3 space-y-4 pb-4">
-      <!-- 基本信息卡片 -->
-      <view class="bg-white rounded-xl p-4 shadow-sm">
-        <view class="flex justify-between items-center py-2 border-b border-gray-100">
-          <text class="text-gray-500 text-sm"> 部门</text>
-          <text class="text-sm">调度中心</text>
-        </view>
-        <view class="flex justify-between items-center py-2 border-b border-gray-100">
-          <text class="text-gray-500 text-sm">️ 职位</text>
-          <text class="text-sm">调度主管</text>
-        </view>
-        <view class="flex justify-between items-center py-2">
-          <text class="text-gray-500 text-sm"> 电话</text>
-          <text class="text-sm">{{ userPhone || '138****8002' }}</text>
-        </view>
-      </view>
-
-      <!-- 功能入口卡片 -->
-      <view class="bg-white rounded-xl p-4 shadow-sm">
-        <view class="flex justify-between items-center py-3 border-b border-gray-100" @click="goToBasicInfo">
-          <text class="text-sm text-gray-700"> 基本信息</text>
-          <text class="text-gray-300 text-xs">></text>
-        </view>
-        <view class="flex justify-between items-center py-3 border-b border-gray-100" @click="goToChangePassword">
-          <text class="text-sm text-gray-700"> 修改密码</text>
-          <text class="text-gray-300 text-xs">></text>
-        </view>
-        <view class="flex justify-between items-center py-3 border-b border-gray-100" @click="goToNotification">
-          <text class="text-sm text-gray-700"> 消息通知</text>
-          <view class="flex items-center">
-            <view v-if="unreadCount > 0" class="w-2 h-2 bg-red-500 rounded-full mr-2"></view>
-            <text class="text-gray-300 text-xs">></text>
-          </view>
-        </view>
-        <view class="flex justify-between items-center py-3" @click="goToHelpCenter">
-          <text class="text-sm text-gray-700"> 帮助中心</text>
-          <text class="text-gray-300 text-xs">></text>
-        </view>
-      </view>
-
-      <!-- 退出登录按钮 -->
-      <button class="w-full py-3 mt-4 bg-red-500 text-white rounded-xl font-medium" @click="logout">
-        退出登录
-      </button>
-    </view>
-
-    <!-- 底部导航 -->
-    <BottomNav :items="navItems" :currentPath="currentPath" @navigate="onNavChange" />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed, ref } from 'vue'
-import BottomNav from '../../components/common/BottomNav.vue'
-import { useAuthStore } from '../../stores/auth'
-import { useNotificationStore } from '../../stores/notification'
-import { navigateTo, showModal, showToast } from '../../utils'
-
-const currentPath = ref('/pages/dispatch/myPage')
-const authStore = useAuthStore()
-const notificationStore = useNotificationStore()
-
-const userName = computed(() => authStore.userName)
-const userPhone = computed(() => authStore.userInfo?.phone)
-const unreadCount = computed(() => notificationStore.unreadCount)
-
-const navItems = [
-  { text: '首页', path: '/pages/dispatch/home', icon: 'uniui-home-filled' },
-  { text: '任务', path: '/pages/dispatch/taskList', icon: 'uniui-flag-filled' },
-  { text: '我的', path: '/pages/dispatch/myPage', icon: 'uniui-person-filled' },
-]
-
-function onNavChange(path: string) {
-  uni.reLaunch({ url: path })
-}
-
-function goToBasicInfo() {
-  navigateTo('/pages/dispatch/basicInfo')
-}
-
-function goToChangePassword() {
-  navigateTo('/pages/dispatch/changePassword')
-}
-
-function goToNotification() {
-  navigateTo('/pages/dispatch/notification')
-}
-
-function goToHelpCenter() {
-  navigateTo('/pages/dispatch/helpCenter')
-}
-
-async function logout() {
-  const res = await showModal({
-    title: '确认退出',
-    content: '确定要退出登录吗?'
-  })
-  if (res.confirm) {
-    await authStore.doLogout()
-    showToast('已退出登录')
-    uni.reLaunch({ url: '/pages/login/login' })
-  }
-}
-</script>

+ 0 - 93
src/pages/dispatch/taskList.vue

@@ -1,93 +0,0 @@
-
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="任务列表" />
-    
-    <!-- 筛选标签 -->
-    <view class="bg-white px-4 py-3 flex gap-2 overflow-x-auto hide-scrollbar">
-      <view 
-        v-for="status in filterStatuses" 
-        :key="status.value"
-        class="filter-tab"
-        :class="{ active: currentFilter === status.value }"
-        @click="currentFilter = status.value"
-      >
-        {{ status.label }}
-      </view>
-    </view>
-
-    <!-- 任务列表 -->
-    <view class="px-4 mt-3">
-      <view v-if="taskStore.loading" class="py-10 text-center">
-        <text class="text-gray-400">加载中...</text>
-      </view>
-      
-      <view v-else-if="filteredTasks.length === 0">
-        <EmptyState message="暂无任务" />
-      </view>
-      
-      <view v-else class="space-y-3">
-        <TaskCard 
-          v-for="task in filteredTasks" 
-          :key="task.id"
-          :task="task"
-          @click="goToDetail"
-        />
-      </view>
-    </view>
-
-    <!-- 新增任务按钮 -->
-    <view class="fixed bottom-20 right-4">
-      <button 
-        class="w-14 h-14 rounded-full bg-blue-500 text-white flex items-center justify-center shadow-lg text-2xl"
-        @click="goToPublishTask"
-      >
-        +
-      </button>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import { useTaskStore } from '../../stores/task'
-import TopBar from '../../components/common/TopBar.vue'
-import TaskCard from '../../components/task/TaskCard.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-
-const taskStore = useTaskStore()
-const currentFilter = ref('all')
-
-const filterStatuses = [
-  { value: 'all', label: '全部' },
-  { value: 'pending', label: '待确认' },
-  { value: 'confirmed', label: '待排班' },
-  { value: 'scheduled', label: '待出车' },
-  { value: 'constructing', label: '施工中' },
-  { value: 'completed', label: '已完成' },
-]
-
-const filteredTasks = computed(() => {
-  if (currentFilter.value === 'all') return taskStore.tasks
-  return taskStore.tasks.filter(t => t.status === currentFilter.value)
-})
-
-function goToDetail(taskId: string) {
-  taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/pages/dispatch/taskDetail' })
-}
-
-function goToPublishTask() {
-  uni.navigateTo({ url: '/pages/dispatch/publishTask' })
-}
-
-onMounted(() => {
-  taskStore.fetchTasks()
-})
-
-// 下拉刷新
-onPullDownRefresh(async () => {
-  await taskStore.fetchTasks()
-  uni.stopPullDownRefresh()
-})
-</script>

+ 0 - 133
src/pages/dispatch/vehicleList.vue

@@ -1,133 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="车辆列表" show-back />
-    
-    <!-- 筛选标签 - 使用 uni-segmented-control -->
-    <uni-segmented-control 
-      :current="currentFilterIndex" 
-      :values="filterLabels"
-      style-type="text"
-      active-color="#4f8ef7"
-      @clickItem="onFilterChange"
-    />
-
-    <!-- 车辆列表 -->
-    <view class="mt-2">
-      <view v-if="loading" class="py-10 text-center">
-        <text class="text-gray-400">加载中...</text>
-      </view>
-      
-      <view v-else-if="filteredVehicles.length === 0">
-        <EmptyState message="暂无车辆" />
-      </view>
-      
-      <uni-list v-else border>
-        <uni-list-item 
-          v-for="vehicle in filteredVehicles" 
-          :key="vehicle.vehicleId"
-          :title="vehicle.plateNo"
-          :note="vehicle.model + ' | ' + (vehicle.driverName || '未分配司机')"
-          :right-text="getStatusText(vehicle.status)"
-          show-arrow
-          @click="showVehicleDetail(vehicle)"
-        >
-          <template #header>
-            <view class="w-10 h-10 rounded-xl flex items-center justify-center mr-3" :class="getStatusBgClass(vehicle.status)">
-              <text class="uni-icons uniui-cart-filled text-lg" :class="getStatusIconClass(vehicle.status)"></text>
-            </view>
-          </template>
-        </uni-list-item>
-      </uni-list>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import EmptyState from '../../components/common/EmptyState.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-import UniSegmentedControl from '../../components/uni-segmented-control/uni-segmented-control.vue'
-import { getVehicleList } from '../../api/vehicle'
-
-const loading = ref(false)
-const vehicles = ref<any[]>([])
-const currentFilterIndex = ref(0)
-
-const filterStatuses = [
-  { value: 'all', label: '全部' },
-  { value: 'available', label: '可用' },
-  { value: 'in_use', label: '使用中' },
-  { value: 'maintenance', label: '维修中' },
-]
-
-const filterLabels = filterStatuses.map(s => s.label)
-
-const filteredVehicles = computed(() => {
-  const filterValue = filterStatuses[currentFilterIndex.value].value
-  if (filterValue === 'all') return vehicles.value
-  return vehicles.value.filter(v => v.status === filterValue)
-})
-
-function getStatusText(status: string) {
-  const map: Record<string, string> = {
-    available: '可用',
-    in_use: '使用中',
-    maintenance: '维修中',
-  }
-  return map[status] || status
-}
-
-function getStatusBgClass(status: string) {
-  const map: Record<string, string> = {
-    available: 'bg-green-50',
-    in_use: 'bg-blue-50',
-    maintenance: 'bg-orange-50',
-  }
-  return map[status] || 'bg-gray-50'
-}
-
-function getStatusIconClass(status: string) {
-  const map: Record<string, string> = {
-    available: 'text-green-500',
-    in_use: 'text-blue-500',
-    maintenance: 'text-orange-500',
-  }
-  return map[status] || 'text-gray-400'
-}
-
-function onFilterChange(index: number) {
-  currentFilterIndex.value = index
-}
-
-function showVehicleDetail(vehicle: any) {
-  uni.showModal({
-    title: vehicle.plateNo,
-    content: `型号: ${vehicle.model}\n类型: ${vehicle.vehicleType}\n司机: ${vehicle.driverName || '未分配'}\n状态: ${getStatusText(vehicle.status)}`,
-    showCancel: false
-  })
-}
-
-onMounted(async () => {
-  await fetchVehicles()
-})
-
-async function fetchVehicles() {
-  loading.value = true
-  try {
-    const res = await getVehicleList()
-    vehicles.value = res.rows || []
-  } catch (error) {
-    console.error('获取车辆列表失败:', error)
-  } finally {
-    loading.value = false
-  }
-}
-
-// 下拉刷新
-onPullDownRefresh(async () => {
-  await fetchVehicles()
-  uni.stopPullDownRefresh()
-})
-</script>

+ 18 - 0
src/pages/home/home.vue

@@ -0,0 +1,18 @@
+<template>
+  <view class="app-container">
+    <SalesHome v-if="role === 'sales'" />
+    <DispatchHome v-else-if="role === 'dispatch'" />
+    <ConstructionHome v-else />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useAuthStore } from '@/stores/auth'
+import SalesHome from '@/components/home/SalesHome.vue'
+import DispatchHome from '@/components/home/DispatchHome.vue'
+import ConstructionHome from '@/components/home/ConstructionHome.vue'
+
+const authStore = useAuthStore()
+const role = computed(() => authStore.userRole || 'sales')
+</script>

+ 1 - 6
src/pages/index/index.vue

@@ -19,12 +19,7 @@ onLoad(async () => {
   await delay(500)
   authStore.checkLogin()
   if (authStore.isLoggedIn && authStore.userRole) {
-    const homeMap: Record<string, string> = {
-      sales: '/pages/sales/home',
-      dispatch: '/pages/dispatch/home',
-      construction: '/pages/construction/home'
-    }
-    uni.reLaunch({ url: homeMap[authStore.userRole] || '/pages/login/login' })
+    uni.switchTab({ url: '/pages/home/home' })
   } else {
     uni.reLaunch({ url: '/pages/login/login' })
   }

+ 52 - 73
src/pages/login/login.vue

@@ -1,63 +1,65 @@
 
 <template>
-  <view class="app-container min-h-screen bg-gradient-to-b from-blue-500 to-blue-600 flex flex-col items-center justify-center px-8">
+  <view class="app-container min-h-screen bg-background flex flex-col items-center justify-center px-8">
     <!-- Logo -->
     <view class="mb-8 text-center">
-      <view class="w-20 h-20 bg-white rounded-2xl flex items-center justify-center mb-4 mx-auto shadow-lg">
-        <text class="text-4xl"></text>
+      <view class="w-20 h-20 bg-primary rounded-2xl flex items-center justify-center mb-4 mx-auto shadow-card">
+        <text class="text-3xl font-bold text-white">清</text>
+      </view>
+      <text class="text-gray-800 text-2xl font-bold">清道夫系统</text>
+      <text class="text-gray-500 text-sm mt-2 block">环境服务管理平台</text>
+    </view>
+
+    <!-- 角色选择 -->
+    <view class="w-full mb-4">
+      <view class="flex bg-surface rounded-xl p-1 border border-gray-200">
+        <view
+          v-for="item in roleOptions"
+          :key="item.value"
+          class="flex-1 py-2 text-center text-sm font-medium rounded-lg transition-all"
+          :class="selectedRole === item.value ? 'bg-primary text-white shadow-card' : 'text-gray-500'"
+          @click="selectedRole = item.value"
+        >
+          {{ item.label }}
+        </view>
       </view>
-      <text class="text-white text-2xl font-bold">清道夫系统</text>
-      <text class="text-blue-100 text-sm mt-2 block">环境服务管理平台</text>
     </view>
 
     <!-- 登录表单 -->
-    <view class="w-full bg-white rounded-2xl p-6 shadow-xl">
+    <view class="w-full bg-white rounded-2xl p-6 shadow-card border border-gray-200">
       <text class="text-gray-800 text-lg font-semibold mb-6 block text-center">账号登录</text>
-      
-      <view class="space-y-4">
+
+      <view class="gap-y-4">
         <!-- 用户名 -->
         <view>
-          <text class="text-sm text-gray-600 block mb-2">用户名</text>
-          <view class="flex items-center bg-gray-100 rounded-xl px-3 py-3">
+          <text class="text-sm text-gray-500 block mb-2">用户名</text>
+          <view class="flex items-center bg-surface rounded-xl px-3 py-3 border border-gray-200">
             <text class="text-gray-400 mr-2"></text>
-            <input 
-              class="flex-1 bg-transparent text-sm" 
+            <input
+              class="flex-1 bg-transparent text-sm text-gray-800"
               placeholder="请输入用户名"
               v-model="form.username"
             />
           </view>
         </view>
-        
+
         <!-- 密码 -->
         <view>
-          <text class="text-sm text-gray-600 block mb-2">密码</text>
-          <view class="flex items-center bg-gray-100 rounded-xl px-3 py-3">
+          <text class="text-sm text-gray-500 block mb-2">密码</text>
+          <view class="flex items-center bg-surface rounded-xl px-3 py-3 border border-gray-200">
             <text class="text-gray-400 mr-2"></text>
-            <input 
-              class="flex-1 bg-transparent text-sm" 
+            <input
+              class="flex-1 bg-transparent text-sm text-gray-800"
               placeholder="请输入密码"
               type="password"
               v-model="form.password"
             />
           </view>
         </view>
-
-        <!-- 记住密码 -->
-        <view class="flex items-center justify-between">
-          <view class="flex items-center" @click="rememberMe = !rememberMe">
-            <view 
-              class="w-5 h-5 rounded border-2 flex items-center justify-center mr-2 transition-colors"
-              :class="rememberMe ? 'bg-blue-500 border-blue-500' : 'border-gray-300'"
-            >
-              <text v-if="rememberMe" class="text-white text-xs"></text>
-            </view>
-            <text class="text-sm text-gray-600">记住密码</text>
-          </view>
-        </view>
       </view>
 
       <button
-        class="w-full mt-6 py-3.5 rounded-xl text-white font-medium text-base login-btn"
+        class="w-full mt-6 py-3 rounded-xl text-white font-medium text-base bg-primary"
         :class="canLogin ? 'opacity-100' : 'opacity-50'"
         :disabled="!canLogin || loading"
         @click="handleLogin"
@@ -72,7 +74,7 @@
     </view>
 
     <!-- 版本号 -->
-    <text class="text-blue-200 text-xs mt-6">v1.0.0</text>
+    <text class="text-gray-400 text-xs mt-6">v1.0.0</text>
   </view>
 </template>
 
@@ -83,27 +85,32 @@ import { showToast } from '../../utils'
 
 const authStore = useAuthStore()
 
+interface RoleOption {
+  label: string
+  value: 'sales' | 'dispatch' | 'construction'
+}
+
+const roleOptions: RoleOption[] = [
+  { label: '销售端', value: 'sales' },
+  { label: '调度端', value: 'dispatch' },
+  { label: '施工端', value: 'construction' },
+]
+
+const selectedRole = ref<RoleOption['value']>('sales')
+
 const form = ref({
   username: '',
   password: '',
 })
 
 const loading = ref(false)
-const rememberMe = ref(false)
 
 const canLogin = computed(() => form.value.username.trim() && form.value.password.trim())
 
 onMounted(() => {
-  // 读取记住的账号密码
-  const savedUsername = uni.getStorageSync('remember_username')
-  const savedPassword = uni.getStorageSync('remember_password')
-  const savedRemember = uni.getStorageSync('remember_me')
-  
-  if (savedRemember === 'true' && savedUsername) {
-    form.value.username = savedUsername
-    form.value.password = savedPassword || ''
-    rememberMe.value = true
-  }
+  uni.removeStorageSync('remember_username')
+  uni.removeStorageSync('remember_password')
+  uni.removeStorageSync('remember_me')
 })
 
 async function handleLogin() {
@@ -111,28 +118,8 @@ async function handleLogin() {
 
   loading.value = true
   try {
-    const role = await authStore.doLogin(form.value.username, form.value.password)
-
-    // 处理记住密码
-    if (rememberMe.value) {
-      uni.setStorageSync('remember_username', form.value.username)
-      uni.setStorageSync('remember_password', form.value.password)
-      uni.setStorageSync('remember_me', 'true')
-    } else {
-      uni.removeStorageSync('remember_username')
-      uni.removeStorageSync('remember_password')
-      uni.setStorageSync('remember_me', 'false')
-    }
-
-    // 根据角色跳转到对应首页
-    const homeMap: Record<string, string> = {
-      sales: '/pages/sales/home',
-      dispatch: '/pages/dispatch/home',
-      construction: '/pages/construction/home',
-    }
-
-    const homeUrl = homeMap[role] || '/pages/sales/home'
-    uni.reLaunch({ url: homeUrl })
+    await authStore.doLogin(form.value.username, form.value.password, selectedRole.value)
+    uni.switchTab({ url: '/pages/home/home' })
   } catch (error: any) {
     showToast({ title: error.message || '登录失败', icon: 'error' })
   } finally {
@@ -142,12 +129,4 @@ async function handleLogin() {
 </script>
 
 <style scoped>
-.login-btn {
-  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
-}
-
-.login-btn:active {
-  transform: scale(0.98);
-  opacity: 0.9;
-}
 </style>

+ 9 - 0
src/pages/my/my.vue

@@ -0,0 +1,9 @@
+<template>
+  <view class="app-container">
+    <ProfileView />
+  </view>
+</template>
+
+<script setup lang="ts">
+import ProfileView from '@/components/my/ProfileView.vue'
+</script>

+ 0 - 83
src/pages/sales/basicInfo.vue

@@ -1,83 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="基本信息" show-back />
-
-    <view class="px-4 py-4 space-y-4">
-      <!-- 头像区域 -->
-      <uni-card :is-shadow="true">
-        <view class="flex items-center py-2">
-          <view class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mr-4">
-            <text class="uni-icons uniui-person-filled text-blue-500 text-2xl"></text>
-          </view>
-          <view>
-            <text class="text-lg font-semibold text-gray-800 block">{{ userInfo?.name || '-' }}</text>
-            <text class="text-sm text-gray-500">{{ userInfo?.department || '销售部' }} · {{ roleText }}</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 基本信息 -->
-      <uni-card title="基本信息" icon="uniui-person-filled" :is-shadow="true">
-        <uni-list border="false">
-          <uni-list-item title="姓名" :note="userInfo?.name || '-'" />
-          <uni-list-item title="工号" :note="userInfo?.id || '-'" />
-          <uni-list-item title="部门" :note="userInfo?.department || '销售部'" />
-          <uni-list-item title="职位" :note="roleText" />
-          <uni-list-item title="电话" :note="maskedPhone" />
-          <uni-list-item title="邮箱" :note="email" />
-          <uni-list-item title="状态" :right-text="'在职'" right-color="#34c759" />
-        </uni-list>
-      </uni-card>
-
-      <!-- 操作按钮 -->
-      <view class="pt-2">
-        <button
-          class="w-full py-3 bg-blue-500 text-white rounded-xl font-medium flex items-center justify-center"
-          @click="handleEdit"
-        >
-          <text class="uni-icons uniui-compose mr-2"></text>
-          编辑信息
-        </button>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-import { useAuthStore } from '../../stores/auth'
-
-const authStore = useAuthStore()
-
-const userInfo = computed(() => authStore.userInfo)
-
-const roleText = computed(() => {
-  const roleMap: Record<string, string> = {
-    sales: '销售经理',
-    dispatch: '调度员',
-    construction: '施工员'
-  }
-  return roleMap[authStore.userRole] || '销售经理'
-})
-
-const maskedPhone = computed(() => {
-  const phone = userInfo.value?.phone || ''
-  if (phone.length === 11) {
-    return phone.slice(0, 3) + '****' + phone.slice(7)
-  }
-  return phone || '-'
-})
-
-const email = computed(() => {
-  const name = userInfo.value?.name || 'user'
-  return `${name.toLowerCase()}@qingdaofu.com`
-})
-
-function handleEdit() {
-  uni.showToast({ title: '编辑功能开发中', icon: 'none' })
-}
-</script>

+ 0 - 120
src/pages/sales/changePassword.vue

@@ -1,120 +0,0 @@
-<template>
-  <view class="app-container pb-20">
-    <TopBar title="修改密码" show-back />
-
-    <view class="px-4 py-4">
-      <uni-card title="密码修改" icon="uniui-locked-filled" :is-shadow="true">
-        <view class="space-y-4 pt-2">
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 当前密码
-            </text>
-            <input
-              v-model="form.currentPassword"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-              type="password"
-              placeholder="请输入当前密码"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 新密码
-            </text>
-            <input
-              v-model="form.newPassword"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-              type="password"
-              placeholder="请输入新密码(6-20位)"
-            />
-          </view>
-          <view>
-            <text class="text-sm text-gray-600 block mb-2">
-              <text class="text-red-500">*</text> 确认新密码
-            </text>
-            <input
-              v-model="form.confirmPassword"
-              class="w-full px-4 py-3 border border-gray-200 rounded-lg text-sm text-gray-800 bg-white"
-              type="password"
-              placeholder="请再次输入新密码"
-            />
-          </view>
-        </view>
-
-        <view class="mt-4">
-          <view class="bg-blue-50 rounded-lg p-3 mb-4">
-            <text class="text-blue-600 text-sm block font-medium">密码要求:</text>
-            <view class="text-gray-500 text-xs mt-2 space-y-1">
-              <text class="block">• 长度6-20位</text>
-              <text class="block">• 必须包含字母和数字</text>
-              <text class="block">• 不能与当前密码相同</text>
-            </view>
-          </view>
-
-          <button
-            class="w-full py-3 bg-blue-500 text-white rounded-xl font-medium"
-            @click="handleSubmit"
-          >
-            确认修改
-          </button>
-        </view>
-      </uni-card>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { reactive } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-
-const form = reactive({
-  currentPassword: '',
-  newPassword: '',
-  confirmPassword: ''
-})
-
-function validateForm(): boolean {
-  if (!form.currentPassword) {
-    uni.showToast({ title: '请输入当前密码', icon: 'none' })
-    return false
-  }
-  if (!form.newPassword) {
-    uni.showToast({ title: '请输入新密码', icon: 'none' })
-    return false
-  }
-  if (form.newPassword.length < 6 || form.newPassword.length > 20) {
-    uni.showToast({ title: '密码长度应为6-20位', icon: 'none' })
-    return false
-  }
-  if (!/^(?=.*[a-zA-Z])(?=.*\d).+$/.test(form.newPassword)) {
-    uni.showToast({ title: '密码必须包含字母和数字', icon: 'none' })
-    return false
-  }
-  if (form.newPassword === form.currentPassword) {
-    uni.showToast({ title: '新密码不能与当前密码相同', icon: 'none' })
-    return false
-  }
-  if (form.newPassword !== form.confirmPassword) {
-    uni.showToast({ title: '两次输入的新密码不一致', icon: 'none' })
-    return false
-  }
-  return true
-}
-
-async function handleSubmit() {
-  if (!validateForm()) return
-
-  uni.showModal({
-    title: '确认修改',
-    content: '确定要修改密码吗?修改后需要重新登录。',
-    success: (res) => {
-      if (res.confirm) {
-        uni.showToast({ title: '密码修改成功', icon: 'success' })
-        form.currentPassword = ''
-        form.newPassword = ''
-        form.confirmPassword = ''
-      }
-    }
-  })
-}
-</script>

+ 0 - 124
src/pages/sales/helpCenter.vue

@@ -1,124 +0,0 @@
-<template>
-  <view class="app-container">
-    <TopBar title="帮助中心" show-back />
-
-    <view class="px-4 py-4">
-      <!-- 常见问题 -->
-      <view class="card p-4 mb-4">
-        <view class="flex items-center mb-4">
-          <text class="text-blue-500 mr-2"></text>
-          <text class="text-base font-semibold text-gray-800">常见问题</text>
-        </view>
-
-        <view class="space-y-3">
-          <view
-            v-for="faq in faqs"
-            :key="faq.id"
-            class="border-b border-gray-100 pb-3 last:border-b-0 last:pb-0"
-          >
-            <view
-              class="flex justify-between items-center"
-              @click="toggleFaq(faq.id)"
-            >
-              <text class="text-sm text-gray-700 flex-1">{{ faq.question }}</text>
-              <text
-                class="text-gray-400 text-xs ml-2 transition-transform"
-                :class="{ 'rotate-180': expandedFaq === faq.id }"
-              >
-                {{ expandedFaq === faq.id ? '' : '' }}
-              </text>
-            </view>
-            <view
-              v-if="expandedFaq === faq.id"
-              class="mt-2 text-gray-500 text-sm bg-gray-50 p-3 rounded-lg"
-            >
-              {{ faq.answer }}
-            </view>
-          </view>
-        </view>
-      </view>
-
-      <!-- 知识库 -->
-      <view class="card p-4 mb-4">
-        <view class="flex items-center mb-4">
-          <text class="text-green-500 mr-2"></text>
-          <text class="text-base font-semibold text-gray-800">知识库</text>
-        </view>
-
-        <view class="space-y-3">
-          <view
-            v-for="knowledge in knowledges"
-            :key="knowledge.id"
-            class="flex items-start py-2 border-b border-gray-100 last:border-b-0"
-            @click="showKnowledgeDetail(knowledge)"
-          >
-            <view class="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center mr-3 flex-shrink-0">
-              <text class="text-blue-400"></text>
-            </view>
-            <view class="flex-1">
-              <text class="text-sm text-gray-800 block">{{ knowledge.title }}</text>
-              <text class="text-xs text-gray-400 mt-1 block">{{ knowledge.summary }}</text>
-            </view>
-            <text class="text-gray-300 text-xs ml-2">›</text>
-          </view>
-        </view>
-      </view>
-
-      <!-- 联系客服 -->
-      <view class="card p-4">
-        <view class="flex items-center mb-4">
-          <text class="text-green-500 mr-2"></text>
-          <text class="text-base font-semibold text-gray-800">联系客服</text>
-        </view>
-        <view class="flex items-center justify-between py-3 border-b border-gray-100">
-          <view class="flex items-center">
-            <text class="text-blue-500 mr-3"></text>
-            <text class="text-sm text-gray-700">客服热线</text>
-          </view>
-          <text class="text-blue-500 text-sm">400-888-8888</text>
-        </view>
-        <view class="flex items-center justify-between py-3 border-b border-gray-100">
-          <view class="flex items-center">
-            <text class="text-blue-500 mr-3">️</text>
-            <text class="text-sm text-gray-700">电子邮箱</text>
-          </view>
-          <text class="text-gray-600 text-sm">service@qingdaofu.com</text>
-        </view>
-        <view class="flex items-center justify-between py-3">
-          <view class="flex items-center">
-            <text class="text-blue-500 mr-3"></text>
-            <text class="text-sm text-gray-700">服务时间</text>
-          </view>
-          <text class="text-gray-600 text-sm">周一至周五 9:00-18:00</text>
-        </view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue'
-import TopBar from '../../components/common/TopBar.vue'
-import { useCommonStore } from '../../stores/common'
-import { showToast, showModal } from '../../utils'
-import type { Knowledge } from '../../types'
-
-const commonStore = useCommonStore()
-
-const faqs = ref(commonStore.faqs)
-const knowledges = ref(commonStore.knowledges)
-const expandedFaq = ref<string | null>(null)
-
-function toggleFaq(id: string) {
-  expandedFaq.value = expandedFaq.value === id ? null : id
-}
-
-async function showKnowledgeDetail(knowledge: Knowledge) {
-  await showModal({
-    title: knowledge.title,
-    content: knowledge.content,
-    showCancel: false,
-    confirmText: '知道了'
-  })
-}
-</script>

+ 0 - 210
src/pages/sales/home.vue

@@ -1,210 +0,0 @@
-<template>
-  <view class="app-container">
-    <!-- 顶部区域 -->
-    <view class="relative overflow-hidden">
-      <view class="absolute inset-0 bg-gradient-to-br from-blue-500 to-blue-700"></view>
-      <view class="absolute -top-20 -right-20 w-60 h-60 bg-white opacity-5 rounded-full"></view>
-      <view class="absolute -bottom-10 -left-10 w-40 h-40 bg-white opacity-5 rounded-full"></view>
-      
-      <view class="relative px-5 pt-12 pb-8">
-        <view class="flex items-center justify-between">
-          <view class="flex items-center">
-            <view class="w-12 h-12 bg-white/20 backdrop-blur rounded-2xl flex items-center justify-center mr-3">
-              <text class="uni-icons uniui-person-filled text-white text-xl"></text>
-            </view>
-            <view>
-              <text class="text-blue-100 text-xs block">下午好</text>
-              <text class="text-white text-lg font-bold">{{ userName }}</text>
-            </view>
-          </view>
-          <button class="w-10 h-10 bg-white/15 backdrop-blur rounded-full flex items-center justify-center relative" @click="goToNotification">
-            <text class="uni-icons uniui-notification-filled text-white text-lg"></text>
-          </button>
-        </view>
-      </view>
-    </view>
-
-    <!-- 主内容区域 -->
-    <view class="px-4 -mt-5 space-y-4 pb-24">
-      <!-- 统计卡片 -->
-      <uni-card title="数据概览" :is-shadow="true" :is-full="false">
-        <view class="grid grid-cols-3 gap-3">
-          <view class="bg-blue-50 rounded-2xl p-4 text-center" @click="goToProjectList">
-            <text class="stat-number text-blue-600 block">{{ projectStore.totalCount }}</text>
-            <text class="text-desc mt-1 block">本月项目</text>
-          </view>
-          <view class="bg-orange-50 rounded-2xl p-4 text-center" @click="goToTaskList">
-            <text class="stat-number text-orange-500 block">{{ taskStore.pendingCount }}</text>
-            <text class="text-desc mt-1 block">待确认</text>
-          </view>
-          <view class="bg-green-50 rounded-2xl p-4 text-center" @click="goToTaskList">
-            <text class="stat-number text-green-500 block">{{ taskStore.todayCount }}</text>
-            <text class="text-desc mt-1 block">进行中</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 快捷入口 -->
-      <uni-card title="快捷功能" :is-shadow="true">
-        <view class="grid grid-cols-4 gap-4">
-          <view class="flex flex-col items-center" @click="goToProjectList">
-            <view class="w-14 h-14 bg-blue-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-folder-add-filled text-blue-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">项目库</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToTaskList">
-            <view class="w-14 h-14 bg-green-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-flag-filled text-green-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">任务</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToTaskReminder">
-            <view class="w-14 h-14 bg-orange-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-notification-filled text-orange-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">提醒</text>
-          </view>
-          <view class="flex flex-col items-center" @click="goToNoticeList">
-            <view class="w-14 h-14 bg-purple-50 rounded-2xl flex items-center justify-center mb-2 transition-transform active:scale-95">
-              <text class="uni-icons uniui-sound-filled text-purple-500 text-2xl"></text>
-            </view>
-            <text class="text-xs text-gray-600 font-medium">公告</text>
-          </view>
-        </view>
-      </uni-card>
-
-      <!-- 最新任务 -->
-      <uni-card 
-        title="最新任务" 
-        extra="查看全部" 
-        :is-shadow="true"
-        @click-extra="goToTaskList"
-      >
-        <view v-if="taskStore.tasks.length === 0" class="py-6 text-center">
-          <text class="text-desc">暂无任务</text>
-        </view>
-        <uni-list v-else border="false">
-          <uni-list-item 
-            v-for="task in taskStore.tasks.slice(0, 3)" 
-            :key="task.id"
-            :title="task.projectName"
-            :note="task.scheduleDate"
-            show-arrow
-            @click="goToTaskDetail(task.id)"
-          >
-            <template #header>
-              <view class="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center mr-3">
-                <text class="uni-icons uniui-flag text-blue-500"></text>
-              </view>
-            </template>
-            <template #footer>
-              <StatusTag :status="task.status" />
-            </template>
-          </uni-list-item>
-        </uni-list>
-      </uni-card>
-
-      <!-- 通知公告 -->
-      <uni-card 
-        title="通知公告" 
-        extra="更多" 
-        :is-shadow="true"
-        @click-extra="goToNoticeList"
-      >
-        <uni-list border="false">
-          <uni-list-item 
-            v-for="(notice, index) in notices" 
-            :key="index"
-            :title="notice.title"
-            :note="notice.date"
-            show-arrow
-            @click="goToNoticeDetail"
-          >
-            <template #header>
-              <view class="w-2 h-2 rounded-full mt-1.5 mr-2 flex-shrink-0" :class="index === 0 ? 'bg-red-400' : 'bg-gray-300'"></view>
-            </template>
-          </uni-list-item>
-        </uni-list>
-      </uni-card>
-    </view>
-
-    <!-- 底部导航 -->
-    <BottomNav :items="navItems" :currentPath="currentPath" @navigate="onNavChange" />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, onMounted } from 'vue'
-import { useAuthStore } from '../../stores/auth'
-import { useProjectStore } from '../../stores/project'
-import { useTaskStore } from '../../stores/task'
-import BottomNav from '../../components/common/BottomNav.vue'
-import StatusTag from '../../components/common/StatusTag.vue'
-import UniCard from '../../components/uni-card/uni-card.vue'
-import UniList from '../../components/uni-list/uni-list.vue'
-import UniListItem from '../../components/uni-list/uni-list-item.vue'
-
-const authStore = useAuthStore()
-const projectStore = useProjectStore()
-const taskStore = useTaskStore()
-
-const userName = ref(authStore.user?.name || '张销售')
-const currentPath = ref('/pages/sales/home')
-
-const navItems = [
-  { name: '首页', path: '/pages/sales/home', icon: 'uniui-home-filled' },
-  { name: '任务', path: '/pages/common/taskList', icon: 'uniui-flag-filled' },
-  { name: '我的', path: '/pages/common/profile', icon: 'uniui-person-filled' },
-]
-
-const notices = [
-  { title: '关于2024年第一季度销售激励方案的通知', date: '2024-01-15' },
-  { title: '春节期间服务安排调整公告', date: '2024-01-10' },
-]
-
-function onNavChange(path: string) {
-  uni.reLaunch({ url: path })
-}
-
-function goToProjectList() {
-  uni.navigateTo({ url: '/pages/common/projectList' })
-}
-
-function goToTaskList() {
-  uni.navigateTo({ url: '/pages/common/taskList' })
-}
-
-function goToTaskDetail(taskId: string) {
-  taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/pages/common/taskDetail' })
-}
-
-function goToTaskReminder() {
-  uni.navigateTo({ url: '/pages/sales/taskReminder' })
-}
-
-function goToNotification() {
-  uni.navigateTo({ url: '/pages/sales/notification' })
-}
-
-function goToNoticeList() {
-  uni.navigateTo({ url: '/pages/common/noticeList' })
-}
-
-function goToNoticeDetail() {
-  uni.navigateTo({ url: '/pages/common/noticeDetail' })
-}
-
-onMounted(() => {
-  projectStore.fetchProjects()
-  taskStore.fetchTasks()
-})
-</script>
-
-<style scoped>
-.stat-number {
-  font-size: 24px;
-  font-weight: 800;
-}
-</style>

+ 7 - 0
src/pages/schedule/schedule.vue

@@ -0,0 +1,7 @@
+<template>
+  <ScheduleView :show-top-bar="false" :show-back="false" :show-status-bar="true" />
+</template>
+
+<script setup lang="ts">
+import ScheduleView from '@/components/dispatch/ScheduleView.vue'
+</script>

+ 32 - 0
src/pages/task/task.vue

@@ -0,0 +1,32 @@
+<template>
+  <view class="app-container">
+    <TaskListView ref="taskListRef" :role="role" />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { onLoad, onShow, onPullDownRefresh } from '@dcloudio/uni-app'
+import { useAuthStore } from '@/stores/auth'
+import { useTaskStore } from '@/stores/task'
+import TaskListView from '@/components/task/TaskListView.vue'
+
+const authStore = useAuthStore()
+const taskStore = useTaskStore()
+const taskListRef = ref<InstanceType<typeof TaskListView> | null>(null)
+
+const role = computed(() => (authStore.userRole as 'sales' | 'dispatch' | 'construction') || 'sales')
+
+onLoad(() => {
+  taskStore.fetchTasks()
+})
+
+onShow(() => {
+  taskStore.fetchTasks()
+})
+
+onPullDownRefresh(async () => {
+  await taskStore.fetchTasks()
+  uni.stopPullDownRefresh()
+})
+</script>

BIN
src/static/tabbar/home-active.png


BIN
src/static/tabbar/home.png


BIN
src/static/tabbar/my-active.png


BIN
src/static/tabbar/my.png


BIN
src/static/tabbar/task-active.png


BIN
src/static/tabbar/task.png


+ 14 - 5
src/stores/auth.ts

@@ -15,8 +15,8 @@ export const useAuthStore = defineStore('auth', () => {
   const userInfo = computed(() => user.value)
 
   // Actions
-  async function doLogin(username: string, password: string) {
-    const res = await login({ username, password })
+  async function doLogin(username: string, password: string, selectedRole?: 'sales' | 'dispatch' | 'construction') {
+    const res = await login({ username, password, roleType: selectedRole })
     
     // 后端 roleType: front/dispatch/construction
     // 前端 role: sales/dispatch/construction
@@ -26,13 +26,22 @@ export const useAuthStore = defineStore('auth', () => {
       construction: 'construction',
     }
 
+    // 优先使用后端返回的角色,如果后端没有返回则使用用户选择的角色
+    const backendRole = roleMap[res.roleType]
+    const finalRole = backendRole || selectedRole || 'sales'
+
     const userData: User = {
       id: String(res.userId),
       name: res.nickName || res.userName,
-      phone: '',
+      phone: res.phonenumber || '',
       avatar: res.avatar,
-      role: roleMap[res.roleType] || 'sales',
-      department: '',
+      role: finalRole,
+      department: res.dept?.deptName || '',
+      position: '',
+      employeeNo: '',
+      email: res.email || '',
+      hireDate: '',
+      team: '',
       status: 'active',
     }
 

+ 8 - 1
src/stores/project.ts

@@ -1,7 +1,7 @@
 import { defineStore } from 'pinia'
 import { ref, computed } from 'vue'
 import type { Project } from '../types'
-import { getProjectList } from '../api/project'
+import { getProjectList, deleteProject as apiDeleteProject } from '../api/project'
 
 export const useProjectStore = defineStore('project', () => {
   // State
@@ -75,6 +75,12 @@ export const useProjectStore = defineStore('project', () => {
     total.value++
   }
 
+  async function deleteProject(projectId: string) {
+    await apiDeleteProject(Number(projectId))
+    projects.value = projects.value.filter(p => p.id !== projectId)
+    total.value--
+  }
+
   return {
     projects,
     currentProject,
@@ -93,5 +99,6 @@ export const useProjectStore = defineStore('project', () => {
     setFilter,
     setCurrentProject,
     addProject,
+    deleteProject,
   }
 })

+ 22 - 2
src/stores/task.ts

@@ -18,8 +18,18 @@ export const useTaskStore = defineStore('task', () => {
   })
 
   const pendingCount = computed(() => tasks.value.filter(t => t.status === 'pending').length)
+  const confirmedCount = computed(() => tasks.value.filter(t => t.status === 'confirmed').length)
+  const scheduledCount = computed(() => tasks.value.filter(t => t.status === 'scheduled').length)
   const todayCount = computed(() => tasks.value.filter(t => ['scheduled', 'departed', 'constructing'].includes(t.status)).length)
+  const processingCount = computed(() => todayCount.value)
   const completedCount = computed(() => tasks.value.filter(t => t.status === 'completed').length)
+  const rejectedCount = computed(() => tasks.value.filter(t => t.status === 'rejected').length)
+  const emergencyCount = computed(() => tasks.value.filter(t => t.type === 'emergency').length)
+  const departedCount = computed(() => tasks.value.filter(t => t.status === 'departed').length)
+  const arrivedCount = computed(() => tasks.value.filter(t => t.status === 'arrived').length)
+  const inspectingCount = computed(() => tasks.value.filter(t => t.status === 'inspecting').length)
+  const tomorrowCount = ref(2)
+  const historyCount = computed(() => completedCount.value)
 
   // Actions
   async function fetchTasks(pageNum = 1, pageSize = 10) {
@@ -37,16 +47,16 @@ export const useTaskStore = defineStore('task', () => {
     loading.value = true
     try {
       const res = await getTaskDetail(taskId)
-      currentTask.value = res
       // 转换日志为时间线
       if (res.logList) {
-        currentTask.value.timeline = res.logList.map((log: any) => ({
+        res.timeline = res.logList.map((log: any) => ({
           time: log.createTime,
           action: log.action,
           operator: log.operatorName,
           role: log.operatorRole || 'dispatch',
         }))
       }
+      currentTask.value = res
     } finally {
       loading.value = false
     }
@@ -74,8 +84,18 @@ export const useTaskStore = defineStore('task', () => {
     total,
     filteredTasks,
     pendingCount,
+    confirmedCount,
+    scheduledCount,
     todayCount,
+    processingCount,
     completedCount,
+    rejectedCount,
+    emergencyCount,
+    departedCount,
+    arrivedCount,
+    inspectingCount,
+    tomorrowCount,
+    historyCount,
     fetchTasks,
     fetchTaskDetail,
     setFilter,

Неке датотеке нису приказане због велике количине промена