# Design Document: Admin Frontend Business Modules ## Overview 本设计文档描述了学业邑规划 MiAssessment 管理后台前端业务模块的技术架构和实现方案。系统基于 Vue 3.5 + TypeScript 5.6 + Element Plus 2.9 技术栈,在现有的前端模板基础上扩展8个核心业务模块页面。 ### 设计目标 1. **组件化设计**: 页面组件独立封装,复用现有基础组件 2. **类型安全**: 使用 TypeScript 定义所有接口和数据类型 3. **统一风格**: 遵循现有代码风格和 Element Plus 设计规范 4. **权限集成**: 与现有权限系统无缝集成 5. **可测试性**: 支持单元测试和属性测试 ## Architecture ### 系统架构图 ```mermaid graph TB subgraph "Vue Application" A[Router] --> B[Layout] B --> C[Business Pages] subgraph "Business Pages" C1[Dashboard] C2[Config] C3[Content - Banner/Promotion] C4[Assessment - Type/Question/Category/Conclusion] C5[User] C6[Order] C7[Planner - Info/Booking] C8[Distribution - InviteCode/Commission/Withdrawal] end end subgraph "API Layer" D[API Services] D1[dashboard.ts] D2[config.ts] D3[content.ts] D4[assessment.ts] D5[user.ts] D6[order.ts] D7[planner.ts] D8[distribution.ts] end subgraph "Shared Components" E1[ImageUpload] E2[DictSelect] E3[DictRadio] E4[DictCheckbox] end C --> D C --> E1 C --> E2 C --> E3 ``` ### 项目结构 ``` server/MiAssessment/src/MiAssessment.Admin/admin-web/src/ ├── api/ │ └── business/ # 业务模块 API │ ├── dashboard.ts # 仪表盘 API │ ├── config.ts # 系统配置 API │ ├── content.ts # 内容管理 API (轮播图、宣传图) │ ├── assessment.ts # 测评管理 API │ ├── user.ts # 用户管理 API │ ├── order.ts # 订单管理 API │ ├── planner.ts # 规划师管理 API │ └── distribution.ts # 分销管理 API ├── views/ │ ├── dashboard/ │ │ └── index.vue # 仪表盘页面 (已存在,需扩展) │ └── business/ # 业务模块页面 │ ├── config/ │ │ └── index.vue # 系统配置页面 │ ├── content/ │ │ ├── banner/ │ │ │ └── index.vue # 轮播图管理页面 │ │ └── promotion/ │ │ └── index.vue # 宣传图管理页面 │ ├── assessment/ │ │ ├── type/ │ │ │ └── index.vue # 测评类型管理页面 │ │ ├── question/ │ │ │ └── index.vue # 题库管理页面 │ │ ├── category/ │ │ │ └── index.vue # 报告分类管理页面 │ │ └── conclusion/ │ │ └── index.vue # 报告结论管理页面 │ ├── user/ │ │ └── index.vue # 用户管理页面 │ ├── order/ │ │ └── index.vue # 订单管理页面 │ ├── planner/ │ │ ├── index.vue # 规划师管理页面 │ │ └── booking/ │ │ └── index.vue # 预约记录管理页面 │ └── distribution/ │ ├── invite-code/ │ │ └── index.vue # 邀请码管理页面 │ ├── commission/ │ │ └── index.vue # 佣金记录管理页面 │ └── withdrawal/ │ └── index.vue # 提现审核管理页面 └── router/ └── index.ts # 路由配置 (需扩展) ``` ## Components and Interfaces ### 1. API 服务层设计 #### 通用类型定义 ```typescript // types/common.ts export interface PagedRequest { pageIndex: number pageSize: number } export interface PagedResult { items: T[] total: number pageIndex: number pageSize: number } export interface ApiResponse { code: number message: string data: T } export interface UpdateStatusRequest { id: number status: number } export interface SortItem { id: number sort: number } export interface UpdateSortRequest { items: SortItem[] } ``` #### Dashboard API (api/business/dashboard.ts) ```typescript import request from '@/utils/request' export interface TodayStatistics { newUsers: number newOrders: number revenue: number } export interface TotalStatistics { totalUsers: number totalOrders: number totalRevenue: number } export interface DashboardOverview { today: TodayStatistics total: TotalStatistics } export interface TrendItem { date: string value: number } export interface TrendsData { userTrend: TrendItem[] orderTrend: TrendItem[] revenueTrend: TrendItem[] } export interface TrendsQuery { startDate: string endDate: string } export interface PendingItems { pendingWithdrawals: number pendingBookings: number } // 获取概览数据 export function getOverview() { return request.get('/api/admin/dashboard/getOverview') } // 获取趋势数据 export function getTrends(params: TrendsQuery) { return request.get('/api/admin/dashboard/getTrends', { params }) } // 获取待办事项 export function getPendingItems() { return request.get('/api/admin/dashboard/getPendingItems') } ``` #### Config API (api/business/config.ts) ```typescript import request from '@/utils/request' export interface ConfigItem { id: number configKey: string configValue: string configType: string description: string } export interface ConfigGroup { configType: string typeName: string items: ConfigItem[] } export interface UpdateConfigRequest { configKey: string configValue: string } // 获取配置列表 export function getConfigList() { return request.get('/api/admin/config/getList') } // 更新配置 export function updateConfig(data: UpdateConfigRequest) { return request.post('/api/admin/config/update', data) } // 根据Key获取配置 export function getConfigByKey(configKey: string) { return request.get('/api/admin/config/getByKey', { params: { configKey } }) } ``` #### Content API (api/business/content.ts) ```typescript import request from '@/utils/request' import type { PagedRequest, PagedResult, UpdateStatusRequest, UpdateSortRequest } from '@/types/common' // ==================== Banner 类型定义 ==================== export interface BannerItem { id: number title: string imageUrl: string linkType: number linkTypeName: string linkUrl: string appId: string sort: number status: number statusName: string createTime: string } export interface BannerQuery extends PagedRequest { title?: string status?: number } export interface CreateBannerRequest { title?: string imageUrl: string linkType: number linkUrl?: string appId?: string sort: number status: number } export interface UpdateBannerRequest extends CreateBannerRequest { id: number } // ==================== Promotion 类型定义 ==================== export interface PromotionItem { id: number title: string imageUrl: string position: number positionName: string sort: number status: number statusName: string createTime: string } export interface PromotionQuery extends PagedRequest { title?: string position?: number status?: number } export interface CreatePromotionRequest { title?: string imageUrl: string position: number sort: number status: number } export interface UpdatePromotionRequest extends CreatePromotionRequest { id: number } // ==================== Banner API ==================== export function getBannerList(params: BannerQuery) { return request.get>('/api/admin/content/banner/getList', { params }) } export function createBanner(data: CreateBannerRequest) { return request.post('/api/admin/content/banner/create', data) } export function updateBanner(data: UpdateBannerRequest) { return request.post('/api/admin/content/banner/update', data) } export function deleteBanner(id: number) { return request.post('/api/admin/content/banner/delete', { id }) } export function updateBannerStatus(data: UpdateStatusRequest) { return request.post('/api/admin/content/banner/updateStatus', data) } export function updateBannerSort(data: UpdateSortRequest) { return request.post('/api/admin/content/banner/updateSort', data) } // ==================== Promotion API ==================== export function getPromotionList(params: PromotionQuery) { return request.get>('/api/admin/content/promotion/getList', { params }) } export function createPromotion(data: CreatePromotionRequest) { return request.post('/api/admin/content/promotion/create', data) } export function updatePromotion(data: UpdatePromotionRequest) { return request.post('/api/admin/content/promotion/update', data) } export function deletePromotion(id: number) { return request.post('/api/admin/content/promotion/delete', { id }) } export function updatePromotionStatus(data: UpdateStatusRequest) { return request.post('/api/admin/content/promotion/updateStatus', data) } ``` #### Assessment API (api/business/assessment.ts) ```typescript import request from '@/utils/request' import type { PagedRequest, PagedResult, UpdateStatusRequest } from '@/types/common' // ==================== Assessment Type 类型定义 ==================== export interface AssessmentTypeItem { id: number name: string code: string imageUrl: string introContent: string price: number questionCount: number sort: number status: number statusName: string createTime: string } export interface AssessmentTypeQuery extends PagedRequest { name?: string code?: string status?: number } export interface CreateAssessmentTypeRequest { name: string code: string imageUrl?: string introContent?: string price: number sort: number status: number } export interface UpdateAssessmentTypeRequest extends CreateAssessmentTypeRequest { id: number } // ==================== Question 类型定义 ==================== export interface QuestionItem { id: number assessmentTypeId: number assessmentTypeName: string questionNo: number content: string sort: number status: number categoryIds: number[] } export interface QuestionQuery extends PagedRequest { assessmentTypeId?: number questionNo?: number status?: number } export interface CreateQuestionRequest { assessmentTypeId: number questionNo: number content: string sort: number status: number } export interface UpdateQuestionRequest extends CreateQuestionRequest { id: number } export interface BatchImportResult { successCount: number failCount: number errors: { row: number; message: string }[] } // ==================== Category 类型定义 ==================== export interface CategoryTreeNode { id: number parentId: number name: string code: string categoryType: number categoryTypeName: string scoreRule: number scoreRuleName: string sort: number children: CategoryTreeNode[] } export interface CreateCategoryRequest { assessmentTypeId: number parentId?: number name: string code: string categoryType: number scoreRule: number sort: number } export interface UpdateCategoryRequest extends CreateCategoryRequest { id: number } // ==================== Mapping 类型定义 ==================== export interface BatchUpdateMappingsRequest { questionId: number categoryIds: number[] } // ==================== Conclusion 类型定义 ==================== export interface ConclusionItem { id: number categoryId: number categoryName: string conclusionType: number conclusionTypeName: string title: string content: string } export interface CreateConclusionRequest { categoryId: number conclusionType: number title: string content: string } export interface UpdateConclusionRequest extends CreateConclusionRequest { id: number } // ==================== Assessment Type API ==================== export function getAssessmentTypeList(params: AssessmentTypeQuery) { return request.get>('/api/admin/assessment/type/getList', { params }) } export function createAssessmentType(data: CreateAssessmentTypeRequest) { return request.post('/api/admin/assessment/type/create', data) } export function updateAssessmentType(data: UpdateAssessmentTypeRequest) { return request.post('/api/admin/assessment/type/update', data) } export function deleteAssessmentType(id: number) { return request.post('/api/admin/assessment/type/delete', { id }) } export function updateAssessmentTypeStatus(data: UpdateStatusRequest) { return request.post('/api/admin/assessment/type/updateStatus', data) } // ==================== Question API ==================== export function getQuestionList(params: QuestionQuery) { return request.get>('/api/admin/assessment/question/getList', { params }) } export function createQuestion(data: CreateQuestionRequest) { return request.post('/api/admin/assessment/question/create', data) } export function updateQuestion(data: UpdateQuestionRequest) { return request.post('/api/admin/assessment/question/update', data) } export function deleteQuestion(id: number) { return request.post('/api/admin/assessment/question/delete', { id }) } export function batchImportQuestions(data: FormData) { return request.post('/api/admin/assessment/question/batchImport', data) } // ==================== Category API ==================== export function getCategoryTree(assessmentTypeId: number) { return request.get('/api/admin/assessment/category/getTree', { params: { assessmentTypeId } }) } export function createCategory(data: CreateCategoryRequest) { return request.post('/api/admin/assessment/category/create', data) } export function updateCategory(data: UpdateCategoryRequest) { return request.post('/api/admin/assessment/category/update', data) } export function deleteCategory(id: number) { return request.post('/api/admin/assessment/category/delete', { id }) } // ==================== Mapping API ==================== export function getMappingsByQuestion(questionId: number) { return request.get('/api/admin/assessment/mapping/getByQuestion', { params: { questionId } }) } export function batchUpdateMappings(data: BatchUpdateMappingsRequest) { return request.post('/api/admin/assessment/mapping/batchUpdate', data) } // ==================== Conclusion API ==================== export function getConclusionList(categoryId: number) { return request.get('/api/admin/assessment/conclusion/getList', { params: { categoryId } }) } export function createConclusion(data: CreateConclusionRequest) { return request.post('/api/admin/assessment/conclusion/create', data) } export function updateConclusion(data: UpdateConclusionRequest) { return request.post('/api/admin/assessment/conclusion/update', data) } export function deleteConclusion(id: number) { return request.post('/api/admin/assessment/conclusion/delete', { id }) } ``` #### User API (api/business/user.ts) ```typescript import request from '@/utils/request' import type { PagedRequest, PagedResult, UpdateStatusRequest } from '@/types/common' export interface UserItem { id: number uid: string phone: string nickname: string avatar: string userLevel: number userLevelName: string balance: number totalIncome: number status: number statusName: string createTime: string lastLoginTime: string | null } export interface UserDetail extends UserItem { parentUserId: number | null parentUserNickname: string | null parentUserUid: string | null withdrawnAmount: number orderCount: number assessmentCount: number inviteCount: number } export interface UserQuery extends PagedRequest { uid?: string phone?: string nickname?: string userLevel?: number status?: number createTimeStart?: string createTimeEnd?: string } export interface UpdateUserLevelRequest { id: number userLevel: number } // 获取用户列表 export function getUserList(params: UserQuery) { return request.get>('/api/admin/user/getList', { params }) } // 获取用户详情 export function getUserDetail(id: number) { return request.get('/api/admin/user/getDetail', { params: { id } }) } // 更新用户状态 export function updateUserStatus(data: UpdateStatusRequest) { return request.post('/api/admin/user/updateStatus', data) } // 更新用户等级 export function updateUserLevel(data: UpdateUserLevelRequest) { return request.post('/api/admin/user/updateLevel', data) } // 导出用户列表 export function exportUsers(params: UserQuery) { return request.get('/api/admin/user/export', { params, responseType: 'blob' }) } ``` #### Order API (api/business/order.ts) ```typescript import request from '@/utils/request' import type { PagedRequest, PagedResult } from '@/types/common' export interface OrderItem { id: number orderNo: string userId: number userNickname: string userPhone: string orderType: number orderTypeName: string productName: string amount: number payAmount: number payType: number | null payTypeName: string | null status: number statusName: string payTime: string | null createTime: string } export interface OrderDetail extends OrderItem { productId: number inviteCodeId: number | null inviteCode: string | null transactionId: string | null refundTime: string | null refundAmount: number | null refundReason: string | null remark: string | null relatedRecord: unknown } export interface OrderQuery extends PagedRequest { orderNo?: string userId?: number orderType?: number status?: number payType?: number createTimeStart?: string createTimeEnd?: string } export interface RefundRequest { orderId: number refundAmount: number refundReason: string } // 获取订单列表 export function getOrderList(params: OrderQuery) { return request.get>('/api/admin/order/getList', { params }) } // 获取订单详情 export function getOrderDetail(id: number) { return request.get('/api/admin/order/getDetail', { params: { id } }) } // 退款 export function refundOrder(data: RefundRequest) { return request.post('/api/admin/order/refund', data) } // 导出订单列表 export function exportOrders(params: OrderQuery) { return request.get('/api/admin/order/export', { params, responseType: 'blob' }) } ``` #### Planner API (api/business/planner.ts) ```typescript import request from '@/utils/request' import type { PagedRequest, PagedResult, UpdateStatusRequest, UpdateSortRequest } from '@/types/common' // ==================== Planner 类型定义 ==================== export interface PlannerItem { id: number name: string avatar: string title: string intro: string price: number sort: number status: number statusName: string createTime: string } export interface PlannerQuery extends PagedRequest { name?: string status?: number } export interface CreatePlannerRequest { name: string avatar: string title?: string intro?: string price: number sort: number status: number } export interface UpdatePlannerRequest extends CreatePlannerRequest { id: number } // ==================== Booking 类型定义 ==================== export interface BookingItem { id: number userId: number userNickname: string userPhone: string plannerId: number plannerName: string bookingDate: string studentName: string studentGrade: number studentGradeName: string status: number statusName: string createTime: string } export interface BookingDetail extends BookingItem { plannerAvatar: string plannerTitle: string studentSchool: string studentScores: string remark: string | null orderId: number orderNo: string } export interface BookingQuery extends PagedRequest { plannerId?: number userId?: number bookingDateStart?: string bookingDateEnd?: string status?: number } export interface UpdateBookingStatusRequest { id: number status: number } // ==================== Planner API ==================== export function getPlannerList(params: PlannerQuery) { return request.get>('/api/admin/planner/getList', { params }) } export function createPlanner(data: CreatePlannerRequest) { return request.post('/api/admin/planner/create', data) } export function updatePlanner(data: UpdatePlannerRequest) { return request.post('/api/admin/planner/update', data) } export function deletePlanner(id: number) { return request.post('/api/admin/planner/delete', { id }) } export function updatePlannerStatus(data: UpdateStatusRequest) { return request.post('/api/admin/planner/updateStatus', data) } export function updatePlannerSort(data: UpdateSortRequest) { return request.post('/api/admin/planner/updateSort', data) } // ==================== Booking API ==================== export function getBookingList(params: BookingQuery) { return request.get>('/api/admin/planner/booking/getList', { params }) } export function getBookingDetail(id: number) { return request.get('/api/admin/planner/booking/getDetail', { params: { id } }) } export function updateBookingStatus(data: UpdateBookingStatusRequest) { return request.post('/api/admin/planner/booking/updateStatus', data) } export function exportBookings(params: BookingQuery) { return request.get('/api/admin/planner/booking/export', { params, responseType: 'blob' }) } ``` #### Distribution API (api/business/distribution.ts) ```typescript import request from '@/utils/request' import type { PagedRequest, PagedResult } from '@/types/common' // ==================== InviteCode 类型定义 ==================== export interface InviteCodeItem { id: number code: string batchNo: string assignUserId: number | null assignUserNickname: string | null assignTime: string | null useUserId: number | null useUserNickname: string | null useOrderId: number | null useTime: string | null status: number statusName: string createTime: string } export interface InviteCodeQuery extends PagedRequest { code?: string batchNo?: string assignUserId?: number status?: number } export interface GenerateInviteCodesRequest { count: number } export interface GenerateResult { batchNo: string count: number codes: string[] } export interface AssignInviteCodesRequest { codeIds: number[] userId: number } // ==================== Commission 类型定义 ==================== export interface CommissionItem { id: number userId: number userNickname: string fromUserId: number fromUserNickname: string orderId: number orderNo: string orderAmount: number commissionRate: number commissionAmount: number level: number levelName: string status: number statusName: string settleTime: string | null createTime: string } export interface CommissionQuery extends PagedRequest { userId?: number fromUserId?: number orderId?: number level?: number status?: number createTimeStart?: string createTimeEnd?: string } export interface CommissionStatistics { totalAmount: number pendingAmount: number settledAmount: number totalCount: number pendingCount: number settledCount: number } // ==================== Withdrawal 类型定义 ==================== export interface WithdrawalItem { id: number withdrawalNo: string userId: number userNickname: string userPhone: string amount: number beforeBalance: number afterBalance: number status: number statusName: string auditUserId: number | null auditUserName: string | null auditTime: string | null auditRemark: string | null payTime: string | null payTransactionId: string | null createTime: string } export interface WithdrawalDetail extends WithdrawalItem { userBalance: number userTotalIncome: number userWithdrawnAmount: number } export interface WithdrawalQuery extends PagedRequest { withdrawalNo?: string userId?: number status?: number createTimeStart?: string createTimeEnd?: string } export interface ApproveWithdrawalRequest { id: number } export interface RejectWithdrawalRequest { id: number auditRemark: string } export interface CompleteWithdrawalRequest { id: number payTransactionId: string } // ==================== InviteCode API ==================== export function getInviteCodeList(params: InviteCodeQuery) { return request.get>('/api/admin/distribution/inviteCode/getList', { params }) } export function generateInviteCodes(data: GenerateInviteCodesRequest) { return request.post('/api/admin/distribution/inviteCode/generate', data) } export function assignInviteCodes(data: AssignInviteCodesRequest) { return request.post('/api/admin/distribution/inviteCode/assign', data) } export function exportInviteCodes(params: InviteCodeQuery) { return request.get('/api/admin/distribution/inviteCode/export', { params, responseType: 'blob' }) } // ==================== Commission API ==================== export function getCommissionList(params: CommissionQuery) { return request.get>('/api/admin/distribution/commission/getList', { params }) } export function getCommissionDetail(id: number) { return request.get('/api/admin/distribution/commission/getDetail', { params: { id } }) } export function getCommissionStatistics(params?: { userId?: number }) { return request.get('/api/admin/distribution/commission/getStatistics', { params }) } export function exportCommissions(params: CommissionQuery) { return request.get('/api/admin/distribution/commission/export', { params, responseType: 'blob' }) } // ==================== Withdrawal API ==================== export function getWithdrawalList(params: WithdrawalQuery) { return request.get>('/api/admin/distribution/withdrawal/getList', { params }) } export function getWithdrawalDetail(id: number) { return request.get('/api/admin/distribution/withdrawal/getDetail', { params: { id } }) } export function approveWithdrawal(data: ApproveWithdrawalRequest) { return request.post('/api/admin/distribution/withdrawal/approve', data) } export function rejectWithdrawal(data: RejectWithdrawalRequest) { return request.post('/api/admin/distribution/withdrawal/reject', data) } export function completeWithdrawal(data: CompleteWithdrawalRequest) { return request.post('/api/admin/distribution/withdrawal/complete', data) } export function exportWithdrawals(params: WithdrawalQuery) { return request.get('/api/admin/distribution/withdrawal/export', { params, responseType: 'blob' }) } ``` ### 2. 路由配置设计 ```typescript // router/business.ts - 业务模块路由配置 import type { RouteRecordRaw } from 'vue-router' import Layout from '@/layout/index.vue' export const businessRoutes: RouteRecordRaw[] = [ { path: '/business', component: Layout, redirect: '/business/config', meta: { title: '业务管理', icon: 'business' }, children: [ { path: 'config', name: 'Config', component: () => import('@/views/business/config/index.vue'), meta: { title: '系统配置', permission: 'config:view' } }, { path: 'content', name: 'Content', redirect: '/business/content/banner', meta: { title: '内容管理' }, children: [ { path: 'banner', name: 'Banner', component: () => import('@/views/business/content/banner/index.vue'), meta: { title: '轮播图管理', permission: 'banner:view' } }, { path: 'promotion', name: 'Promotion', component: () => import('@/views/business/content/promotion/index.vue'), meta: { title: '宣传图管理', permission: 'promotion:view' } } ] }, { path: 'assessment', name: 'Assessment', redirect: '/business/assessment/type', meta: { title: '测评管理' }, children: [ { path: 'type', name: 'AssessmentType', component: () => import('@/views/business/assessment/type/index.vue'), meta: { title: '测评类型', permission: 'assessment:view' } }, { path: 'question', name: 'Question', component: () => import('@/views/business/assessment/question/index.vue'), meta: { title: '题库管理', permission: 'question:view' } }, { path: 'category', name: 'Category', component: () => import('@/views/business/assessment/category/index.vue'), meta: { title: '报告分类', permission: 'category:view' } }, { path: 'conclusion', name: 'Conclusion', component: () => import('@/views/business/assessment/conclusion/index.vue'), meta: { title: '报告结论', permission: 'conclusion:view' } } ] }, { path: 'user', name: 'BusinessUser', component: () => import('@/views/business/user/index.vue'), meta: { title: '用户管理', permission: 'user:view' } }, { path: 'order', name: 'Order', component: () => import('@/views/business/order/index.vue'), meta: { title: '订单管理', permission: 'order:view' } }, { path: 'planner', name: 'Planner', redirect: '/business/planner/list', meta: { title: '规划师管理' }, children: [ { path: 'list', name: 'PlannerList', component: () => import('@/views/business/planner/index.vue'), meta: { title: '规划师列表', permission: 'planner:view' } }, { path: 'booking', name: 'Booking', component: () => import('@/views/business/planner/booking/index.vue'), meta: { title: '预约记录', permission: 'booking:view' } } ] }, { path: 'distribution', name: 'Distribution', redirect: '/business/distribution/invite-code', meta: { title: '分销管理' }, children: [ { path: 'invite-code', name: 'InviteCode', component: () => import('@/views/business/distribution/invite-code/index.vue'), meta: { title: '邀请码管理', permission: 'inviteCode:view' } }, { path: 'commission', name: 'Commission', component: () => import('@/views/business/distribution/commission/index.vue'), meta: { title: '佣金记录', permission: 'commission:view' } }, { path: 'withdrawal', name: 'Withdrawal', component: () => import('@/views/business/distribution/withdrawal/index.vue'), meta: { title: '提现审核', permission: 'withdrawal:view' } } ] } ] } ] ``` ## Data Models ### 页面组件数据模型 #### 通用列表页面状态 ```typescript // 通用列表页面状态接口 interface ListPageState { loading: boolean tableData: T[] total: number queryParams: Q dialogVisible: boolean dialogTitle: string formData: Partial formLoading: boolean } // 使用示例 - Banner 页面 interface BannerPageState extends ListPageState { // 额外的页面特定状态 } ``` #### 仪表盘页面数据模型 ```typescript interface DashboardState { loading: boolean overview: DashboardOverview | null trends: TrendsData | null pendingItems: PendingItems | null dateRange: [string, string] } ``` #### 配置页面数据模型 ```typescript interface ConfigPageState { loading: boolean configGroups: ConfigGroup[] editingKey: string | null editValue: string saving: boolean } ``` ### 字典类型定义 需要在后台配置的字典类型: | 字典编码 | 字典名称 | 字典项 | |---------|---------|--------| | banner_link_type | 轮播图跳转类型 | 1-内部页面, 2-外部链接, 3-小程序 | | common_status | 通用状态 | 0-禁用, 1-启用 | | promotion_position | 宣传图位置 | 1-首页底部, 2-团队页面 | | assessment_status | 测评状态 | 0-下线, 1-上线, 2-即将上线 | | user_level | 用户等级 | 1-普通用户, 2-合伙人, 3-渠道商 | | order_type | 订单类型 | 1-测评订单, 2-规划订单 | | order_status | 订单状态 | 1-待支付, 2-已支付, 3-已完成, 4-退款中, 5-已退款, 6-已取消 | | pay_type | 支付方式 | 1-微信支付 | | booking_status | 预约状态 | 1-待确认, 2-已确认, 3-已完成, 4-已取消 | | invite_code_status | 邀请码状态 | 1-未分配, 2-已分配, 3-已使用 | | commission_level | 佣金层级 | 1-直接下级, 2-间接下级 | | commission_status | 佣金状态 | 1-待结算, 2-已结算 | | withdrawal_status | 提现状态 | 1-待审核, 2-处理中, 3-已完成, 4-已取消 | | category_type | 分类类型 | 1-八大智能, 2-个人特质, 3-细分能力, 4-先天学习, 5-学习能力, 6-大脑类型, 7-性格类型, 8-未来能力 | | score_rule | 计分规则 | 1-累加(1-10), 2-二值(0/1) | | conclusion_type | 结论类型 | 1-最强, 2-较强, 3-较弱, 4-最弱 | | student_grade | 学生年级 | 1-小学, 2-初中, 3-高中 | ## Error Handling ### 前端错误处理策略 ```typescript // utils/error-handler.ts import { ElMessage, ElMessageBox } from 'element-plus' // API 错误码映射 const errorMessages: Record = { 1001: '参数错误', 1002: '未授权,请重新登录', 1003: 'Token已过期,请重新登录', 1004: '没有权限执行此操作', 2001: '业务处理失败', 2002: '数据不存在', 2003: '数据已存在', 2004: '数据正在使用中,无法删除', 2005: '无效的操作', 5000: '系统错误,请稍后重试' } // 处理 API 错误 export function handleApiError(code: number, message?: string) { const errorMessage = message || errorMessages[code] || '未知错误' if (code === 1002 || code === 1003) { // Token 相关错误,跳转登录 ElMessageBox.alert(errorMessage, '提示', { confirmButtonText: '重新登录', callback: () => { // 清除 token 并跳转登录页 localStorage.removeItem('token') window.location.href = '/login' } }) } else if (code === 1004) { // 权限错误 ElMessage.warning(errorMessage) } else { ElMessage.error(errorMessage) } } // 表单验证错误处理 export function handleValidationError(errors: Record) { const firstError = Object.values(errors)[0]?.[0] if (firstError) { ElMessage.warning(firstError) } } ``` ## Correctness Properties *A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* Based on the prework analysis, the following correctness properties have been identified for property-based testing: ### Property 1: Statistics Display Correctness *For any* valid API response containing statistics data (today's or cumulative), the Dashboard component SHALL render the exact values from the response in the corresponding statistics cards without modification. **Validates: Requirements 1.1, 1.2** ### Property 2: Config Grouping Correctness *For any* list of configuration items with various ConfigType values, the Config page SHALL group all items by their ConfigType and display each group in a separate collapsible panel. **Validates: Requirements 2.1** ### Property 3: Config Value Validation *For any* configuration update where ConfigType is 'price', the validation SHALL reject non-positive numbers. *For any* configuration update where ConfigType is 'commission', the validation SHALL reject values outside the range [0, 1]. **Validates: Requirements 2.3, 2.4, 2.5** ### Property 4: Banner Link Type Validation *For any* Banner form submission where LinkType is 1 (internal page) or 2 (external link), the form validation SHALL require LinkUrl to be non-empty. *For any* Banner form submission where LinkType is 3 (mini-program), the form validation SHALL require both LinkUrl and AppId to be non-empty. **Validates: Requirements 3.3, 3.4** ### Property 5: Price Field Validation *For any* form containing a price field (Assessment Type, Planner), the validation SHALL reject non-positive numbers and accept only positive decimal values. **Validates: Requirements 5.3, 12.3** ### Property 6: Query Parameter Handling *For any* list page with search/filter functionality, when the user enters search criteria, the page SHALL pass all non-empty filter values as query parameters to the corresponding API endpoint. **Validates: Requirements 9.2, 1.4** ### Property 7: Order Status Color Mapping *For any* order status value, the Order page SHALL map it to a consistent, predefined color according to the status-color mapping configuration. **Validates: Requirements 10.6** ### Property 8: Refund Amount Validation *For any* refund form submission, the validation SHALL reject refund amounts that exceed the original pay amount of the order. **Validates: Requirements 11.3** ### Property 9: Permission-Based UI Rendering *For any* UI element protected by v-permission directive and any user permission set, the element SHALL be visible if and only if the user has the required permission. **Validates: Requirements 19.1** ### Property 10: Dictionary Fallback Behavior *For any* field using dictionary components (DictSelect, DictRadio) where the dictionary data is unavailable or the value is not found in the dictionary, the component SHALL display the raw value with a visual warning indicator. **Validates: Requirements 20.4** ## Testing Strategy ### 测试框架 - **单元测试**: Vitest + Vue Test Utils - **属性测试**: fast-check - **组件测试**: @vue/test-utils ### 测试分层 1. **单元测试**: 测试工具函数、验证逻辑、数据转换 2. **组件测试**: 测试 Vue 组件的渲染和交互 3. **属性测试**: 验证核心业务规则的正确性 ### 测试文件结构 ``` src/ ├── api/business/__tests__/ │ ├── dashboard.test.ts │ ├── config.test.ts │ ├── content.test.ts │ └── ... ├── views/business/__tests__/ │ ├── config.test.ts │ ├── banner.test.ts │ └── ... └── utils/__tests__/ ├── validation.test.ts └── error-handler.test.ts ``` ### 属性测试配置 ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'] } } }) ``` ### 属性测试示例 ```typescript // src/utils/__tests__/validation.property.test.ts import { describe, it, expect } from 'vitest' import * as fc from 'fast-check' import { validatePrice, validateCommissionRate } from '../validation' describe('Validation Properties', () => { // Feature: admin-frontend, Property 3: Config Value Validation it('should reject non-positive prices', () => { fc.assert( fc.property( fc.double({ max: 0, noNaN: true }), (price) => { expect(validatePrice(price)).toBe(false) } ), { numRuns: 100 } ) }) // Feature: admin-frontend, Property 3: Config Value Validation it('should accept positive prices', () => { fc.assert( fc.property( fc.double({ min: 0.01, max: 1000000, noNaN: true }), (price) => { expect(validatePrice(price)).toBe(true) } ), { numRuns: 100 } ) }) // Feature: admin-frontend, Property 3: Config Value Validation it('should reject commission rates outside [0, 1]', () => { fc.assert( fc.property( fc.oneof( fc.double({ max: -0.01, noNaN: true }), fc.double({ min: 1.01, noNaN: true }) ), (rate) => { expect(validateCommissionRate(rate)).toBe(false) } ), { numRuns: 100 } ) }) // Feature: admin-frontend, Property 3: Config Value Validation it('should accept commission rates within [0, 1]', () => { fc.assert( fc.property( fc.double({ min: 0, max: 1, noNaN: true }), (rate) => { expect(validateCommissionRate(rate)).toBe(true) } ), { numRuns: 100 } ) }) }) ``` ### 组件测试示例 ```typescript // src/views/business/__tests__/banner.test.ts import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import * as fc from 'fast-check' import BannerForm from '../content/banner/components/BannerForm.vue' describe('Banner Form Properties', () => { // Feature: admin-frontend, Property 4: Banner Link Type Validation it('should require linkUrl when linkType is 1 or 2', () => { fc.assert( fc.property( fc.constantFrom(1, 2), (linkType) => { const wrapper = mount(BannerForm, { props: { formData: { linkType, linkUrl: '', appId: '' } } }) const form = wrapper.vm expect(form.rules.linkUrl[0].required).toBe(true) } ), { numRuns: 100 } ) }) // Feature: admin-frontend, Property 4: Banner Link Type Validation it('should require both linkUrl and appId when linkType is 3', () => { const wrapper = mount(BannerForm, { props: { formData: { linkType: 3, linkUrl: '', appId: '' } } }) const form = wrapper.vm expect(form.rules.linkUrl[0].required).toBe(true) expect(form.rules.appId[0].required).toBe(true) }) }) ``` ### 测试覆盖要求 - 工具函数覆盖率 > 90% - 验证逻辑 100% 覆盖 - 核心业务组件覆盖率 > 80% - 每个属性测试至少运行 100 次迭代