|
- {% extends "./layout.html" %}
-
- {% block title %}控制台{% endblock %}
-
- {% block css %}
- <style>
- .stat-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
- .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; }
- .stat-card .card-icon { width: 52px; height: 52px; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
- .stat-card .card-icon svg { width: 26px; height: 26px; }
- .stat-card .card-info { flex: 1; }
- .stat-card .label { font-size: 13px; color: #909399; margin-bottom: 6px; }
- .stat-card .value { font-size: 28px; font-weight: 700; line-height: 1.2; }
- .stat-card .trend { font-size: 12px; color: #909399; margin-top: 6px; }
- .stat-card .trend .num { font-weight: 600; }
- .stat-card.card-total .card-icon { background: rgba(255,120,0,0.1); }
- .stat-card.card-total .value { color: #ff7800; }
- .stat-card.card-total .trend .num { color: #ff7800; }
- .stat-card.card-pending .card-icon { background: rgba(230,162,60,0.1); }
- .stat-card.card-pending .value { color: #E6A23C; }
- .stat-card.card-pending .trend .num { color: #E6A23C; }
- .stat-card.card-rejected .card-icon { background: rgba(245,108,108,0.1); }
- .stat-card.card-rejected .value { color: #F56C6C; }
- .stat-card.card-rejected .trend .num { color: #F56C6C; }
- .stat-card.card-approved .card-icon { background: rgba(103,194,58,0.1); }
- .stat-card.card-approved .value { color: #67C23A; }
- .stat-card.card-approved .trend .num { color: #67C23A; }
- .panel { background: #fff; border-radius: 10px; padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 24px; }
- .panel h3 { font-size: 16px; color: #303133; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #EBEEF5; }
- </style>
- {% endblock %}
-
- {% block content %}
- <div id="dashboardApp" v-cloak>
- <!-- 统计卡片 -->
- <div class="stat-cards" v-loading="loading">
- <div class="stat-card card-total">
- <div class="card-icon">
- <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>
- </div>
- <div class="card-info">
- <div class="label">患者总数</div>
- <div class="value">${ counts.all.toLocaleString() }</div>
- <div class="trend">今日新增 <span class="num">+${ todayCounts.all }</span></div>
- </div>
- </div>
- <div class="stat-card card-pending">
- <div class="card-icon">
- <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>
- </div>
- <div class="card-info">
- <div class="label">待审核</div>
- <div class="value">${ counts.pending.toLocaleString() }</div>
- <div class="trend">今日新增 <span class="num">+${ todayCounts.pending }</span></div>
- </div>
- </div>
- <div class="stat-card card-rejected">
- <div class="card-icon">
- <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>
- </div>
- <div class="card-info">
- <div class="label">已驳回</div>
- <div class="value">${ counts.rejected.toLocaleString() }</div>
- <div class="trend">今日新增 <span class="num">+${ todayCounts.rejected }</span></div>
- </div>
- </div>
- <div class="stat-card card-approved">
- <div class="card-icon">
- <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>
- </div>
- <div class="card-info">
- <div class="label">审核通过</div>
- <div class="value">${ counts.approved.toLocaleString() }</div>
- <div class="trend">今日新增 <span class="num">+${ todayCounts.approved }</span></div>
- </div>
- </div>
- </div>
-
- <!-- 趋势折线图 -->
- <div class="panel">
- <h3>患者新增趋势(近30天)</h3>
- <div id="trendChart" style="width:100%;height:360px;"></div>
- </div>
-
- <!-- 最近提交记录 -->
- <div class="panel">
- <h3>最近提交记录</h3>
- <el-table :data="recentList" stripe border>
- <el-table-column prop="patient_no" label="编号" min-width="180"></el-table-column>
- <el-table-column prop="name" label="姓名" min-width="80"></el-table-column>
- <el-table-column label="手机号" min-width="120">
- <template #default="{ row }">${ row.phone_mask }</template>
- </el-table-column>
- <el-table-column label="标识" min-width="80">
- <template #default="{ row }">
- <el-tag v-if="row.tag" type="danger" size="small">${ row.tag }</el-tag>
- <span v-else style="color:#999;">—</span>
- </template>
- </el-table-column>
- <el-table-column label="提交时间" min-width="160">
- <template #default="{ row }">${ row.create_time }</template>
- </el-table-column>
- <el-table-column label="状态" min-width="90" align="center">
- <template #default="{ row }">
- <el-tag v-if="row.status === -1" type="info" size="small">待提交</el-tag>
- <el-tag v-else-if="row.status === 0" type="warning" size="small">待审核</el-tag>
- <el-tag v-else-if="row.status === 1" type="success" size="small">审核通过</el-tag>
- <el-tag v-else-if="row.status === 2" type="danger" size="small">已驳回</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" min-width="100" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="viewDetail(row)">查看详情</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- {% endblock %}
-
- {% block js %}
- <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
- <script>
- var { createApp, ref, reactive, onMounted, nextTick } = Vue;
-
- var app = createApp({
- delimiters: ['${', '}'],
- setup() {
- var loading = ref(true);
- var counts = reactive({ all: 0, pending: 0, approved: 0, rejected: 0 });
- var todayCounts = reactive({ all: 0, pending: 0, approved: 0, rejected: 0 });
- var recentList = ref([]);
- var trendData = ref([]);
- var canView = {{ canView | dump | safe }};
-
- async function loadStats() {
- loading.value = true;
- try {
- var res = await fetch('/admin/dashboard/stats').then(function(r) { return r.json(); });
- if (res.code === 0) {
- Object.assign(counts, res.data.counts);
- Object.assign(todayCounts, res.data.todayCounts || {});
- recentList.value = res.data.recent || [];
- trendData.value = res.data.trend || [];
- nextTick(function() { renderChart(); });
- }
- } finally {
- loading.value = false;
- }
- }
-
- function renderChart() {
- var dom = document.getElementById('trendChart');
- if (!dom || !window.echarts) return;
- var chart = echarts.init(dom);
-
- // 生成完整30天日期序列
- var dataMap = {};
- trendData.value.forEach(function(r) { dataMap[r.date] = r; });
- var dates = [], totals = [], rectals = [];
- for (var i = 29; i >= 0; i--) {
- var d = new Date();
- d.setDate(d.getDate() - i);
- var key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
- dates.push(key);
- var row = dataMap[key];
- totals.push(row ? row.total : 0);
- rectals.push(row ? row.rectal : 0);
- }
-
- chart.setOption({
- tooltip: { trigger: 'axis' },
- legend: { data: ['患者总数', '直肠癌患者'], top: 0 },
- grid: { left: 40, right: 24, top: 40, bottom: 30 },
- xAxis: { type: 'category', data: dates, boundaryGap: false },
- yAxis: { type: 'value', minInterval: 1 },
- series: [
- {
- name: '患者总数',
- type: 'line',
- data: totals,
- smooth: true,
- itemStyle: { color: '#ff7800' },
- 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)' }] } }
- },
- {
- name: '直肠癌患者',
- type: 'line',
- data: rectals,
- smooth: true,
- itemStyle: { color: '#409EFF' },
- 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)' }] } }
- }
- ]
- });
- window.addEventListener('resize', function() { chart.resize(); });
- }
-
- function viewDetail(row) {
- if (!canView) {
- ElementPlus.ElMessageBox.alert('暂无查看详情权限,请先联系管理员授权', '温馨提示', {
- confirmButtonText: '知道了',
- type: 'warning'
- });
- return;
- }
- window.location.href = '/admin/patient/detail.html?id=' + row.id;
- }
-
- onMounted(function() { loadStats(); });
-
- return { loading, counts, todayCounts, recentList, viewDetail };
- }
- });
-
- app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
- app.mount('#dashboardApp');
- </script>
- {% endblock %}
|