Dashboard.vue 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  1. <template>
  2. <div class="dashboard">
  3. <div class="admin-dashboard">
  4. <!-- 欢迎区域 -->
  5. <div class="welcome-banner">
  6. <div class="welcome-content">
  7. <h1 class="welcome-title">欢迎回来!👋</h1>
  8. <p class="welcome-desc">今天是 {{ todayDate }},来看看今天的运营数据吧</p>
  9. </div>
  10. <div class="welcome-illustration">📊</div>
  11. </div>
  12. <!-- 统计卡片 -->
  13. <el-row :gutter="20" class="stats-row">
  14. <el-col :xs="12" :sm="12" :md="6">
  15. <el-card class="stat-card" shadow="hover">
  16. <div class="stat-icon" style="background: linear-gradient(135deg, #FF8C42, #FFA66B);">
  17. <i class="el-icon-s-home"></i>
  18. </div>
  19. <div class="stat-info">
  20. <div class="stat-value">{{ stats.totalFamilies || 0 }}</div>
  21. <div class="stat-label">家庭总数</div>
  22. </div>
  23. </el-card>
  24. </el-col>
  25. <el-col :xs="12" :sm="12" :md="6">
  26. <el-card class="stat-card" shadow="hover">
  27. <div class="stat-icon" style="background: linear-gradient(135deg, #6366F1, #818CF8);">
  28. <i class="el-icon-user"></i>
  29. </div>
  30. <div class="stat-info">
  31. <div class="stat-value">{{ stats.totalParents || 0 }}</div>
  32. <div class="stat-label">家长总数</div>
  33. </div>
  34. </el-card>
  35. </el-col>
  36. <el-col :xs="12" :sm="12" :md="6">
  37. <el-card class="stat-card" shadow="hover">
  38. <div class="stat-icon" style="background: linear-gradient(135deg, #F59E0B, #FBBF24);">
  39. <i class="el-icon-s-custom"></i>
  40. </div>
  41. <div class="stat-info">
  42. <div class="stat-value">{{ stats.totalChildren || 0 }}</div>
  43. <div class="stat-label">孩子总数</div>
  44. </div>
  45. </el-card>
  46. </el-col>
  47. <el-col :xs="12" :sm="12" :md="6">
  48. <el-card class="stat-card" shadow="hover">
  49. <div class="stat-icon" style="background: linear-gradient(135deg, #10B981, #34D399);">
  50. <i class="el-icon-s-cooperation"></i>
  51. </div>
  52. <div class="stat-info">
  53. <div class="stat-value">{{ stats.totalTeachers || 0 }}</div>
  54. <div class="stat-label">规划师总数</div>
  55. </div>
  56. </el-card>
  57. </el-col>
  58. </el-row>
  59. <!-- 第二行统计卡片 -->
  60. <el-row :gutter="20" class="stats-row">
  61. <el-col :xs="12" :sm="12" :md="6">
  62. <el-card class="stat-card todo-stat-card" shadow="hover" @click.native="stats.pendingGuideCount > 0 && $router.push('/review-center?tab=guide')">
  63. <div class="stat-icon" :class="stats.pendingGuideCount > 0 ? 'todo-heart' : 'todo-disabled'">
  64. <i class="el-icon-s-check"></i>
  65. </div>
  66. <div class="stat-info">
  67. <div class="stat-value">{{ stats.pendingGuideCount }}</div>
  68. <div class="stat-label">待审核规划师</div>
  69. <div class="stat-trend" :class="stats.pendingGuideCount > 0 ? '' : 'disabled'">
  70. {{ stats.pendingGuideCount > 0 ? '点击前往审核' : '暂无待审核' }}
  71. </div>
  72. </div>
  73. </el-card>
  74. </el-col>
  75. <el-col :xs="12" :sm="12" :md="6">
  76. <el-card class="stat-card todo-stat-card" shadow="hover" @click.native="stats.pendingPackageCount > 0 && $router.push('/review-center?tab=package')">
  77. <div class="stat-icon" :class="stats.pendingPackageCount > 0 ? 'todo-danger' : 'todo-disabled'">
  78. <i class="el-icon-s-goods"></i>
  79. </div>
  80. <div class="stat-info">
  81. <div class="stat-value">{{ stats.pendingPackageCount }}</div>
  82. <div class="stat-label">待审核任务模板</div>
  83. <div class="stat-trend" :class="stats.pendingPackageCount > 0 ? '' : 'disabled'">
  84. {{ stats.pendingPackageCount > 0 ? '点击前往审核' : '暂无待审核' }}
  85. </div>
  86. </div>
  87. </el-card>
  88. </el-col>
  89. <el-col :xs="12" :sm="12" :md="6">
  90. <el-card class="stat-card" shadow="hover">
  91. <div class="stat-icon" style="background: linear-gradient(135deg, #0EA5E9, #38BDF8);">
  92. <i class="el-icon-user-solid"></i>
  93. </div>
  94. <div class="stat-info">
  95. <div class="stat-value">{{ stats.activeUsers || 0 }}</div>
  96. <div class="stat-label">活跃用户</div>
  97. </div>
  98. </el-card>
  99. </el-col>
  100. <el-col :xs="12" :sm="12" :md="6">
  101. <el-card class="stat-card" shadow="hover">
  102. <div class="stat-icon" style="background: linear-gradient(135deg, #8B5CF6, #A78BFA);">
  103. <i class="el-icon-s-home"></i>
  104. </div>
  105. <div class="stat-info">
  106. <div class="stat-value">{{ stats.monthFamilies || 0 }}</div>
  107. <div class="stat-label">本月新增家庭</div>
  108. </div>
  109. </el-card>
  110. </el-col>
  111. </el-row>
  112. <!-- 图表区域 -->
  113. <el-row :gutter="20" class="chart-row">
  114. <el-col :span="16">
  115. <el-card class="chart-card" v-loading="trendLoading">
  116. <div slot="header" class="card-header">
  117. <span class="card-title">📈 用户与家庭增长趋势(近30天)</span>
  118. </div>
  119. <div ref="trendChart" class="echarts-container"></div>
  120. </el-card>
  121. </el-col>
  122. <el-col :span="8">
  123. <el-card class="chart-card" v-loading="memberLoading">
  124. <div slot="header" class="card-header">
  125. <span class="card-title">🍩 会员等级分布</span>
  126. </div>
  127. <div ref="memberPieChart" class="echarts-container"></div>
  128. </el-card>
  129. </el-col>
  130. </el-row>
  131. <!-- 内容区域 -->
  132. <el-row :gutter="20" class="content-row">
  133. <el-col :span="14">
  134. <el-card class="content-card" shadow="hover">
  135. <div slot="header" class="card-header">
  136. <span class="card-title">📋 最近任务</span>
  137. <el-button type="text" size="small" @click="$router.push('/tasks')">
  138. 查看全部 →
  139. </el-button>
  140. </div>
  141. <div class="adm-table-wrap">
  142. <el-table :data="recentTasks" size="small" class="custom-table" height="100%">
  143. <el-table-column prop="title" label="任务名称" min-width="180"></el-table-column>
  144. <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
  145. <el-table-column prop="status" label="状态" width="100">
  146. <template slot-scope="scope">
  147. <el-tag
  148. :type="scope.row.status === 'completed' ? 'success' : 'warning'"
  149. size="small"
  150. effect="plain"
  151. >
  152. {{ scope.row.status === 'completed' ? '已完成' : '进行中' }}
  153. </el-tag>
  154. </template>
  155. </el-table-column>
  156. <el-table-column prop="points" label="积分" width="80">
  157. <template slot-scope="scope">
  158. <span class="points-text">+{{ scope.row.points }}</span>
  159. </template>
  160. </el-table-column>
  161. </el-table>
  162. </div>
  163. </el-card>
  164. </el-col>
  165. <el-col :span="10">
  166. <el-card class="content-card system-health-card" shadow="hover">
  167. <div slot="header" class="card-header">
  168. <span class="card-title">⚙️ 系统状态</span>
  169. <el-tag size="mini" type="success">正常运行</el-tag>
  170. </div>
  171. <div class="health-grid">
  172. <div class="health-item">
  173. <div class="health-icon health-icon--status">
  174. <i class="el-icon-success"></i>
  175. </div>
  176. <div class="health-info">
  177. <div class="health-label">系统状态</div>
  178. <div class="health-value health-value--online">● 在线</div>
  179. </div>
  180. </div>
  181. <div class="health-item">
  182. <div class="health-icon health-icon--version">
  183. <i class="el-icon-info"></i>
  184. </div>
  185. <div class="health-info">
  186. <div class="health-label">系统版本</div>
  187. <div class="health-value">
  188. {{ version }}
  189. <button type="button" style="font-size:12px;border:none;background:none;color:#409EFF;cursor:pointer;padding:0;margin-left:6px" @click="showChangelog()">查看变更日志</button>
  190. </div>
  191. </div>
  192. </div>
  193. <div class="health-item">
  194. <div class="health-icon health-icon--time">
  195. <i class="el-icon-time"></i>
  196. </div>
  197. <div class="health-info">
  198. <div class="health-label">最后刷新</div>
  199. <div class="health-value">{{ todayDate }}</div>
  200. </div>
  201. </div>
  202. <div class="health-item">
  203. <div class="health-icon health-icon--api">
  204. <i class="el-icon-link"></i>
  205. </div>
  206. <div class="health-info">
  207. <div class="health-label">API 接口</div>
  208. <div class="health-value">~45 个</div>
  209. </div>
  210. </div>
  211. <div class="health-item">
  212. <div class="health-icon health-icon--uptime">
  213. <i class="el-icon-time"></i>
  214. </div>
  215. <div class="health-info">
  216. <div class="health-label">服务器运行时长</div>
  217. <div class="health-value">{{ serverHealth.uptime }}</div>
  218. </div>
  219. </div>
  220. <div class="health-item">
  221. <div class="health-icon health-icon--cpu">
  222. <i class="el-icon-cpu"></i>
  223. </div>
  224. <div class="health-info">
  225. <div class="health-label">CPU 使用率</div>
  226. <div class="health-value">{{ serverHealth.cpuUsagePercent }}% ({{ serverHealth.cpuCores }}核)</div>
  227. </div>
  228. </div>
  229. <div class="health-item">
  230. <div class="health-icon health-icon--memory">
  231. <i class="el-icon-coffee"></i>
  232. </div>
  233. <div class="health-info">
  234. <div class="health-label">JVM 内存</div>
  235. <div class="health-value">{{ serverHealth.memUsed }} / {{ serverHealth.memMax }} ({{ serverHealth.memUsagePercent }}%)</div>
  236. </div>
  237. </div>
  238. </div>
  239. <div class="health-links">
  240. <el-button size="mini" icon="el-icon-s-order" @click="$router.push('/tasks')">任务管理</el-button>
  241. <el-button size="mini" icon="el-icon-s-custom" @click="$router.push('/families')">家庭管理</el-button>
  242. <el-button size="mini" icon="el-icon-user" @click="$router.push('/users')">用户管理</el-button>
  243. </div>
  244. </el-card>
  245. </el-col>
  246. </el-row>
  247. <!-- 快捷操作 -->
  248. <el-row :gutter="20">
  249. <el-col :span="24">
  250. <div class="quick-actions-section">
  251. <div class="section-header">
  252. <span class="section-title">快捷操作</span>
  253. </div>
  254. <div class="quick-actions-grid">
  255. <div class="action-card" @click="$router.push('/tasks')">
  256. <div class="action-icon action-icon--primary">
  257. <i class="el-icon-s-order"></i>
  258. </div>
  259. <div class="action-label">任务管理</div>
  260. </div>
  261. <div class="action-card" @click="$router.push('/families')">
  262. <div class="action-icon action-icon--family">
  263. <i class="el-icon-s-custom"></i>
  264. </div>
  265. <div class="action-label">家庭管理</div>
  266. </div>
  267. <div class="action-card" @click="$router.push('/guide-pending')">
  268. <div class="action-icon action-icon--guide">
  269. <i class="el-icon-user"></i>
  270. </div>
  271. <div class="action-label">规划师审核</div>
  272. </div>
  273. <div class="action-card" @click="$router.push('/package-pending')">
  274. <div class="action-icon action-icon--package">
  275. <i class="el-icon-goods"></i>
  276. </div>
  277. <div class="action-label">任务模板审核</div>
  278. </div>
  279. <div class="action-card" @click="$router.push('/rewards')">
  280. <div class="action-icon action-icon--reward">
  281. <i class="el-icon-medal"></i>
  282. </div>
  283. <div class="action-label">奖励管理</div>
  284. </div>
  285. <div class="action-card" @click="$router.push('/users')">
  286. <div class="action-icon action-icon--user">
  287. <i class="el-icon-user-solid"></i>
  288. </div>
  289. <div class="action-label">用户管理</div>
  290. </div>
  291. <div class="action-card" @click="$router.push('/audit')">
  292. <div class="action-icon action-icon--audit">
  293. <i class="el-icon-document-checked"></i>
  294. </div>
  295. <div class="action-label">审核中心</div>
  296. </div>
  297. <div class="action-card" @click="$router.push('/settings')">
  298. <div class="action-icon action-icon--settings">
  299. <i class="el-icon-setting"></i>
  300. </div>
  301. <div class="action-label">系统设置</div>
  302. </div>
  303. </div>
  304. </div>
  305. </el-col>
  306. </el-row>
  307. <!-- 变更日志弹窗 -->
  308. <el-dialog
  309. title="变更日志"
  310. :visible.sync="changelogVisible"
  311. width="900px"
  312. custom-class="changelog-dialog"
  313. append-to-body
  314. >
  315. <div v-loading="changelogLoading" class="changelog-body" v-html="changelogContent"></div>
  316. </el-dialog>
  317. </div>
  318. </div>
  319. </template>
  320. <script>
  321. import * as echarts from 'echarts'
  322. import { getDashboardStats, getMembershipStats, getTrendStats, getDashboardOverview, getSystemHealth } from '@/api/admin'
  323. export default {
  324. name: 'Dashboard',
  325. data() {
  326. return {
  327. loadingTasks: false,
  328. stats: {
  329. totalFamilies: 0,
  330. totalParents: 0,
  331. totalChildren: 0,
  332. totalTeachers: 0,
  333. pendingGuideCount: 0,
  334. pendingPackageCount: 0,
  335. activeUsers: 0,
  336. monthFamilies: 0
  337. },
  338. recentTasks: [],
  339. trendLoading: false,
  340. memberLoading: false,
  341. membershipData: {},
  342. trendData: { familyTrend: {}, userTrend: {} },
  343. trendChart: null,
  344. memberChart: null,
  345. resizeHandler: null,
  346. changelogVisible: false,
  347. changelogContent: '',
  348. changelogLoading: false,
  349. serverHealth: {
  350. uptime: '加载中...',
  351. memUsed: '--',
  352. memMax: '--',
  353. memUsagePercent: '--',
  354. cpuCores: '--',
  355. cpuLoad: '--',
  356. cpuUsagePercent: '--'
  357. }
  358. }
  359. },
  360. computed: {
  361. version() {
  362. return process.env.VUE_APP_VERSION || '1.0.0'
  363. },
  364. todayDate() {
  365. const now = new Date()
  366. const months = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
  367. return `${months[now.getMonth()]}月${now.getDate()}日 星期${['日', '一', '二', '三', '四', '五', '六'][now.getDay()]}`
  368. }
  369. },
  370. mounted() {
  371. this.loadDashboardData()
  372. this.loadTrend()
  373. this.loadMembership()
  374. this.loadServerHealth()
  375. this.resizeHandler = () => {
  376. this.trendChart && this.trendChart.resize()
  377. this.memberChart && this.memberChart.resize()
  378. }
  379. window.addEventListener('resize', this.resizeHandler)
  380. },
  381. beforeDestroy() {
  382. this.trendChart && this.trendChart.dispose()
  383. this.memberChart && this.memberChart.dispose()
  384. this.resizeHandler && window.removeEventListener('resize', this.resizeHandler)
  385. },
  386. methods: {
  387. async loadDashboardData() {
  388. try {
  389. const res = await getDashboardStats()
  390. const d = res.data || {}
  391. this.stats.totalFamilies = d.totalFamilies || 0
  392. this.stats.totalParents = d.totalParents || 0
  393. this.stats.totalChildren = d.totalChildren || 0
  394. this.stats.totalTeachers = d.totalTeachers || 0
  395. this.stats.pendingGuideCount = d.pendingGuideCount || 0
  396. this.stats.pendingPackageCount = d.pendingPackageCount || 0
  397. this.recentTasks = (d.recentTasks || []).map(t => ({
  398. title: t.title,
  399. childName: t.childName || '',
  400. status: t.status,
  401. points: t.points || 0
  402. }))
  403. const overviewRes = await getDashboardOverview()
  404. const overview = overviewRes.data || {}
  405. this.stats.activeUsers = overview.activeUsers || 0
  406. this.stats.monthFamilies = overview.monthFamilies || 0
  407. } catch (error) {
  408. console.error('加载仪表盘数据失败:', error)
  409. }
  410. },
  411. async loadServerHealth() {
  412. try {
  413. const res = await getSystemHealth()
  414. if (res.code === 200 && res.data) {
  415. const d = res.data
  416. this.serverHealth = {
  417. uptime: d.uptime || '--',
  418. memUsed: d.memUsed || '--',
  419. memMax: d.memMax || '--',
  420. memUsagePercent: d.memUsagePercent || '--',
  421. cpuCores: d.cpuCores || '--',
  422. cpuLoad: d.cpuLoad || '--',
  423. cpuUsagePercent: d.cpuUsagePercent || '--'
  424. }
  425. }
  426. } catch (error) {
  427. console.error('加载服务器状态失败:', error)
  428. }
  429. },
  430. showChangelog() {
  431. this.changelogVisible = true
  432. if (this.changelogContent) return
  433. this.changelogLoading = true
  434. fetch(process.env.BASE_URL + 'CHANGELOG.md?t=' + Date.now()).then(function(res) {
  435. if (!res.ok) throw new Error('Not found')
  436. return res.text()
  437. }).then((function(text) {
  438. var html = ''
  439. var lines = text.split('\n')
  440. var inCodeBlock = false
  441. var listBuffer = ''
  442. var inList = false
  443. function flushList() {
  444. if (inList) {
  445. html += '<ul>' + listBuffer + '</ul>'
  446. listBuffer = ''
  447. inList = false
  448. }
  449. }
  450. for (var i = 0; i < lines.length; i++) {
  451. var line = lines[i]
  452. if (line.match(/^```/)) {
  453. flushList()
  454. if (inCodeBlock) {
  455. html += '</code></pre>'
  456. inCodeBlock = false
  457. } else {
  458. html += '<pre><code>'
  459. inCodeBlock = true
  460. }
  461. continue
  462. }
  463. if (inCodeBlock) {
  464. html += this._escapeHtml(line) + '\n'
  465. continue
  466. }
  467. if (line.match(/^### /)) {
  468. flushList()
  469. html += '<h3>' + this._parseInline(line.replace(/^### /, '')) + '</h3>'
  470. continue
  471. }
  472. if (line.match(/^## /)) {
  473. flushList()
  474. html += '<h2>' + this._parseInline(line.replace(/^## /, '')) + '</h2>'
  475. continue
  476. }
  477. if (line.match(/^# /)) {
  478. flushList()
  479. html += '<h1>' + this._parseInline(line.replace(/^# /, '')) + '</h1>'
  480. continue
  481. }
  482. if (line.match(/^\- /)) {
  483. inList = true
  484. listBuffer += '<li>' + this._parseInline(line.replace(/^\- /, '')) + '</li>\n'
  485. continue
  486. }
  487. if (line.trim() === '') {
  488. flushList()
  489. continue
  490. }
  491. flushList()
  492. html += '<p class="cl-item">' + this._parseInline(line) + '</p>\n'
  493. }
  494. flushList()
  495. this.changelogContent = html || '<p class="cl-empty">暂无变更日志</p>'
  496. this.changelogLoading = false
  497. }).bind(this)).catch((function() {
  498. this.changelogContent = '<p class="cl-empty">暂无变更日志</p>'
  499. this.changelogLoading = false
  500. }).bind(this))
  501. },
  502. _parseInline(text) {
  503. text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
  504. text = text.replace(/\*(.+?)\*/g, '<em>$1</em>')
  505. text = text.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>')
  506. text = text.replace(/`(.+?)`/g, '<code style="background:#F1F5F9;padding:2px 4px;border-radius:3px;font-size:13px">$1</code>')
  507. text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  508. return text
  509. },
  510. _escapeHtml(text) {
  511. return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  512. },
  513. async loadTrend() {
  514. this.trendLoading = true
  515. try {
  516. const res = await getTrendStats()
  517. if (res.code === 200 && res.data) {
  518. this.trendData = res.data
  519. this.$nextTick(() => this.initTrendChart())
  520. }
  521. } catch (e) {
  522. console.error('加载趋势数据失败:', e)
  523. } finally {
  524. this.trendLoading = false
  525. }
  526. },
  527. async loadMembership() {
  528. this.memberLoading = true
  529. try {
  530. const res = await getMembershipStats()
  531. if (res.code === 200 && res.data) {
  532. this.membershipData = res.data
  533. this.$nextTick(() => this.initMemberPieChart())
  534. }
  535. } catch (e) {
  536. console.error('加载会员数据失败:', e)
  537. } finally {
  538. this.memberLoading = false
  539. }
  540. },
  541. initTrendChart() {
  542. if (this.trendChart) this.trendChart.dispose()
  543. this.trendChart = echarts.init(this.$refs.trendChart)
  544. const familyData = Object.values(this.trendData.familyTrend || {})
  545. const userData = Object.values(this.trendData.userTrend || {})
  546. const xAxis = Object.keys(this.trendData.familyTrend || {})
  547. this.trendChart.setOption({
  548. tooltip: { trigger: 'axis' },
  549. legend: { data: ['新增家庭', '新增用户'] },
  550. grid: { left: 50, right: 20, bottom: 30, top: 40 },
  551. xAxis: { type: 'category', data: xAxis, axisLabel: { rotate: xAxis.length > 15 ? 45 : 0 } },
  552. yAxis: { type: 'value', minInterval: 1 },
  553. series: [
  554. { name: '新增家庭', type: 'bar', barWidth: '40%', data: familyData, itemStyle: { color: '#F97316', borderRadius: [4, 4, 0, 0] } },
  555. { name: '新增用户', type: 'line', smooth: true, data: userData, lineStyle: { color: '#0EA5E9', width: 2 }, itemStyle: { color: '#0EA5E9' } }
  556. ]
  557. })
  558. },
  559. initMemberPieChart() {
  560. if (this.memberChart) this.memberChart.dispose()
  561. this.memberChart = echarts.init(this.$refs.memberPieChart)
  562. this.memberChart.setOption({
  563. tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
  564. legend: { orient: 'vertical', left: 'left', top: 'center' },
  565. series: [{
  566. type: 'pie',
  567. radius: ['40%', '65%'],
  568. center: ['55%', '50%'],
  569. avoidLabelOverlap: true,
  570. label: { show: false },
  571. emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
  572. data: [
  573. { value: this.membershipData.freeCount || 0, name: '免费用户', itemStyle: { color: '#94A3B8' } },
  574. { value: this.membershipData.familyCount || 0, name: '家庭会员', itemStyle: { color: '#F97316' } },
  575. { value: this.membershipData.providerCount || 0, name: '服务商', itemStyle: { color: '#10B981' } }
  576. ]
  577. }]
  578. })
  579. }
  580. }
  581. }
  582. </script>
  583. <style scoped>
  584. .dashboard {
  585. padding: 0;
  586. }
  587. /* 欢迎横幅 */
  588. .welcome-banner {
  589. background: linear-gradient(135deg, #6366F1, #8B5CF6);
  590. border-radius: 16px;
  591. padding: 32px;
  592. display: flex;
  593. justify-content: space-between;
  594. align-items: center;
  595. margin-bottom: 24px;
  596. color: #FFFFFF;
  597. }
  598. .welcome-title {
  599. font-size: 28px;
  600. font-weight: 700;
  601. margin: 0 0 8px 0;
  602. }
  603. .welcome-desc {
  604. font-size: 16px;
  605. opacity: 0.9;
  606. margin: 0;
  607. }
  608. .welcome-illustration {
  609. font-size: 64px;
  610. }
  611. /* 统计卡片 */
  612. .stats-row {
  613. margin-bottom: 24px;
  614. }
  615. .stat-card {
  616. border-radius: 12px;
  617. border: none;
  618. }
  619. .stat-card ::v-deep .el-card__body {
  620. display: flex;
  621. align-items: center;
  622. padding: 24px;
  623. }
  624. .todo-stat-card {
  625. cursor: pointer;
  626. transition: transform 0.2s, box-shadow 0.2s;
  627. }
  628. .todo-stat-card:hover {
  629. transform: translateY(-2px);
  630. box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  631. }
  632. .stat-icon {
  633. width: 56px;
  634. height: 56px;
  635. border-radius: 12px;
  636. display: flex;
  637. align-items: center;
  638. justify-content: center;
  639. font-size: 24px;
  640. color: #fff;
  641. margin-right: 16px;
  642. flex-shrink: 0;
  643. }
  644. .stat-icon.blue { background: linear-gradient(135deg, #6366F1, #818CF8); }
  645. .stat-icon.green { background: linear-gradient(135deg, #10B981, #34D399); }
  646. .stat-icon.orange { background: linear-gradient(135deg, #F59E0B, #FBBF24); }
  647. .stat-icon.purple { background: linear-gradient(135deg, #8B5CF6, #A78BFA); }
  648. .stat-icon.todo-warning { background: linear-gradient(135deg, #F59E0B, #FBBF24); }
  649. .stat-icon.todo-heart { background: linear-gradient(135deg, #FF6B9D, #FF8FB3); }
  650. .stat-icon.todo-danger { background: linear-gradient(135deg, #EF4444, #F87171); }
  651. .stat-icon.todo-disabled { background: linear-gradient(135deg, #CBD5E1, #94A3B8); }
  652. .stat-value {
  653. font-size: 28px;
  654. font-weight: 700;
  655. color: #1E293B;
  656. line-height: 1.2;
  657. }
  658. .stat-label {
  659. color: #64748B;
  660. font-size: 14px;
  661. margin-top: 4px;
  662. }
  663. .stat-trend {
  664. font-size: 12px;
  665. color: #6366F1;
  666. margin-top: 8px;
  667. }
  668. .stat-trend.disabled {
  669. color: #94A3B8;
  670. }
  671. /* 内容卡片 */
  672. .content-row {
  673. margin-bottom: 24px;
  674. }
  675. .content-card {
  676. border-radius: 12px;
  677. border: none;
  678. margin-bottom: 20px;
  679. }
  680. .card-header {
  681. display: flex;
  682. justify-content: space-between;
  683. align-items: center;
  684. }
  685. .card-title {
  686. font-size: 16px;
  687. font-weight: 600;
  688. color: #1E293B;
  689. }
  690. /* 表格样式 */
  691. .custom-table ::v-deep .el-table__header th {
  692. background: #F8FAFC;
  693. color: #64748B;
  694. font-weight: 600;
  695. }
  696. .points-text {
  697. color: #F59E0B;
  698. font-weight: 600;
  699. }
  700. /* 系统健康面板 */
  701. .system-health-card {
  702. border-radius: 12px;
  703. border: none;
  704. }
  705. .health-grid {
  706. display: flex;
  707. gap: 16px;
  708. flex-wrap: wrap;
  709. }
  710. .health-item {
  711. display: flex;
  712. align-items: center;
  713. gap: 12px;
  714. flex: 1;
  715. min-width: 140px;
  716. }
  717. .health-icon {
  718. width: 40px;
  719. height: 40px;
  720. border-radius: 10px;
  721. display: flex;
  722. align-items: center;
  723. justify-content: center;
  724. font-size: 18px;
  725. color: white;
  726. }
  727. .health-icon--status {
  728. background: linear-gradient(135deg, #10B981, #059669);
  729. }
  730. .health-icon--version {
  731. background: linear-gradient(135deg, #6366F1, #4F46E5);
  732. }
  733. .health-icon--time {
  734. background: linear-gradient(135deg, #F59E0B, #D97706);
  735. }
  736. .health-icon--api {
  737. background: linear-gradient(135deg, #0EA5E9, #0284C7);
  738. }
  739. .health-icon--uptime {
  740. background: linear-gradient(135deg, #8B5CF6, #7C3AED);
  741. }
  742. .health-icon--cpu {
  743. background: linear-gradient(135deg, #EF4444, #F87171);
  744. }
  745. .health-icon--memory {
  746. background: linear-gradient(135deg, #10B981, #059669);
  747. }
  748. .health-info {
  749. flex: 1;
  750. }
  751. .health-label {
  752. font-size: 12px;
  753. color: #9ca3af;
  754. margin-bottom: 2px;
  755. }
  756. .health-value {
  757. font-size: 14px;
  758. font-weight: 600;
  759. color: #1f2937;
  760. }
  761. .health-value--online {
  762. color: #10B981;
  763. }
  764. .health-links {
  765. margin-top: 16px;
  766. padding-top: 16px;
  767. border-top: 1px solid #f3f4f6;
  768. display: flex;
  769. gap: 8px;
  770. }
  771. /* 快捷操作 */
  772. .quick-card .el-card__body {
  773. padding: 12px;
  774. }
  775. .quick-actions {
  776. display: flex;
  777. justify-content: space-around;
  778. }
  779. .quick-item {
  780. display: flex;
  781. flex-direction: column;
  782. align-items: center;
  783. cursor: pointer;
  784. padding: 12px;
  785. border-radius: 8px;
  786. transition: background 0.2s;
  787. }
  788. .quick-item:hover {
  789. background: #F8FAFC;
  790. }
  791. .quick-item i {
  792. font-size: 24px;
  793. color: #6366F1;
  794. margin-bottom: 8px;
  795. }
  796. .quick-item span {
  797. font-size: 12px;
  798. color: #64748B;
  799. }
  800. /* 快捷操作 4x2 网格 */
  801. .quick-actions-section {
  802. margin-bottom: 24px;
  803. }
  804. .section-header {
  805. margin-bottom: 12px;
  806. }
  807. .section-title {
  808. font-size: 16px;
  809. font-weight: 600;
  810. color: #1f2937;
  811. }
  812. .quick-actions-grid {
  813. display: grid;
  814. grid-template-columns: repeat(4, 1fr);
  815. gap: 12px;
  816. }
  817. .action-card {
  818. background: white;
  819. border-radius: 12px;
  820. padding: 20px 16px;
  821. text-align: center;
  822. cursor: pointer;
  823. transition: all 0.2s ease;
  824. border: 1px solid #f3f4f6;
  825. }
  826. .action-card:hover {
  827. transform: translateY(-2px);
  828. box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  829. border-color: #e5e7eb;
  830. }
  831. .action-icon {
  832. width: 48px;
  833. height: 48px;
  834. border-radius: 12px;
  835. display: flex;
  836. align-items: center;
  837. justify-content: center;
  838. margin: 0 auto 8px;
  839. font-size: 22px;
  840. color: white;
  841. }
  842. .action-icon--primary {
  843. background: linear-gradient(135deg, #FF8C42, #FF6B35);
  844. }
  845. .action-icon--family {
  846. background: linear-gradient(135deg, #6366F1, #4F46E5);
  847. }
  848. .action-icon--guide {
  849. background: linear-gradient(135deg, #F59E0B, #D97706);
  850. }
  851. .action-icon--package {
  852. background: linear-gradient(135deg, #10B981, #059669);
  853. }
  854. .action-icon--reward {
  855. background: linear-gradient(135deg, #FF6B9D, #E11D48);
  856. }
  857. .action-icon--user {
  858. background: linear-gradient(135deg, #0EA5E9, #0284C7);
  859. }
  860. .action-icon--audit {
  861. background: linear-gradient(135deg, #8B5CF6, #7C3AED);
  862. }
  863. .action-icon--settings {
  864. background: linear-gradient(135deg, #6B7280, #4B5563);
  865. }
  866. .action-label {
  867. font-size: 13px;
  868. color: #374151;
  869. font-weight: 500;
  870. }
  871. /* 图表区域 */
  872. .chart-row {
  873. margin-bottom: 24px;
  874. }
  875. .chart-card {
  876. border-radius: 12px;
  877. border: none;
  878. min-height: 320px;
  879. }
  880. .echarts-container {
  881. height: 260px;
  882. width: 100%;
  883. }
  884. </style>
  885. <style>
  886. .changelog-dialog .el-dialog__body {
  887. max-height: 700px;
  888. overflow-y: auto;
  889. }
  890. .changelog-body {
  891. padding: 8px 4px;
  892. line-height: 1.8;
  893. font-size: 14px;
  894. color: #374151;
  895. }
  896. .changelog-body h1 {
  897. font-size: 20px;
  898. font-weight: 700;
  899. margin: 16px 0 8px;
  900. color: #1E293B;
  901. }
  902. .changelog-body h2 {
  903. font-size: 17px;
  904. font-weight: 600;
  905. margin: 14px 0 6px;
  906. color: #1E293B;
  907. border-left: 3px solid #6366F1;
  908. padding-left: 8px;
  909. }
  910. .changelog-body h3 {
  911. font-size: 15px;
  912. font-weight: 600;
  913. margin: 10px 0 4px;
  914. color: #374151;
  915. }
  916. .changelog-body ul {
  917. margin: 4px 0 8px;
  918. padding-left: 20px;
  919. }
  920. .changelog-body li {
  921. margin: 2px 0;
  922. }
  923. .changelog-body .cl-item {
  924. margin: 4px 0;
  925. }
  926. .changelog-body .cl-empty {
  927. text-align: center;
  928. color: #94A3B8;
  929. padding: 32px 0;
  930. }
  931. .changelog-body code {
  932. background: #F1F5F9;
  933. padding: 2px 6px;
  934. border-radius: 3px;
  935. font-size: 13px;
  936. }
  937. .changelog-body pre {
  938. background: #F8FAFC;
  939. border: 1px solid #E2E8F0;
  940. border-radius: 8px;
  941. padding: 12px;
  942. overflow-x: auto;
  943. }
  944. .changelog-body pre code {
  945. background: none;
  946. padding: 0;
  947. }
  948. .changelog-body a {
  949. color: #6366F1;
  950. text-decoration: none;
  951. }
  952. .changelog-body a:hover {
  953. text-decoration: underline;
  954. }
  955. </style>