|
|
@@ -1,5 +1,6 @@
|
|
|
<template>
|
|
|
- <div class="dashboard">
|
|
|
+ <div class="dashboard" :class="{ 'teacher-mode': isTeacher }">
|
|
|
+ <div v-if="!isTeacher" class="admin-dashboard">
|
|
|
<!-- 欢迎区域 -->
|
|
|
<div class="welcome-banner">
|
|
|
<div class="welcome-content">
|
|
|
@@ -178,11 +179,227 @@
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="teacher-dashboard-wrapper">
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <el-row :gutter="24" class="td-stat-row">
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="td-stat-card">
|
|
|
+ <div class="td-stat-icon" style="background: linear-gradient(135deg, #3B82F6 0%, #60A5FA 100%)">
|
|
|
+ <i class="el-icon-user-solid"></i>
|
|
|
+ </div>
|
|
|
+ <div class="td-stat-content">
|
|
|
+ <div class="td-stat-value">{{ teacherStats.serviceFamilyCount }}</div>
|
|
|
+ <div class="td-stat-label">服务家庭</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="td-stat-card">
|
|
|
+ <div class="td-stat-icon" style="background: linear-gradient(135deg, #10B981 0%, #34D399 100%)">
|
|
|
+ <i class="el-icon-s-data"></i>
|
|
|
+ </div>
|
|
|
+ <div class="td-stat-content">
|
|
|
+ <div class="td-stat-value">{{ teacherStats.activeFamilyCount }}</div>
|
|
|
+ <div class="td-stat-label">活跃家庭</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="td-stat-card">
|
|
|
+ <div class="td-stat-icon" style="background: linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)">
|
|
|
+ <i class="el-icon-coin"></i>
|
|
|
+ </div>
|
|
|
+ <div class="td-stat-content">
|
|
|
+ <div class="td-stat-value">{{ formatPriceWithSymbol(teacherStats.monthlySales) }}</div>
|
|
|
+ <div class="td-stat-label">本月销售额</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="td-stat-card">
|
|
|
+ <div class="td-stat-icon" style="background: linear-gradient(135deg, #8B5CF6 0%, #A78BFA 100%)">
|
|
|
+ <i class="el-icon-wallet"></i>
|
|
|
+ </div>
|
|
|
+ <div class="td-stat-content">
|
|
|
+ <div class="td-stat-value">{{ formatPriceWithSymbol(teacherStats.myCommission) }}</div>
|
|
|
+ <div class="td-stat-label">我的佣金</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 快捷操作 + 今日概览 -->
|
|
|
+ <el-row :gutter="24" class="td-section-row">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card class="td-quick-card" shadow="hover">
|
|
|
+ <div slot="header" class="td-card-header">
|
|
|
+ <span class="td-card-title">快捷操作</span>
|
|
|
+ </div>
|
|
|
+ <div class="td-quick-actions">
|
|
|
+ <div class="td-action-item" @click="$router.push('/teacher-families')">
|
|
|
+ <div class="td-action-icon" style="background: #EBF5FF">
|
|
|
+ <i class="el-icon-user" style="color: #3B82F6"></i>
|
|
|
+ </div>
|
|
|
+ <span>管理家庭</span>
|
|
|
+ </div>
|
|
|
+ <div class="td-action-item" @click="$router.push('/teacher-messages')">
|
|
|
+ <div class="td-action-icon" style="background: #ECFDF5">
|
|
|
+ <i class="el-icon-chat-line-round" style="color: #10B981"></i>
|
|
|
+ </div>
|
|
|
+ <span>消息中心</span>
|
|
|
+ </div>
|
|
|
+ <div class="td-action-item" @click="$router.push('/teacher-orders')">
|
|
|
+ <div class="td-action-icon" style="background: #FFFBEB">
|
|
|
+ <i class="el-icon-document" style="color: #F59E0B"></i>
|
|
|
+ </div>
|
|
|
+ <span>查看订单</span>
|
|
|
+ </div>
|
|
|
+ <div class="td-action-item" @click="$router.push('/teacher-assessment')">
|
|
|
+ <div class="td-action-icon" style="background: #F5F3FF">
|
|
|
+ <i class="el-icon-edit-outline" style="color: #8B5CF6"></i>
|
|
|
+ </div>
|
|
|
+ <span>DAN测评</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="16">
|
|
|
+ <el-card class="td-overview-card" shadow="hover">
|
|
|
+ <div slot="header" class="td-card-header">
|
|
|
+ <span class="td-card-title">今日概览</span>
|
|
|
+ <span class="td-card-date">{{ todayDate }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="td-today-stats">
|
|
|
+ <div class="td-today-stat-item">
|
|
|
+ <div class="td-today-stat-value" style="color: #3B82F6">{{ todayStats.newFamilies }}</div>
|
|
|
+ <div class="td-today-stat-label">新增家庭</div>
|
|
|
+ </div>
|
|
|
+ <div class="td-today-stat-item">
|
|
|
+ <div class="td-today-stat-value" style="color: #10B981">{{ todayStats.completedTasks }}</div>
|
|
|
+ <div class="td-today-stat-label">完成任务</div>
|
|
|
+ </div>
|
|
|
+ <div class="td-today-stat-item">
|
|
|
+ <div class="td-today-stat-value" style="color: #F59E0B">{{ todayStats.newOrders }}</div>
|
|
|
+ <div class="td-today-stat-label">新订单</div>
|
|
|
+ </div>
|
|
|
+ <div class="td-today-stat-item">
|
|
|
+ <div class="td-today-stat-value" style="color: #8B5CF6">{{ todayStats.pendingReviews }}</div>
|
|
|
+ <div class="td-today-stat-label">待审核</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 我的家庭 + 我的团队 -->
|
|
|
+ <el-row :gutter="24" class="td-section-row">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-card class="td-list-card" shadow="hover">
|
|
|
+ <div slot="header" class="td-card-header">
|
|
|
+ <span class="td-card-title">我的家庭</span>
|
|
|
+ <el-button type="text" @click="$router.push('/teacher-families')">查看全部 <i class="el-icon-arrow-right"></i></el-button>
|
|
|
+ </div>
|
|
|
+ <div class="td-family-list">
|
|
|
+ <div v-for="family in families" :key="family.id" class="td-family-item">
|
|
|
+ <div class="td-family-avatar">
|
|
|
+ <i class="el-icon-house"></i>
|
|
|
+ </div>
|
|
|
+ <div class="td-family-info">
|
|
|
+ <div class="td-family-name">{{ family.familyName || '家庭' + family.familyId }}</div>
|
|
|
+ <div class="td-family-meta">
|
|
|
+ <span class="td-meta-tag"><i class="el-icon-user"></i> {{ family.childrenCount || 0 }}个孩子</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="td-family-stats">
|
|
|
+ <el-progress type="circle" :percentage="family.taskCompletionRate || 0" :width="50" :stroke-width="4"></el-progress>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="families.length === 0" class="td-empty-state">
|
|
|
+ <i class="el-icon-folder-delete"></i>
|
|
|
+ <p>暂无服务家庭</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-card class="td-list-card" shadow="hover">
|
|
|
+ <div slot="header" class="td-card-header">
|
|
|
+ <span class="td-card-title">我的团队</span>
|
|
|
+ <el-button type="text" @click="$router.push('/teacher-team')">查看全部 <i class="el-icon-arrow-right"></i></el-button>
|
|
|
+ </div>
|
|
|
+ <div class="td-team-list">
|
|
|
+ <div v-for="member in teamMembers" :key="member.id" class="td-team-item">
|
|
|
+ <div class="td-member-avatar">
|
|
|
+ <i class="el-icon-user"></i>
|
|
|
+ </div>
|
|
|
+ <div class="td-member-info">
|
|
|
+ <div class="td-member-name">{{ member.nickname || '规划师' + member.id }}</div>
|
|
|
+ <div class="td-member-level">
|
|
|
+ <el-tag size="mini" :type="getLevelType(member.level)" effect="dark">{{ member.level || '初级' }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="td-member-performance">
|
|
|
+ <div class="td-performance-value">{{ formatPriceWithSymbol(member.monthlySales || 0) }}</div>
|
|
|
+ <div class="td-performance-label">本月业绩</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="teamMembers.length === 0" class="td-empty-state">
|
|
|
+ <i class="el-icon-user-solid"></i>
|
|
|
+ <p>暂无团队成员</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 最近订单 -->
|
|
|
+ <el-row :gutter="24" class="td-section-row">
|
|
|
+ <el-col :span="24">
|
|
|
+ <el-card class="td-orders-card" shadow="hover">
|
|
|
+ <div slot="header" class="td-card-header">
|
|
|
+ <span class="td-card-title">最近订单</span>
|
|
|
+ <el-button type="text" @click="$router.push('/teacher-orders')">查看全部 <i class="el-icon-arrow-right"></i></el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="recentOrders" stripe style="width: 100%" class="td-orders-table">
|
|
|
+ <el-table-column prop="orderNo" label="订单号" width="200">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span class="td-order-no">{{ scope.row.orderNo }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="familyName" label="家庭">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div class="td-family-cell"><i class="el-icon-house"></i> {{ scope.row.familyName }}</div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="packageName" label="套餐">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag size="small" effect="plain">{{ scope.row.packageName }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="amount" label="金额" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span class="td-amount-value">{{ formatPriceWithSymbol(scope.row.amount) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="status" label="状态" width="100">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag :type="getStatusType(scope.row.status)" size="small" effect="dark">{{ scope.row.status }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createdAt" label="时间" width="160"></el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import { getDashboardStats } from '@/api/admin'
|
|
|
+import { getBoundFamilies, getTeamMembers, getGuideOrders, getGuideDashboardStats } from '@/api/guide'
|
|
|
|
|
|
export default {
|
|
|
name: 'Dashboard',
|
|
|
@@ -197,10 +414,28 @@ export default {
|
|
|
pendingGuideCount: 0,
|
|
|
pendingPackageCount: 0
|
|
|
},
|
|
|
- recentTasks: []
|
|
|
+ recentTasks: [],
|
|
|
+ teacherStats: {
|
|
|
+ serviceFamilyCount: 0,
|
|
|
+ activeFamilyCount: 0,
|
|
|
+ monthlySales: 0,
|
|
|
+ myCommission: 0
|
|
|
+ },
|
|
|
+ families: [],
|
|
|
+ teamMembers: [],
|
|
|
+ recentOrders: [],
|
|
|
+ todayStats: {
|
|
|
+ newFamilies: 0,
|
|
|
+ completedTasks: 0,
|
|
|
+ newOrders: 0,
|
|
|
+ pendingReviews: 0
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
+ isTeacher() {
|
|
|
+ return localStorage.getItem('role') === 'teacher'
|
|
|
+ },
|
|
|
todayDate() {
|
|
|
const now = new Date()
|
|
|
const months = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
|
|
|
@@ -208,7 +443,11 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
- this.loadDashboardData()
|
|
|
+ if (this.isTeacher) {
|
|
|
+ this.loadTeacherData()
|
|
|
+ } else {
|
|
|
+ this.loadDashboardData()
|
|
|
+ }
|
|
|
},
|
|
|
methods: {
|
|
|
async loadDashboardData() {
|
|
|
@@ -230,6 +469,54 @@ export default {
|
|
|
} catch (error) {
|
|
|
console.error('加载仪表盘数据失败:', error)
|
|
|
}
|
|
|
+ },
|
|
|
+ async loadTeacherData() {
|
|
|
+ try {
|
|
|
+ const results = await Promise.all([
|
|
|
+ getBoundFamilies().catch(err => { console.error('获取家庭列表失败:', err); return { data: [] } }),
|
|
|
+ getTeamMembers().catch(err => { console.error('获取团队成员失败:', err); return { data: [] } }),
|
|
|
+ getGuideOrders().catch(err => { console.error('获取订单失败:', err); return { data: { records: [] } } }),
|
|
|
+ getGuideDashboardStats().catch(err => { console.error('获取统计数据失败:', err); return { data: {} } })
|
|
|
+ ])
|
|
|
+ const [familyRes, teamRes, orderRes, statsRes] = results
|
|
|
+ if (familyRes.data) {
|
|
|
+ this.families = familyRes.data.slice(0, 5)
|
|
|
+ this.teacherStats.serviceFamilyCount = familyRes.data.length
|
|
|
+ this.teacherStats.activeFamilyCount = familyRes.data.filter(f => f.status === 'active').length
|
|
|
+ }
|
|
|
+ if (teamRes.data) {
|
|
|
+ this.teamMembers = teamRes.data.slice(0, 5).map(m => ({ ...m, trend: 0 }))
|
|
|
+ }
|
|
|
+ if (orderRes.data && orderRes.data.records) {
|
|
|
+ this.recentOrders = orderRes.data.records.slice(0, 10)
|
|
|
+ const totalSales = orderRes.data.records.reduce((sum, o) => sum + (o.amount || 0), 0)
|
|
|
+ this.teacherStats.monthlySales = totalSales
|
|
|
+ this.teacherStats.myCommission = Math.round(totalSales * 0.1)
|
|
|
+ this.todayStats.newOrders = orderRes.data.records.filter(o => {
|
|
|
+ const today = new Date().toDateString()
|
|
|
+ return new Date(o.createdAt).toDateString() === today
|
|
|
+ }).length
|
|
|
+ }
|
|
|
+ if (statsRes.data) {
|
|
|
+ this.todayStats.newFamilies = statsRes.data.totalFamilies || 0
|
|
|
+ this.todayStats.completedTasks = statsRes.data.todayCompletedTasks || 0
|
|
|
+ this.todayStats.pendingReviews = statsRes.data.pendingReviewTasks || 0
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载教师工作台数据失败:', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ formatPriceWithSymbol(price) {
|
|
|
+ if (price === null || price === undefined) return '¥0'
|
|
|
+ return '¥' + Number(price).toLocaleString()
|
|
|
+ },
|
|
|
+ getLevelType(level) {
|
|
|
+ const map = { '初级': '', '中级': 'success', '高级': 'warning' }
|
|
|
+ return map[level] || ''
|
|
|
+ },
|
|
|
+ getStatusType(status) {
|
|
|
+ const map = { '已完成': 'success', '待支付': 'warning', '已取消': 'info' }
|
|
|
+ return map[status] || 'info'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -459,4 +746,49 @@ export default {
|
|
|
font-size: 12px;
|
|
|
color: #64748B;
|
|
|
}
|
|
|
+
|
|
|
+/* ===== Teacher Dashboard Styles ===== */
|
|
|
+.teacher-dashboard-wrapper { padding: 0; }
|
|
|
+.td-stat-row { margin-bottom: 24px; }
|
|
|
+.td-stat-card { display: flex; align-items: center; padding: 20px 24px; background: #FFFFFF; border-radius: 16px; box-shadow: 0 2px 12px rgba(59,130,246,0.08); border: none; transition: all 0.3s; }
|
|
|
+.td-stat-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(59,130,246,0.12); }
|
|
|
+.td-stat-icon { width: 56px; height: 56px; border-radius: 14px; display: flex; align-items: center; justify-content: center; margin-right: 16px; font-size: 24px; color: #FFFFFF; flex-shrink: 0; }
|
|
|
+.td-stat-value { font-size: 28px; font-weight: 700; color: #1E293B; line-height: 1.2; }
|
|
|
+.td-stat-label { font-size: 14px; color: #64748B; margin-top: 4px; }
|
|
|
+.td-section-row { margin-bottom: 24px; }
|
|
|
+.td-card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
+.td-card-title { font-size: 16px; font-weight: 600; color: #1E293B; }
|
|
|
+.td-card-date { font-size: 13px; color: #64748B; }
|
|
|
+.td-quick-card, .td-overview-card, .td-list-card, .td-orders-card { border-radius: 16px; border: none; box-shadow: 0 2px 12px rgba(59,130,246,0.06); }
|
|
|
+.td-quick-card >>> .el-card__header, .td-overview-card >>> .el-card__header, .td-list-card >>> .el-card__header, .td-orders-card >>> .el-card__header { border-bottom: 1px solid #F1F5F9; padding: 16px 20px; }
|
|
|
+.td-quick-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
|
|
+.td-action-item { display: flex; flex-direction: column; align-items: center; padding: 16px; background: #F8FAFC; border-radius: 12px; cursor: pointer; transition: all 0.2s; }
|
|
|
+.td-action-item:hover { background: #F1F5F9; transform: translateY(-2px); }
|
|
|
+.td-action-icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 8px; font-size: 20px; }
|
|
|
+.td-action-item span { font-size: 13px; color: #64748B; }
|
|
|
+.td-today-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
|
|
+.td-today-stat-item { text-align: center; padding: 12px; background: #F8FAFC; border-radius: 12px; }
|
|
|
+.td-today-stat-value { font-size: 28px; font-weight: 700; line-height: 1.2; }
|
|
|
+.td-today-stat-label { font-size: 13px; color: #64748B; margin-top: 4px; }
|
|
|
+.td-family-list, .td-team-list { max-height: 400px; overflow-y: auto; }
|
|
|
+.td-family-item, .td-team-item { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #F1F5F9; }
|
|
|
+.td-family-item:last-child, .td-team-item:last-child { border-bottom: none; }
|
|
|
+.td-family-avatar, .td-member-avatar { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: #EBF5FF; color: #3B82F6; font-size: 20px; margin-right: 12px; flex-shrink: 0; }
|
|
|
+.td-family-info, .td-member-info { flex: 1; min-width: 0; }
|
|
|
+.td-family-name, .td-member-name { font-size: 14px; font-weight: 600; color: #1E293B; margin-bottom: 4px; }
|
|
|
+.td-family-meta { display: flex; gap: 12px; }
|
|
|
+.td-meta-tag { font-size: 12px; color: #64748B; }
|
|
|
+.td-meta-tag i { margin-right: 2px; }
|
|
|
+.td-family-stats { text-align: center; margin-left: 12px; }
|
|
|
+.td-member-level { margin-top: 2px; }
|
|
|
+.td-member-performance { text-align: right; margin-left: 12px; }
|
|
|
+.td-performance-value { font-size: 14px; font-weight: 600; color: #1E293B; }
|
|
|
+.td-performance-label { font-size: 11px; color: #64748B; margin-top: 2px; }
|
|
|
+.td-empty-state { text-align: center; padding: 40px 0; color: #94A3B8; }
|
|
|
+.td-empty-state i { font-size: 40px; margin-bottom: 8px; }
|
|
|
+.td-empty-state p { font-size: 14px; margin: 0; }
|
|
|
+.td-orders-table >>> .el-table__header th { background: #F8FAFC; color: #64748B; font-weight: 600; }
|
|
|
+.td-order-no { color: #3B82F6; font-weight: 500; }
|
|
|
+.td-family-cell i { margin-right: 4px; color: #64748B; }
|
|
|
+.td-amount-value { font-weight: 600; color: #1E293B; }
|
|
|
</style>
|