diff --git a/docs/superpowers/plans/2026-04-01-dashboard-redesign-plan.md b/docs/superpowers/plans/2026-04-01-dashboard-redesign-plan.md new file mode 100644 index 0000000..ded7b06 --- /dev/null +++ b/docs/superpowers/plans/2026-04-01-dashboard-redesign-plan.md @@ -0,0 +1,1085 @@ +# Dashboard 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:** Implement role-based buyer/supplier dashboard with unified API, real business data, and Vue 3 frontend components. + +**Architecture:** Role-based data filtering via unified `/api/dashboard/` endpoint. Backend computes KPIs from Inquiry/QuotationMaster/Supplier tables. Frontend uses Pinia store with separate buyer/supplier state branches. + +**Tech Stack:** Django REST Framework, Vue 3 + TypeScript + Pinia + ECharts + +--- + +## Backend Implementation + +### File Structure + +**Create:** +- `backend/apps/pisadmin/dashboard/__init__.py` +- `backend/apps/pisadmin/dashboard/apps.py` +- `backend/apps/pisadmin/dashboard/urls.py` +- `backend/apps/pisadmin/dashboard/serializers.py` +- `backend/apps/pisadmin/dashboard/views.py` + +**Modify:** +- `backend/application/urls.py` - add `path("api/pisadmin/dashboard/", include("apps.pisadmin.dashboard.urls"))` + +--- + +### Task 1: Create Dashboard App Skeleton + +- [ ] **Step 1: Create `backend/apps/pisadmin/dashboard/__init__.py`** +```python +``` + +- [ ] **Step 2: Create `backend/apps/pisadmin/dashboard/apps.py`** +```python +from django.apps import AppConfig + + +class DashboardConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.pisadmin.dashboard' +``` + +- [ ] **Step 3: Create `backend/apps/pisadmin/dashboard/urls.py`** +```python +from django.urls import path +from .views import DashboardView + +urlpatterns = [ + path('', DashboardView.as_view(), name='dashboard'), +] +``` + +- [ ] **Step 4: Register app in `backend/application/settings.py`** +Add `'apps.pisadmin.dashboard'` to `INSTALLED_APPS` list. + +- [ ] **Step 5: Add URL to `backend/application/urls.py`** +Add `path("api/pisadmin/dashboard/", include("apps.pisadmin.dashboard.urls"))` to urlpatterns. + +- [ ] **Step 6: Commit** +```bash +git add backend/apps/pisadmin/dashboard/ backend/application/urls.py backend/application/settings.py +git commit -m "feat(dashboard): create dashboard app skeleton with URL routing" +``` + +--- + +### Task 2: Implement Dashboard Serializers + +- [ ] **Step 1: Write `backend/apps/pisadmin/dashboard/serializers.py`** + +```python +from rest_framework import serializers + + +class BuyerKPISerializer(serializers.Serializer): + total_inquiries = serializers.IntegerField() + pending_inquiries = serializers.IntegerField() + completed_quotes = serializers.IntegerField() + total_suppliers = serializers.IntegerField() + + +class BuyerTaskSerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField() + inquiry_no = serializers.CharField() + status = serializers.CharField() + created_at = serializers.DateTimeField() + + +class SupplierKPISerializer(serializers.Serializer): + total_quotes = serializers.IntegerField() + pending_quotes = serializers.IntegerField() + won_quotes = serializers.IntegerField() + conversion_rate = serializers.FloatField() + + +class SupplierQuoteSerializer(serializers.Serializer): + id = serializers.IntegerField() + inquiry_no = serializers.CharField() + item_name = serializers.CharField() + quantity = serializers.IntegerField() + unit = serializers.CharField() + deadline = serializers.DateTimeField() + + +class MessageSerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField() + content = serializers.CharField() + is_read = serializers.BooleanField() + created_at = serializers.DateTimeField() + + +class TrendSerializer(serializers.Serializer): + month = serializers.CharField() + count = serializers.IntegerField(required=False) + quotes = serializers.IntegerField(required=False) + won = serializers.IntegerField(required=False) + + +class BuyerDashboardSerializer(serializers.Serializer): + kpi = BuyerKPISerializer() + tasks = BuyerTaskSerializer(many=True) + messages = MessageSerializer(many=True) + trend = TrendSerializer(many=True) + + +class SupplierDashboardSerializer(serializers.Serializer): + kpi = SupplierKPISerializer() + pending_quotes = SupplierQuoteSerializer(many=True) + messages = MessageSerializer(many=True) + trend = TrendSerializer(many=True) + + +class DashboardResponseSerializer(serializers.Serializer): + buyer = BuyerDashboardSerializer(allow_null=True) + supplier = SupplierDashboardSerializer(allow_null=True) +``` + +- [ ] **Step 2: Commit** +```bash +git add backend/apps/pisadmin/dashboard/serializers.py +git commit -m "feat(dashboard): add dashboard serializers" +``` + +--- + +### Task 3: Implement Dashboard View + +- [ ] **Step 1: Write `backend/apps/pisadmin/dashboard/views.py`** + +```python +from datetime import timedelta + +from django.utils import timezone +from django.db.models import Count +from django.db.models.functions import TruncMonth +from rest_framework.views import APIView +from rest_framework.response import Response + +from apps.pisadmin.miscprocurement.models import Inquiry +from apps.pissupplier.models import QuotationMaster +from apps.pisadmin.basicinfo.models import Supplier + + +def get_buyer_kpi(user): + """采购方 KPI 计算""" + total_inquiries = Inquiry.objects.filter(create_user=user.username).count() + pending_inquiries = Inquiry.objects.filter(create_user=user.username, status__in=[1, 2, 3]).count() + completed_quotes = QuotationMaster.objects.filter( + inquiry_no__in=Inquiry.objects.filter(create_user=user.username).values('inquiry_no') + ).count() + total_suppliers = Supplier.objects.count() + return { + 'total_inquiries': total_inquiries, + 'pending_inquiries': pending_inquiries, + 'completed_quotes': completed_quotes, + 'total_suppliers': total_suppliers, + } + + +def get_buyer_tasks(user): + """采购方待办任务""" + six_months_ago = timezone.now() - timedelta(days=180) + inquiries = Inquiry.objects.filter( + create_user=user.username, + status__in=[1, 2, 3] + ).order_by('-create_time')[:10] + return [ + { + 'id': i.id, + 'title': i.title, + 'inquiry_no': i.inquiry_no, + 'status': dict(Inquiry.STATUS_CHOICES).get(i.status, str(i.status)), + 'created_at': i.create_time, + } + for i in inquiries + ] + + +def get_supplier_kpi(user): + """供应商 KPI 计算""" + total_quotes = QuotationMaster.objects.filter(supplier_code=user.username).count() + pending_quotes = QuotationMaster.objects.filter(supplier_code=user.username, status__in=[1, 2]).count() + won_quotes = QuotationMaster.objects.filter(supplier_code=user.username, is_awarded=1).count() + conversion_rate = (won_quotes / total_quotes * 100) if total_quotes > 0 else 0.0 + return { + 'total_quotes': total_quotes, + 'pending_quotes': pending_quotes, + 'won_quotes': won_quotes, + 'conversion_rate': round(conversion_rate, 2), + } + + +def get_supplier_pending_quotes(user): + """供应商待报价清单""" + quotes = QuotationMaster.objects.filter( + supplier_code=user.username, + status__in=[1, 2] + ).order_by('-creattime')[:10] + result = [] + for q in quotes: + inquiry = Inquiry.objects.filter(inquiry_no=q.inquiry_no).first() + result.append({ + 'id': q.autoid, + 'inquiry_no': q.inquiry_no, + 'item_name': inquiry.title if inquiry else '', + 'quantity': 0, + 'unit': '', + 'deadline': q.quote_deadline, + }) + return result + + +def get_trend_data(user, is_buyer=True): + """近6个月趋势数据""" + six_months_ago = timezone.now() - timedelta(days=180) + if is_buyer: + data = ( + Inquiry.objects.filter( + create_user=user.username, + create_time__gte=six_months_ago + ) + .annotate(month=TruncMonth('create_time')) + .values('month') + .annotate(count=Count('id')) + .order_by('month') + ) + else: + data = ( + QuotationMaster.objects.filter( + supplier_code=user.username, + creattime__gte=six_months_ago + ) + .annotate(month=TruncMonth('creattime')) + .values('month') + .annotate(quotes=Count('id')) + .order_by('month') + ) + # Add won counts + result = [] + for d in data: + won = QuotationMaster.objects.filter( + supplier_code=user.username, + creattime__month=d['month'].month, + creattime__year=d['month'].year, + is_awarded=1 + ).count() + result.append({ + 'month': d['month'].strftime('%Y-%m'), + 'quotes': d['quotes'], + 'won': won, + }) + return result + return [{'month': d['month'].strftime('%Y-%m'), 'count': d['count']} for d in data] + + +def get_messages(user): + """消息通知 - 占位实现,后续接入通知系统""" + return [] + + +class DashboardView(APIView): + """看板数据视图""" + + def get(self, request): + user = request.user + if not user or not user.is_authenticated: + return Response({'buyer': None, 'supplier': None}) + + # 判断用户角色 + # 这里需要根据实际角色系统判断,暂用用户名特征区分 + # 供应商用户:有 supplier_code 在 QuotationMaster 中 + has_supplier_role = QuotationMaster.objects.filter(supplier_code=user.username).exists() + # 采购方用户:有 create_user 在 Inquiry 中 + has_buyer_role = Inquiry.objects.filter(create_user=user.username).exists() + + response_data = {'buyer': None, 'supplier': None} + + if has_buyer_role: + response_data['buyer'] = { + 'kpi': get_buyer_kpi(user), + 'tasks': get_buyer_tasks(user), + 'messages': get_messages(user), + 'trend': get_trend_data(user, is_buyer=True), + } + + if has_supplier_role: + response_data['supplier'] = { + 'kpi': get_supplier_kpi(user), + 'pending_quotes': get_supplier_pending_quotes(user), + 'messages': get_messages(user), + 'trend': get_trend_data(user, is_buyer=False), + } + + return Response(response_data) +``` + +- [ ] **Step 2: Commit** +```bash +git add backend/apps/pisadmin/dashboard/views.py +git commit -m "feat(dashboard): implement dashboard view with role-based data filtering" +``` + +--- + +## Frontend Implementation + +### File Structure + +**Create:** +- `web/src/api/pisadmin/dashboard.ts` +- `web/src/stores/modules/dashboard.ts` +- `web/src/views/pisadmin/dashboard/index.vue` +- `web/src/views/pisadmin/dashboard/BuyerDashboard.vue` +- `web/src/views/pisadmin/dashboard/SupplierDashboard.vue` +- `web/src/components/pisadmin/dashboard/KpiCard.vue` +- `web/src/components/pisadmin/dashboard/TrendChart.vue` +- `web/src/components/pisadmin/dashboard/MessageList.vue` +- `web/src/components/pisadmin/dashboard/BuyerTaskList.vue` +- `web/src/components/pisadmin/dashboard/SupplierQuoteList.vue` + +**Modify:** +- `web/src/router/route.ts` - add dashboard routes + +--- + +### Task 4: Create Frontend API and Store + +- [ ] **Step 1: Create `web/src/api/pisadmin/dashboard.ts`** + +```typescript +import request from '/@/utils/request'; + +export function getDashboard() { + return request({ + url: '/api/pisadmin/dashboard/', + method: 'get', + }); +} +``` + +- [ ] **Step 2: Create `web/src/stores/modules/dashboard.ts`** + +```typescript +import { defineStore } from 'pinia'; +import { getDashboard } from '/@/api/pisadmin/dashboard'; + +interface BuyerDashboard { + kpi: { + total_inquiries: number; + pending_inquiries: number; + completed_quotes: number; + total_suppliers: number; + } | null; + tasks: Array<{ + id: number; + title: string; + inquiry_no: string; + status: string; + created_at: string; + }>; + messages: Array<{ + id: number; + title: string; + content: string; + is_read: boolean; + created_at: string; + }>; + trend: Array<{ month: string; count: number }>; +} + +interface SupplierDashboard { + kpi: { + total_quotes: number; + pending_quotes: number; + won_quotes: number; + conversion_rate: number; + } | null; + pending_quotes: Array<{ + id: number; + inquiry_no: string; + item_name: string; + quantity: number; + unit: string; + deadline: string; + }>; + messages: Array<{ + id: number; + title: string; + content: string; + is_read: boolean; + created_at: string; + }>; + trend: Array<{ month: string; quotes: number; won: number }>; +} + +export const useDashboardStore = defineStore('dashboard', { + state: () => ({ + buyer: null as BuyerDashboard | null, + supplier: null as SupplierDashboard | null, + loading: false, + }), + actions: { + async fetchDashboard() { + this.loading = true; + try { + const res: any = await getDashboard(); + this.buyer = res.data?.buyer || null; + this.supplier = res.data?.supplier || null; + } finally { + this.loading = false; + } + }, + }, +}); +``` + +- [ ] **Step 3: Commit** +```bash +git add web/src/api/pisadmin/dashboard.ts web/src/stores/modules/dashboard.ts +git commit -m "feat(dashboard): add dashboard API and Pinia store" +``` + +--- + +### Task 5: Create Dashboard Components + +- [ ] **Step 1: Create `web/src/components/pisadmin/dashboard/KpiCard.vue`** + +```vue + + + + + + + + {{ value }} + {{ label }} + + + + + + + + +``` + +- [ ] **Step 2: Create `web/src/components/pisadmin/dashboard/TrendChart.vue`** + +```vue + + + + + + + +``` + +- [ ] **Step 3: Create `web/src/components/pisadmin/dashboard/MessageList.vue`** + +```vue + + + + + + {{ msg.title }} + {{ formatTime(msg.created_at) }} + + + + + + + + + + +``` + +- [ ] **Step 4: Create `web/src/components/pisadmin/dashboard/BuyerTaskList.vue`** + +```vue + + + + + + + {{ row.status }} + + + + + {{ formatDate(row.created_at) }} + + + + + + +``` + +- [ ] **Step 5: Create `web/src/components/pisadmin/dashboard/SupplierQuoteList.vue`** + +```vue + + + + + + + + + {{ formatDate(row.deadline) }} + + + + + 去报价 + + + + + + +``` + +- [ ] **Step 6: Commit** +```bash +git add web/src/components/pisadmin/dashboard/ web/src/api/pisadmin/dashboard.ts web/src/stores/modules/dashboard.ts +git commit -m "feat(dashboard): add dashboard Vue components" +``` + +--- + +### Task 6: Create Dashboard Pages + +- [ ] **Step 1: Create `web/src/views/pisadmin/dashboard/index.vue`** + +```vue + + + + + + + 采购方看板 + 供应商看板 + + + + + + + + + + + +``` + +- [ ] **Step 2: Create `web/src/views/pisadmin/dashboard/BuyerDashboard.vue`** + +```vue + + + + 采购方看板 + {{ currentDate }} + + + + + + + + + + + + + + + + + + + + + + + + 待办任务 + + + + + + + + 业务趋势 + + + + + + + + + + 消息通知 + + + + + + + + + +``` + +- [ ] **Step 3: Create `web/src/views/pisadmin/dashboard/SupplierDashboard.vue`** + +```vue + + + + 供应商看板 + {{ currentDate }} + + + + + + + + + + + + + + + + + + + + + + + + 待报价清单 + + + + + + + + 报价与中标趋势 + + + + + + + + + + 消息通知 + + + + + + + + + +``` + +- [ ] **Step 4: Commit** +```bash +git add web/src/views/pisadmin/dashboard/ +git commit -m "feat(dashboard): add buyer/supplier dashboard pages" +``` + +--- + +### Task 7: Add Dashboard Routes + +- [ ] **Step 1: Modify `web/src/router/route.ts`** + +Add to `dynamicRoutes` array: +```typescript +{ + path: '/dashboard', + name: 'Dashboard', + component: () => import('/@/views/pisadmin/dashboard/index.vue'), + meta: { + title: 'message.router.dashboard', + isLink: '', + isHide: false, + isKeepAlive: true, + isAffix: false, + isIframe: false, + icon: 'iconfont icon-dashboard', + }, +}, +``` + +- [ ] **Step 2: Commit** +```bash +git add web/src/router/route.ts +git commit -m "feat(dashboard): add dashboard route" +``` + +--- + +## Verification Checklist + +- [ ] Backend: `GET /api/pisadmin/dashboard/` returns correct data structure +- [ ] Backend: Role-based filtering works (buyer only, supplier only, both) +- [ ] Frontend: Dashboard page loads without errors +- [ ] Frontend: KPI cards display correct data +- [ ] Frontend: Trend charts render correctly +- [ ] Frontend: Task/Quote lists display correctly +- [ ] Frontend: Role switching works for users with both roles