Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

220 lignes
9.9 KiB

  1. {% extends "./layout.html" %}
  2. {% block title %}控制台{% endblock %}
  3. {% block css %}
  4. <style>
  5. .stat-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
  6. .stat-card { background: #fff; border-radius: 10px; padding: 20px 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); display: flex; align-items: center; gap: 16px; }
  7. .stat-card .card-icon { width: 52px; height: 52px; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  8. .stat-card .card-icon svg { width: 26px; height: 26px; }
  9. .stat-card .card-info { flex: 1; }
  10. .stat-card .label { font-size: 13px; color: #909399; margin-bottom: 6px; }
  11. .stat-card .value { font-size: 28px; font-weight: 700; line-height: 1.2; }
  12. .stat-card .trend { font-size: 12px; color: #909399; margin-top: 6px; }
  13. .stat-card .trend .num { font-weight: 600; }
  14. .stat-card.card-total .card-icon { background: rgba(255,120,0,0.1); }
  15. .stat-card.card-total .value { color: #ff7800; }
  16. .stat-card.card-total .trend .num { color: #ff7800; }
  17. .stat-card.card-pending .card-icon { background: rgba(230,162,60,0.1); }
  18. .stat-card.card-pending .value { color: #E6A23C; }
  19. .stat-card.card-pending .trend .num { color: #E6A23C; }
  20. .stat-card.card-rejected .card-icon { background: rgba(245,108,108,0.1); }
  21. .stat-card.card-rejected .value { color: #F56C6C; }
  22. .stat-card.card-rejected .trend .num { color: #F56C6C; }
  23. .stat-card.card-approved .card-icon { background: rgba(103,194,58,0.1); }
  24. .stat-card.card-approved .value { color: #67C23A; }
  25. .stat-card.card-approved .trend .num { color: #67C23A; }
  26. .panel { background: #fff; border-radius: 10px; padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 24px; }
  27. .panel h3 { font-size: 16px; color: #303133; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #EBEEF5; }
  28. </style>
  29. {% endblock %}
  30. {% block content %}
  31. <div id="dashboardApp" v-cloak>
  32. <!-- 统计卡片 -->
  33. <div class="stat-cards" v-loading="loading">
  34. <div class="stat-card card-total">
  35. <div class="card-icon">
  36. <svg viewBox="0 0 1024 1024" fill="#ff7800"><path d="M563.2 462.4a192 192 0 1 0-102.4 0A320 320 0 0 0 192 768a32 32 0 0 0 32 32h576a32 32 0 0 0 32-32 320 320 0 0 0-268.8-305.6zM352 320a160 160 0 1 1 320 0 160 160 0 0 1-320 0zm-128 448a288 288 0 0 1 576 0H224z"/></svg>
  37. </div>
  38. <div class="card-info">
  39. <div class="label">患者总数</div>
  40. <div class="value">${ counts.all.toLocaleString() }</div>
  41. <div class="trend">今日新增 <span class="num">+${ todayCounts.all }</span></div>
  42. </div>
  43. </div>
  44. <div class="stat-card card-pending">
  45. <div class="card-icon">
  46. <svg viewBox="0 0 1024 1024" fill="#E6A23C"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm-32-588v272l208 124.8 32-53.6-176-104.8V296h-64z"/></svg>
  47. </div>
  48. <div class="card-info">
  49. <div class="label">待审核</div>
  50. <div class="value">${ counts.pending.toLocaleString() }</div>
  51. <div class="trend">今日新增 <span class="num">+${ todayCounts.pending }</span></div>
  52. </div>
  53. </div>
  54. <div class="stat-card card-rejected">
  55. <div class="card-icon">
  56. <svg viewBox="0 0 1024 1024" fill="#F56C6C"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm158.4-489.6L557.6 512l112.8 117.6-45.2 45.2L512 557.6l-117.6 112.8-45.2-45.2L466.4 512 349.2 394.4l45.2-45.2L512 466.4l117.6-112.8z"/></svg>
  57. </div>
  58. <div class="card-info">
  59. <div class="label">已驳回</div>
  60. <div class="value">${ counts.rejected.toLocaleString() }</div>
  61. <div class="trend">今日新增 <span class="num">+${ todayCounts.rejected }</span></div>
  62. </div>
  63. </div>
  64. <div class="stat-card card-approved">
  65. <div class="card-icon">
  66. <svg viewBox="0 0 1024 1024" fill="#67C23A"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm193.6-505.6L448 636.8l-129.6-129.6-45.2 45.2L448 727.2l302.8-302.8z"/></svg>
  67. </div>
  68. <div class="card-info">
  69. <div class="label">审核通过</div>
  70. <div class="value">${ counts.approved.toLocaleString() }</div>
  71. <div class="trend">今日新增 <span class="num">+${ todayCounts.approved }</span></div>
  72. </div>
  73. </div>
  74. </div>
  75. <!-- 趋势折线图 -->
  76. <div class="panel">
  77. <h3>患者新增趋势(近30天)</h3>
  78. <div id="trendChart" style="width:100%;height:360px;"></div>
  79. </div>
  80. <!-- 最近提交记录 -->
  81. <div class="panel">
  82. <h3>最近提交记录</h3>
  83. <el-table :data="recentList" stripe border>
  84. <el-table-column prop="patient_no" label="编号" min-width="180"></el-table-column>
  85. <el-table-column prop="name" label="姓名" min-width="80"></el-table-column>
  86. <el-table-column label="手机号" min-width="120">
  87. <template #default="{ row }">${ row.phone_mask }</template>
  88. </el-table-column>
  89. <el-table-column label="标识" min-width="80">
  90. <template #default="{ row }">
  91. <el-tag v-if="row.tag" type="danger" size="small">${ row.tag }</el-tag>
  92. <span v-else style="color:#999;">—</span>
  93. </template>
  94. </el-table-column>
  95. <el-table-column label="提交时间" min-width="160">
  96. <template #default="{ row }">${ row.create_time }</template>
  97. </el-table-column>
  98. <el-table-column label="状态" min-width="90" align="center">
  99. <template #default="{ row }">
  100. <el-tag v-if="row.status === -1" type="info" size="small">待提交</el-tag>
  101. <el-tag v-else-if="row.status === 0" type="warning" size="small">待审核</el-tag>
  102. <el-tag v-else-if="row.status === 1" type="success" size="small">审核通过</el-tag>
  103. <el-tag v-else-if="row.status === 2" type="danger" size="small">已驳回</el-tag>
  104. </template>
  105. </el-table-column>
  106. <el-table-column label="操作" min-width="100" align="center">
  107. <template #default="{ row }">
  108. <el-button type="primary" link @click="viewDetail(row)">查看详情</el-button>
  109. </template>
  110. </el-table-column>
  111. </el-table>
  112. </div>
  113. </div>
  114. {% endblock %}
  115. {% block js %}
  116. <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
  117. <script>
  118. var { createApp, ref, reactive, onMounted, nextTick } = Vue;
  119. var app = createApp({
  120. delimiters: ['${', '}'],
  121. setup() {
  122. var loading = ref(true);
  123. var counts = reactive({ all: 0, pending: 0, approved: 0, rejected: 0 });
  124. var todayCounts = reactive({ all: 0, pending: 0, approved: 0, rejected: 0 });
  125. var recentList = ref([]);
  126. var trendData = ref([]);
  127. var canView = {{ canView | dump | safe }};
  128. async function loadStats() {
  129. loading.value = true;
  130. try {
  131. var res = await fetch('/admin/dashboard/stats').then(function(r) { return r.json(); });
  132. if (res.code === 0) {
  133. Object.assign(counts, res.data.counts);
  134. Object.assign(todayCounts, res.data.todayCounts || {});
  135. recentList.value = res.data.recent || [];
  136. trendData.value = res.data.trend || [];
  137. nextTick(function() { renderChart(); });
  138. }
  139. } finally {
  140. loading.value = false;
  141. }
  142. }
  143. function renderChart() {
  144. var dom = document.getElementById('trendChart');
  145. if (!dom || !window.echarts) return;
  146. var chart = echarts.init(dom);
  147. // 生成完整30天日期序列
  148. var dataMap = {};
  149. trendData.value.forEach(function(r) { dataMap[r.date] = r; });
  150. var dates = [], totals = [], rectals = [];
  151. for (var i = 29; i >= 0; i--) {
  152. var d = new Date();
  153. d.setDate(d.getDate() - i);
  154. var key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
  155. dates.push(key);
  156. var row = dataMap[key];
  157. totals.push(row ? row.total : 0);
  158. rectals.push(row ? row.rectal : 0);
  159. }
  160. chart.setOption({
  161. tooltip: { trigger: 'axis' },
  162. legend: { data: ['患者总数', '直肠癌患者'], top: 0 },
  163. grid: { left: 40, right: 24, top: 40, bottom: 30 },
  164. xAxis: { type: 'category', data: dates, boundaryGap: false },
  165. yAxis: { type: 'value', minInterval: 1 },
  166. series: [
  167. {
  168. name: '患者总数',
  169. type: 'line',
  170. data: totals,
  171. smooth: true,
  172. itemStyle: { color: '#ff7800' },
  173. areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(255,120,0,0.25)' }, { offset: 1, color: 'rgba(255,120,0,0.02)' }] } }
  174. },
  175. {
  176. name: '直肠癌患者',
  177. type: 'line',
  178. data: rectals,
  179. smooth: true,
  180. itemStyle: { color: '#409EFF' },
  181. areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(64,158,255,0.2)' }, { offset: 1, color: 'rgba(64,158,255,0.02)' }] } }
  182. }
  183. ]
  184. });
  185. window.addEventListener('resize', function() { chart.resize(); });
  186. }
  187. function viewDetail(row) {
  188. if (!canView) {
  189. ElementPlus.ElMessageBox.alert('暂无查看详情权限,请先联系管理员授权', '温馨提示', {
  190. confirmButtonText: '知道了',
  191. type: 'warning'
  192. });
  193. return;
  194. }
  195. window.location.href = '/admin/patient/detail.html?id=' + row.id;
  196. }
  197. onMounted(function() { loadStats(); });
  198. return { loading, counts, todayCounts, recentList, viewDetail };
  199. }
  200. });
  201. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  202. app.mount('#dashboardApp');
  203. </script>
  204. {% endblock %}