docs: add stage 1.7 implementation plan
This commit is contained in:
@@ -0,0 +1,620 @@
|
|||||||
|
# Stage 1.7 Ticket Experience Visual Redesign Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Redesign ticket list, detail, and create pages into a polished Chinese enterprise support experience using shadcn/ui, with minimal backend support for pagination, filters, display users, and transition remarks.
|
||||||
|
|
||||||
|
**Architecture:** Backend keeps the existing Django Ninja ticket router and adds a paginated response shape plus richer filters and display user serialization. Frontend keeps the Vite SPA and current app shell, then decomposes ticket pages into small shadcn-composed components for metrics, filters, metadata, and transition dialogs.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, Django, Django Ninja, pytest, ruff, React, Vite, TypeScript, Tailwind CSS v3, shadcn/ui, Radix UI, npm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source Spec
|
||||||
|
|
||||||
|
- `docs/superpowers/specs/2026-07-01-stage-1-7-ticket-experience-visual-redesign-design.md`
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
Backend:
|
||||||
|
|
||||||
|
- Modify: `backend/apps/tickets/schemas.py`
|
||||||
|
- Modify: `backend/apps/tickets/api.py`
|
||||||
|
- Modify: `backend/apps/tickets/services.py`
|
||||||
|
- Modify: `backend/apps/tickets/tests/test_ticket_api.py`
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
|
||||||
|
- Modify: `frontend/src/features/tickets/types.ts`
|
||||||
|
- Modify: `frontend/src/features/tickets/api.ts`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketListPage.tsx`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketComments.tsx`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketMetricCards.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketFilterBar.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketMetaPanel.tsx`
|
||||||
|
|
||||||
|
## Task 1: Backend Ticket List Support
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/apps/tickets/schemas.py`
|
||||||
|
- Modify: `backend/apps/tickets/api.py`
|
||||||
|
- Modify: `backend/apps/tickets/services.py`
|
||||||
|
- Modify: `backend/apps/tickets/tests/test_ticket_api.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing pagination/filter/display tests**
|
||||||
|
|
||||||
|
Add these tests to `backend/apps/tickets/tests/test_ticket_api.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_tickets_returns_paginated_response_with_display_users(admin_client, db):
|
||||||
|
user_model = get_user_model()
|
||||||
|
requester = user_model.objects.create_user(username="requester", first_name="请求人")
|
||||||
|
assignee = user_model.objects.create_user(username="assignee", first_name="处理人")
|
||||||
|
Ticket.objects.create(
|
||||||
|
ticket_no="TK202607010100",
|
||||||
|
title="Payment failed",
|
||||||
|
description="A",
|
||||||
|
requester=requester,
|
||||||
|
assignee=assignee,
|
||||||
|
related_system="CRM",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = admin_client.get("/api/tickets?page=1&page_size=20")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["page"] == 1
|
||||||
|
assert payload["page_size"] == 20
|
||||||
|
assert payload["total"] == 1
|
||||||
|
assert payload["total_pages"] == 1
|
||||||
|
assert payload["items"][0]["requester"]["display_name"] == "请求人"
|
||||||
|
assert payload["items"][0]["assignee"]["display_name"] == "处理人"
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_tickets_filters_by_priority_related_system_and_view(admin_client, db):
|
||||||
|
user_model = get_user_model()
|
||||||
|
current_user = user_model.objects.get(username="admin")
|
||||||
|
Ticket.objects.create(
|
||||||
|
ticket_no="TK202607010101",
|
||||||
|
title="Mine urgent CRM",
|
||||||
|
description="A",
|
||||||
|
priority="urgent",
|
||||||
|
related_system="CRM",
|
||||||
|
assignee=current_user,
|
||||||
|
created_by=current_user,
|
||||||
|
)
|
||||||
|
Ticket.objects.create(
|
||||||
|
ticket_no="TK202607010102",
|
||||||
|
title="Other medium ERP",
|
||||||
|
description="B",
|
||||||
|
priority="medium",
|
||||||
|
related_system="ERP",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = admin_client.get(
|
||||||
|
"/api/tickets?priority=urgent&related_system=CRM&view=assigned_to_me"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["total"] == 1
|
||||||
|
assert payload["items"][0]["ticket_no"] == "TK202607010101"
|
||||||
|
|
||||||
|
|
||||||
|
def test_transition_ticket_requires_non_empty_remark(admin_client, db):
|
||||||
|
ticket = Ticket.objects.create(
|
||||||
|
ticket_no="TK202607010103",
|
||||||
|
title="A",
|
||||||
|
description="A",
|
||||||
|
status="pending",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = admin_client.post(
|
||||||
|
f"/api/tickets/{ticket.id}/status",
|
||||||
|
data={"status": "in_progress", "remark": ""},
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python -m pytest apps/tickets/tests/test_ticket_api.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fails because list response is currently a list, display users are missing, advanced filters are missing, and empty transition remark is allowed.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement schemas and serialization**
|
||||||
|
|
||||||
|
Add schemas to `backend/apps/tickets/schemas.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TicketUserOut(Schema):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class TicketListOut(Schema):
|
||||||
|
items: list[TicketOut]
|
||||||
|
page: int
|
||||||
|
page_size: int
|
||||||
|
total: int
|
||||||
|
total_pages: int
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend `TicketOut` with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
requester_id: int | None
|
||||||
|
requester: TicketUserOut | None = None
|
||||||
|
assignee: TicketUserOut | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement serializer helpers in `backend/apps/tickets/api.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def serialize_ticket_user(user):
|
||||||
|
if user is None:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"id": user.id,
|
||||||
|
"username": user.username,
|
||||||
|
"display_name": user.get_full_name() or user.username,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_ticket(ticket: Ticket) -> dict:
|
||||||
|
return {
|
||||||
|
"id": ticket.id,
|
||||||
|
"ticket_no": ticket.ticket_no,
|
||||||
|
"title": ticket.title,
|
||||||
|
"description": ticket.description,
|
||||||
|
"status": ticket.status,
|
||||||
|
"priority": ticket.priority,
|
||||||
|
"impact_scope": ticket.impact_scope,
|
||||||
|
"environment": ticket.environment,
|
||||||
|
"related_system": ticket.related_system,
|
||||||
|
"requester_id": ticket.requester_id,
|
||||||
|
"assignee_id": ticket.assignee_id,
|
||||||
|
"requester": serialize_ticket_user(ticket.requester),
|
||||||
|
"assignee": serialize_ticket_user(ticket.assignee),
|
||||||
|
"created_at": ticket.created_at,
|
||||||
|
"updated_at": ticket.updated_at,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement paginated list and filters**
|
||||||
|
|
||||||
|
Change `list_tickets` to return `TicketListOut` and accept:
|
||||||
|
|
||||||
|
```python
|
||||||
|
page: int = 1
|
||||||
|
page_size: int = 20
|
||||||
|
status: str | None = None
|
||||||
|
priority: str | None = None
|
||||||
|
q: str | None = None
|
||||||
|
related_system: str | None = None
|
||||||
|
assignee_id: int | None = None
|
||||||
|
view: str = "all"
|
||||||
|
```
|
||||||
|
|
||||||
|
Filter rules:
|
||||||
|
|
||||||
|
- `status`: exact status.
|
||||||
|
- `priority`: exact priority.
|
||||||
|
- `q`: `ticket_no`, `title`, or `description` contains keyword.
|
||||||
|
- `related_system`: contains text.
|
||||||
|
- `assignee_id`: exact assignee.
|
||||||
|
- `view=assigned_to_me`: `assignee=request.user`.
|
||||||
|
- `view=created_by_me`: `created_by=request.user`.
|
||||||
|
- `view=high_priority`: `priority in ["high", "urgent"]`.
|
||||||
|
|
||||||
|
Clamp:
|
||||||
|
|
||||||
|
```python
|
||||||
|
page = max(page, 1)
|
||||||
|
page_size = min(max(page_size, 1), 100)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Require transition remarks**
|
||||||
|
|
||||||
|
In `update_ticket_status`, reject blank remarks:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if not payload.remark.strip():
|
||||||
|
raise HttpError(400, "Transition remark is required")
|
||||||
|
```
|
||||||
|
|
||||||
|
Import `HttpError` from `ninja.errors`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Update existing tests for new list shape**
|
||||||
|
|
||||||
|
Change existing list assertions in `test_list_tickets_filters_by_status`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["total"] == 1
|
||||||
|
assert payload["items"][0]["ticket_no"] == "TK202606300001"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Verify GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python -m pytest apps/tickets/tests/test_ticket_api.py apps/tickets/tests/test_ticket_export.py apps/accounts/tests/test_permission_enforcement.py -v
|
||||||
|
python -m ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all selected tests pass and ruff has no errors.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/apps/tickets
|
||||||
|
git commit -m "feat: add ticket list visual support api"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 2: Frontend Ticket API And List Redesign
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/features/tickets/types.ts`
|
||||||
|
- Modify: `frontend/src/features/tickets/api.ts`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketListPage.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketMetricCards.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketFilterBar.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Refresh shadcn docs**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx shadcn@latest docs tabs table card badge select input pagination skeleton sonner
|
||||||
|
```
|
||||||
|
|
||||||
|
Use installed components first. Do not overwrite existing locally patched components.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update ticket frontend types**
|
||||||
|
|
||||||
|
In `frontend/src/features/tickets/types.ts`, add:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type TicketUser = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
display_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TicketListResponse = {
|
||||||
|
items: Ticket[]
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
total: number
|
||||||
|
total_pages: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend `Ticket`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
requester_id: number | null
|
||||||
|
requester: TicketUser | null
|
||||||
|
assignee: TicketUser | null
|
||||||
|
```
|
||||||
|
|
||||||
|
Add list query type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type TicketListParams = {
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
status?: string
|
||||||
|
priority?: string
|
||||||
|
q?: string
|
||||||
|
related_system?: string
|
||||||
|
assignee_id?: number
|
||||||
|
view?: 'all' | 'assigned_to_me' | 'created_by_me' | 'high_priority'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update ticket API**
|
||||||
|
|
||||||
|
In `frontend/src/features/tickets/api.ts`, update `listTickets` to return `TicketListResponse` and include all query fields:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function listTickets(params: TicketListParams = {}) {
|
||||||
|
const query = new URLSearchParams()
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
query.set(key, String(value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const suffix = query.toString() ? `?${query}` : ''
|
||||||
|
return apiRequest<TicketListResponse>(`/tickets${suffix}`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create TicketMetricCards**
|
||||||
|
|
||||||
|
Create `frontend/src/features/tickets/TicketMetricCards.tsx` with compact shadcn `Card` metrics for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
待处理
|
||||||
|
处理中
|
||||||
|
待确认
|
||||||
|
高优先级
|
||||||
|
```
|
||||||
|
|
||||||
|
Props:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type TicketMetricCardsProps = {
|
||||||
|
tickets: Ticket[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Create TicketFilterBar**
|
||||||
|
|
||||||
|
Create `frontend/src/features/tickets/TicketFilterBar.tsx` using `AppFilterBar`, `Input`, `Select`, and `Button`.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type TicketFilterBarProps = {
|
||||||
|
q: string
|
||||||
|
status: string
|
||||||
|
priority: string
|
||||||
|
relatedSystem: string
|
||||||
|
onQChange: (value: string) => void
|
||||||
|
onStatusChange: (value: string) => void
|
||||||
|
onPriorityChange: (value: string) => void
|
||||||
|
onRelatedSystemChange: (value: string) => void
|
||||||
|
onReset: () => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Redesign TicketListPage**
|
||||||
|
|
||||||
|
Update `TicketListPage`:
|
||||||
|
|
||||||
|
- Use shadcn `Tabs` for `全部工单`, `我的待办`, `我创建的`, `高优先级`.
|
||||||
|
- Use `TicketMetricCards`.
|
||||||
|
- Use `TicketFilterBar`.
|
||||||
|
- Use backend pagination fields.
|
||||||
|
- Use `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationPrevious`, `PaginationNext`.
|
||||||
|
- Table columns must be Chinese and include requester/assignee display when available.
|
||||||
|
- Empty/error/loading states must be Chinese.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Verify frontend GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run typecheck
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/features/tickets
|
||||||
|
git commit -m "feat: redesign ticket list experience"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 3: Frontend Ticket Detail Redesign
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketDetailPage.tsx`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketComments.tsx`
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketStatusTimeline.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx`
|
||||||
|
- Create: `frontend/src/features/tickets/TicketMetaPanel.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Refresh shadcn docs**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx shadcn@latest docs dialog textarea card badge separator tabs sonner
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create TicketTransitionDialog**
|
||||||
|
|
||||||
|
Create a dialog component with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type TicketTransitionDialogProps = {
|
||||||
|
open: boolean
|
||||||
|
status: TicketStatus | null
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onSubmit: (status: TicketStatus, remark: string) => Promise<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Visible text:
|
||||||
|
|
||||||
|
```text
|
||||||
|
处理说明
|
||||||
|
请说明本次流转的原因、处理动作或待确认事项。
|
||||||
|
取消
|
||||||
|
确认提交
|
||||||
|
```
|
||||||
|
|
||||||
|
The submit button is disabled until `remark.trim()` is non-empty.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create TicketMetaPanel**
|
||||||
|
|
||||||
|
Create `TicketMetaPanel` with shadcn `Card`, `Badge`, `Separator`, `AppField`, `AppStatusBadge`, and `AppPriorityBadge`.
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
```text
|
||||||
|
当前状态
|
||||||
|
优先级
|
||||||
|
处理人
|
||||||
|
请求人
|
||||||
|
关联系统
|
||||||
|
运行环境
|
||||||
|
影响范围
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update TicketStatusTimeline**
|
||||||
|
|
||||||
|
Make every event label Chinese:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const EVENT_LABELS: Record<string, string> = {
|
||||||
|
created: '创建工单',
|
||||||
|
updated: '更新工单',
|
||||||
|
status_change: '状态变更',
|
||||||
|
commented: '新增评论',
|
||||||
|
attachment_added: '新增附件',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update TicketComments**
|
||||||
|
|
||||||
|
Use Chinese labels and distinguish:
|
||||||
|
|
||||||
|
```text
|
||||||
|
内部备注
|
||||||
|
用户可见回复
|
||||||
|
```
|
||||||
|
|
||||||
|
Use shadcn `Tabs` or `Select` to choose visibility. Existing API can still receive:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{ comment_type: 'internal_note' | 'user_reply', visibility: 'internal' | 'public' }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Redesign TicketDetailPage**
|
||||||
|
|
||||||
|
Update page:
|
||||||
|
|
||||||
|
- Loading title: `正在加载工单`
|
||||||
|
- Error title: `无法加载工单`
|
||||||
|
- Header actions open `TicketTransitionDialog`.
|
||||||
|
- Main column contains `问题描述`, `处理记录`, `评论与协作`.
|
||||||
|
- Side column uses `TicketMetaPanel`, `SolutionLinkPanel`, and restrained AI placeholder.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Verify frontend GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run typecheck
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/features/tickets
|
||||||
|
git commit -m "feat: redesign ticket detail experience"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 4: Frontend Ticket Create Redesign And Final Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Refresh shadcn docs**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx shadcn@latest docs card input select textarea badge alert
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Redesign create page**
|
||||||
|
|
||||||
|
Update `TicketCreatePage` into grouped sections:
|
||||||
|
|
||||||
|
```text
|
||||||
|
基础信息
|
||||||
|
影响与优先级
|
||||||
|
环境与系统
|
||||||
|
问题描述
|
||||||
|
提交确认
|
||||||
|
```
|
||||||
|
|
||||||
|
Visible copy must be Chinese. Use `Card`, `Input`, `Select`, `Textarea`, `Badge`, and `Button`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Use Chinese error feedback**
|
||||||
|
|
||||||
|
On create failure, show:
|
||||||
|
|
||||||
|
```text
|
||||||
|
创建工单失败,请检查必填信息或稍后重试。
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run full verification**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python -m pytest apps/tickets/tests apps/accounts/tests/test_permission_enforcement.py -v
|
||||||
|
python -m ruff check .
|
||||||
|
|
||||||
|
cd ../frontend
|
||||||
|
npm run typecheck
|
||||||
|
npm run build
|
||||||
|
npx shadcn@latest info
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all commands pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Browser visual smoke test**
|
||||||
|
|
||||||
|
With local backend/frontend running:
|
||||||
|
|
||||||
|
- Login as seeded admin.
|
||||||
|
- Open `#/tickets` and verify Chinese tabs, metrics, filters, table, and pagination.
|
||||||
|
- Open a ticket detail page and verify transition dialog requires handling note.
|
||||||
|
- Open `#/tickets/new` and verify grouped create form has no obvious overlap.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/features/tickets
|
||||||
|
git commit -m "feat: redesign ticket create experience"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Final Review Checklist
|
||||||
|
|
||||||
|
- [ ] Ticket list uses shadcn tabs, cards, table, filters, and pagination.
|
||||||
|
- [ ] Ticket detail uses a Chinese transition dialog and no English loading/error copy.
|
||||||
|
- [ ] Ticket comments distinguish internal notes and user-visible replies.
|
||||||
|
- [ ] Ticket create page uses grouped Chinese form sections.
|
||||||
|
- [ ] Backend ticket list supports pagination and visual filters.
|
||||||
|
- [ ] Ticket responses include requester and assignee display fields.
|
||||||
|
- [ ] Empty transition remarks are rejected.
|
||||||
|
- [ ] Backend ticket tests pass.
|
||||||
|
- [ ] Backend ruff passes.
|
||||||
|
- [ ] Frontend typecheck passes.
|
||||||
|
- [ ] Frontend build passes.
|
||||||
|
- [ ] shadcn info still reports Vite/Tailwind v3 and installed components.
|
||||||
Reference in New Issue
Block a user