TeacherDashboard.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <template>
  2. <div class="teacher-dashboard-wrapper">
  3. <!-- 统计卡片 -->
  4. <el-row :gutter="24" class="td-stat-row">
  5. <el-col :span="6">
  6. <div class="td-stat-card">
  7. <div class="td-stat-icon" style="background: linear-gradient(135deg, #3B82F6 0%, #60A5FA 100%)">
  8. <i class="el-icon-user-solid"></i>
  9. </div>
  10. <div class="td-stat-content">
  11. <div class="td-stat-value">{{ teacherStats.serviceFamilyCount }}</div>
  12. <div class="td-stat-label">服务家庭</div>
  13. </div>
  14. </div>
  15. </el-col>
  16. <el-col :span="6">
  17. <div class="td-stat-card">
  18. <div class="td-stat-icon" style="background: linear-gradient(135deg, #10B981 0%, #34D399 100%)">
  19. <i class="el-icon-s-data"></i>
  20. </div>
  21. <div class="td-stat-content">
  22. <div class="td-stat-value">{{ teacherStats.activeFamilyCount }}</div>
  23. <div class="td-stat-label">活跃家庭</div>
  24. </div>
  25. </div>
  26. </el-col>
  27. <el-col :span="6">
  28. <div class="td-stat-card">
  29. <div class="td-stat-icon" style="background: linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)">
  30. <i class="el-icon-coin"></i>
  31. </div>
  32. <div class="td-stat-content">
  33. <div class="td-stat-value">{{ formatPriceWithSymbol(teacherStats.monthlySales) }}</div>
  34. <div class="td-stat-label">本月销售额</div>
  35. </div>
  36. </div>
  37. </el-col>
  38. <el-col :span="6">
  39. <div class="td-stat-card">
  40. <div class="td-stat-icon" style="background: linear-gradient(135deg, #8B5CF6 0%, #A78BFA 100%)">
  41. <i class="el-icon-wallet"></i>
  42. </div>
  43. <div class="td-stat-content">
  44. <div class="td-stat-value">{{ formatPriceWithSymbol(teacherStats.myCommission) }}</div>
  45. <div class="td-stat-label">我的佣金</div>
  46. </div>
  47. </div>
  48. </el-col>
  49. </el-row>
  50. <!-- 快捷操作 + 今日概览 -->
  51. <el-row :gutter="24" class="td-section-row">
  52. <el-col :span="8">
  53. <el-card class="td-quick-card" shadow="hover">
  54. <div slot="header" class="td-card-header">
  55. <span class="td-card-title">快捷操作</span>
  56. </div>
  57. <div class="td-quick-actions">
  58. <div class="td-action-item" @click="$router.push('/teacher-families')">
  59. <div class="td-action-icon" style="background: #EBF5FF">
  60. <i class="el-icon-user" style="color: #3B82F6"></i>
  61. </div>
  62. <span>管理家庭</span>
  63. </div>
  64. <div class="td-action-item" @click="$router.push('/teacher-messages')">
  65. <div class="td-action-icon" style="background: #ECFDF5">
  66. <i class="el-icon-chat-line-round" style="color: #10B981"></i>
  67. </div>
  68. <span>消息中心</span>
  69. </div>
  70. <div class="td-action-item" @click="$router.push('/teacher-orders')">
  71. <div class="td-action-icon" style="background: #FFFBEB">
  72. <i class="el-icon-document" style="color: #F59E0B"></i>
  73. </div>
  74. <span>查看订单</span>
  75. </div>
  76. <div class="td-action-item" @click="$router.push('/teacher-assessment')">
  77. <div class="td-action-icon" style="background: #F5F3FF">
  78. <i class="el-icon-edit-outline" style="color: #8B5CF6"></i>
  79. </div>
  80. <span>DAN测评</span>
  81. </div>
  82. <div class="td-action-item" @click="$router.push('/teacher-health-plan-review')">
  83. <div class="td-action-icon" style="background: #FEF3C7">
  84. <i class="el-icon-document-checked" style="color: #D97706"></i>
  85. </div>
  86. <span>健康方案审核</span>
  87. </div>
  88. </div>
  89. </el-card>
  90. </el-col>
  91. <el-col :span="16">
  92. <el-card class="td-overview-card" shadow="hover">
  93. <div slot="header" class="td-card-header">
  94. <span class="td-card-title">今日概览</span>
  95. <span class="td-card-date">{{ todayDate }}</span>
  96. </div>
  97. <div class="td-today-stats">
  98. <div class="td-today-stat-item">
  99. <div class="td-today-stat-value" style="color: #3B82F6">{{ todayStats.newFamilies }}</div>
  100. <div class="td-today-stat-label">新增家庭</div>
  101. </div>
  102. <div class="td-today-stat-item">
  103. <div class="td-today-stat-value" style="color: #10B981">{{ todayStats.completedTasks }}</div>
  104. <div class="td-today-stat-label">完成任务</div>
  105. </div>
  106. <div class="td-today-stat-item">
  107. <div class="td-today-stat-value" style="color: #F59E0B">{{ todayStats.newOrders }}</div>
  108. <div class="td-today-stat-label">新订单</div>
  109. </div>
  110. <div class="td-today-stat-item">
  111. <div class="td-today-stat-value" style="color: #8B5CF6">{{ todayStats.pendingReviews }}</div>
  112. <div class="td-today-stat-label">待审核</div>
  113. </div>
  114. </div>
  115. </el-card>
  116. </el-col>
  117. </el-row>
  118. <!-- 我的家庭 + 我的团队 -->
  119. <el-row :gutter="24" class="td-section-row">
  120. <el-col :span="12">
  121. <el-card class="td-list-card" shadow="hover">
  122. <div slot="header" class="td-card-header">
  123. <span class="td-card-title">我的家庭</span>
  124. <el-button type="text" @click="$router.push('/teacher-families')">查看全部 <i class="el-icon-arrow-right"></i></el-button>
  125. </div>
  126. <div class="td-family-list">
  127. <div v-for="family in families" :key="family.id" class="td-family-item">
  128. <div class="td-family-avatar">
  129. <i class="el-icon-house"></i>
  130. </div>
  131. <div class="td-family-info">
  132. <div class="td-family-name">{{ family.familyName || '家庭' + family.familyId }}</div>
  133. <div class="td-family-meta">
  134. <span class="td-meta-tag"><i class="el-icon-user"></i> {{ family.childrenCount || 0 }}个孩子</span>
  135. </div>
  136. </div>
  137. <div class="td-family-stats">
  138. <el-progress type="circle" :percentage="parseFloat(family.taskCompletionRate) || 0" :width="50" :stroke-width="4"></el-progress>
  139. </div>
  140. </div>
  141. <div v-if="families.length === 0" class="td-empty-state">
  142. <i class="el-icon-folder-delete"></i>
  143. <p>暂无服务家庭</p>
  144. </div>
  145. </div>
  146. </el-card>
  147. </el-col>
  148. <el-col :span="12">
  149. <el-card class="td-list-card" shadow="hover">
  150. <div slot="header" class="td-card-header">
  151. <span class="td-card-title">我的团队</span>
  152. <el-button type="text" @click="$router.push('/teacher-team')">查看全部 <i class="el-icon-arrow-right"></i></el-button>
  153. </div>
  154. <div class="td-team-list">
  155. <div v-for="member in teamMembers" :key="member.id" class="td-team-item">
  156. <div class="td-member-avatar">
  157. <i class="el-icon-user"></i>
  158. </div>
  159. <div class="td-member-info">
  160. <div class="td-member-name">{{ member.nickname || '规划师' + member.id }}</div>
  161. <div class="td-member-level">
  162. <el-tag size="mini" :type="getLevelType(member.level)" effect="dark">{{ member.level || '初级' }}</el-tag>
  163. </div>
  164. </div>
  165. <div class="td-member-performance">
  166. <div class="td-performance-value">{{ formatPriceWithSymbol(member.monthlySales || 0) }}</div>
  167. <div class="td-performance-label">本月业绩</div>
  168. </div>
  169. </div>
  170. <div v-if="teamMembers.length === 0" class="td-empty-state">
  171. <i class="el-icon-user-solid"></i>
  172. <p>暂无团队成员</p>
  173. </div>
  174. </div>
  175. </el-card>
  176. </el-col>
  177. </el-row>
  178. <!-- 最近订单 -->
  179. <el-row :gutter="24" class="td-section-row">
  180. <el-col :span="24">
  181. <el-card class="td-orders-card" shadow="hover">
  182. <div slot="header" class="td-card-header">
  183. <span class="td-card-title">最近订单</span>
  184. <el-button type="text" @click="$router.push('/teacher-orders')">查看全部 <i class="el-icon-arrow-right"></i></el-button>
  185. </div>
  186. <div class="adm-table-wrap">
  187. <el-table :data="recentOrders" stripe style="width: 100%" class="td-orders-table" height="100%">
  188. <el-table-column prop="orderNo" label="订单号" width="200">
  189. <template slot-scope="scope">
  190. <span class="td-order-no">{{ scope.row.orderNo }}</span>
  191. </template>
  192. </el-table-column>
  193. <el-table-column prop="familyName" label="家庭">
  194. <template slot-scope="scope">
  195. <div class="td-family-cell"><i class="el-icon-house"></i> {{ scope.row.familyName }}</div>
  196. </template>
  197. </el-table-column>
  198. <el-table-column prop="packageName" label="任务模板">
  199. <template slot-scope="scope">
  200. <el-tag size="small" effect="plain">{{ scope.row.packageName }}</el-tag>
  201. </template>
  202. </el-table-column>
  203. <el-table-column prop="amount" label="金额" width="120">
  204. <template slot-scope="scope">
  205. <span class="td-amount-value">{{ formatPriceWithSymbol(scope.row.amount) }}</span>
  206. </template>
  207. </el-table-column>
  208. <el-table-column prop="status" label="状态" width="100">
  209. <template slot-scope="scope">
  210. <el-tag :type="getStatusType(scope.row.status)" size="small" effect="dark">{{ scope.row.status }}</el-tag>
  211. </template>
  212. </el-table-column>
  213. <el-table-column prop="createdAt" label="时间" width="160"></el-table-column>
  214. </el-table>
  215. </div>
  216. </el-card>
  217. </el-col>
  218. </el-row>
  219. </div>
  220. </template>
  221. <script>
  222. import { getBoundFamilies, getTeamMembers, getGuideOrders, getGuideDashboardStats } from '@/api/guide'
  223. export default {
  224. name: 'TeacherDashboard',
  225. data() {
  226. return {
  227. teacherStats: {
  228. serviceFamilyCount: 0,
  229. activeFamilyCount: 0,
  230. monthlySales: 0,
  231. myCommission: 0
  232. },
  233. families: [],
  234. teamMembers: [],
  235. recentOrders: [],
  236. todayStats: {
  237. newFamilies: 0,
  238. completedTasks: 0,
  239. newOrders: 0,
  240. pendingReviews: 0
  241. }
  242. }
  243. },
  244. computed: {
  245. todayDate() {
  246. const now = new Date()
  247. const months = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
  248. return `${months[now.getMonth()]}月${now.getDate()}日 星期${['日', '一', '二', '三', '四', '五', '六'][now.getDay()]}`
  249. }
  250. },
  251. mounted() {
  252. this.loadTeacherData()
  253. },
  254. methods: {
  255. async loadTeacherData() {
  256. try {
  257. const results = await Promise.all([
  258. getBoundFamilies().catch(err => { console.error('获取家庭列表失败:', err); return { data: [] } }),
  259. getTeamMembers().catch(err => { console.error('获取团队成员失败:', err); return { data: [] } }),
  260. getGuideOrders().catch(err => { console.error('获取订单失败:', err); return { data: { records: [] } } }),
  261. getGuideDashboardStats().catch(err => { console.error('获取统计数据失败:', err); return { data: {} } })
  262. ])
  263. const [familyRes, teamRes, orderRes, statsRes] = results
  264. // Guard against malformed responses
  265. var familyList = familyRes && familyRes.data ? familyRes.data : []
  266. var teamList = teamRes && teamRes.data ? teamRes.data : []
  267. var orderData = orderRes && orderRes.data ? orderRes.data : { records: [] }
  268. var statsData = statsRes && statsRes.data ? statsRes.data : {}
  269. this.families = (Array.isArray(familyList) ? familyList : []).slice(0, 5)
  270. this.teacherStats.serviceFamilyCount = this.families.length
  271. this.teacherStats.activeFamilyCount = this.families.filter(function(f) { return f.status === 'active' }).length
  272. this.teamMembers = (Array.isArray(teamList) ? teamList : []).slice(0, 5).map(function(m) { return Object.assign({}, m, { trend: 0 }) })
  273. var records = Array.isArray(orderData.records) ? orderData.records : []
  274. this.recentOrders = records.slice(0, 10)
  275. var totalSales = records.reduce(function(sum, o) { return sum + (o.amount || 0) }, 0)
  276. this.teacherStats.monthlySales = totalSales
  277. this.teacherStats.myCommission = Math.round(totalSales * 0.1)
  278. var self = this
  279. this.todayStats.newOrders = records.filter(function(o) {
  280. var today = new Date().toDateString()
  281. return o.createdAt && new Date(o.createdAt).toDateString() === today
  282. }).length
  283. this.todayStats.newFamilies = statsData.totalFamilies || 0
  284. this.todayStats.completedTasks = statsData.todayCompletedTasks || 0
  285. this.todayStats.pendingReviews = statsData.pendingReviewTasks || 0
  286. } catch (e) {
  287. console.error('加载教师工作台数据失败:', e)
  288. }
  289. },
  290. formatPriceWithSymbol(price) {
  291. if (price === null || price === undefined) return '¥0'
  292. return '¥' + Number(price).toLocaleString()
  293. },
  294. getLevelType(level) {
  295. const map = { '初级': '', '中级': 'success', '高级': 'warning' }
  296. return map[level] || ''
  297. },
  298. getStatusType(status) {
  299. const map = { '已完成': 'success', '待支付': 'warning', '已取消': 'info' }
  300. return map[status] || 'info'
  301. }
  302. }
  303. }
  304. </script>
  305. <style scoped>
  306. /* ===== Teacher Dashboard Styles ===== */
  307. .teacher-dashboard-wrapper { padding: 0; }
  308. .td-stat-row { margin-bottom: 24px; }
  309. .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; }
  310. .td-stat-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(59,130,246,0.12); }
  311. .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; }
  312. .td-stat-value { font-size: 28px; font-weight: 700; color: #1E293B; line-height: 1.2; }
  313. .td-stat-label { font-size: 14px; color: #64748B; margin-top: 4px; }
  314. .td-section-row { margin-bottom: 24px; }
  315. .td-card-header { display: flex; justify-content: space-between; align-items: center; }
  316. .td-card-title { font-size: 16px; font-weight: 600; color: #1E293B; }
  317. .td-card-date { font-size: 13px; color: #64748B; }
  318. .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); }
  319. .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; }
  320. .td-quick-actions { display: flex; flex-wrap: wrap; gap: 16px; }
  321. .td-action-item { flex: 1 1 calc(50% - 8px); min-width: 120px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; background: #F8FAFC; border-radius: 12px; cursor: pointer; transition: all 0.2s; box-sizing: border-box; }
  322. .td-action-item:hover { background: #F1F5F9; transform: translateY(-2px); }
  323. .td-action-icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 8px; font-size: 20px; }
  324. .td-action-item span { font-size: 13px; color: #64748B; }
  325. .td-today-stats { display: flex; flex-wrap: wrap; gap: 16px; }
  326. .td-today-stat-item { flex: 1 1 calc(25% - 12px); min-width: 100px; text-align: center; padding: 12px; background: #F8FAFC; border-radius: 12px; box-sizing: border-box; }
  327. .td-today-stat-value { font-size: 28px; font-weight: 700; line-height: 1.2; }
  328. .td-today-stat-label { font-size: 13px; color: #64748B; margin-top: 4px; }
  329. .td-family-list, .td-team-list { max-height: 400px; overflow-y: auto; }
  330. .td-family-item, .td-team-item { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #F1F5F9; }
  331. .td-family-item:last-child, .td-team-item:last-child { border-bottom: none; }
  332. .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; }
  333. .td-family-info, .td-member-info { flex: 1; min-width: 0; }
  334. .td-family-name, .td-member-name { font-size: 14px; font-weight: 600; color: #1E293B; margin-bottom: 4px; }
  335. .td-family-meta { display: flex; gap: 12px; }
  336. .td-meta-tag { font-size: 12px; color: #64748B; }
  337. .td-meta-tag i { margin-right: 2px; }
  338. .td-family-stats { text-align: center; margin-left: 12px; }
  339. .td-member-level { margin-top: 2px; }
  340. .td-member-performance { text-align: right; margin-left: 12px; }
  341. .td-performance-value { font-size: 14px; font-weight: 600; color: #1E293B; }
  342. .td-performance-label { font-size: 11px; color: #64748B; margin-top: 2px; }
  343. .td-empty-state { text-align: center; padding: 40px 0; color: #94A3B8; }
  344. .td-empty-state i { font-size: 40px; margin-bottom: 8px; }
  345. .td-empty-state p { font-size: 14px; margin: 0; }
  346. .td-orders-table >>> .el-table__header th { background: #F8FAFC; color: #64748B; font-weight: 600; }
  347. .td-order-no { color: #3B82F6; font-weight: 500; }
  348. .td-family-cell i { margin-right: 4px; color: #64748B; }
  349. .td-amount-value { font-weight: 600; color: #1E293B; }
  350. </style>