feat(health): 新增依从性统计前端页面
- 新增 medicationAdherence.js:统计API接口 - 新增 medicationStatistic/index.vue:统计页面 - 概览卡片:总任务、已服药、漏服、服药率、准时率 - 趋势图:服药率和准时率折线图 - 时段分布:饼图展示 - 日历视图:月度服药日历
This commit is contained in:
73
src/api/health/medicationAdherence.js
Normal file
73
src/api/health/medicationAdherence.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取总体依从性统计
|
||||||
|
export function getOverallAdherence(query) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/overall',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每日依从性统计列表
|
||||||
|
export function getDailyAdherence(query) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/daily',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日历视图数据
|
||||||
|
export function getCalendarData(personId, yearMonth) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/calendar',
|
||||||
|
method: 'get',
|
||||||
|
params: { personId, yearMonth }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取各时段服药统计
|
||||||
|
export function getTimeSlotAdherence(query) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/timeSlot',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取各药品依从性统计
|
||||||
|
export function getMedicineAdherence(query) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/medicine',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取各人员依从性统计
|
||||||
|
export function getPersonAdherence(query) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/person',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取漏服原因统计
|
||||||
|
export function getMissedReasonStats(query) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/missedReason',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取仪表盘概览数据
|
||||||
|
export function getDashboard(personId) {
|
||||||
|
return request({
|
||||||
|
url: '/health/adherence/dashboard',
|
||||||
|
method: 'get',
|
||||||
|
params: { personId }
|
||||||
|
})
|
||||||
|
}
|
||||||
547
src/views/health/medicationStatistic/index.vue
Normal file
547
src/views/health/medicationStatistic/index.vue
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<div class="search-con">
|
||||||
|
<div class="title">查询条件</div>
|
||||||
|
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="80px">
|
||||||
|
<el-form-item label="人员" prop="personId">
|
||||||
|
<el-select v-model="queryParams.personId" placeholder="请选择人员" clearable filterable>
|
||||||
|
<el-option v-for="item in personList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="统计周期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="search-btn-con">
|
||||||
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button type="info" icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 概览卡片 -->
|
||||||
|
<div class="overview-cards" v-loading="loading">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #409EFF;">
|
||||||
|
<el-icon><Calendar /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ overview.totalTasks || 0 }}</div>
|
||||||
|
<div class="stat-label">总任务数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #67C23A;">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ overview.completedTasks || 0 }}</div>
|
||||||
|
<div class="stat-label">已服药</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #F56C6C;">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ overview.missedTasks || 0 }}</div>
|
||||||
|
<div class="stat-label">漏服</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #E6A23C;">
|
||||||
|
<el-icon><Timer /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ overview.adherenceRate || 0 }}%</div>
|
||||||
|
<div class="stat-label">服药率</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #909399;">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ overview.ontimeRate || 0 }}%</div>
|
||||||
|
<div class="stat-label">准时率</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="charts-row">
|
||||||
|
<!-- 趋势图 -->
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title">服药趋势(最近7天)</div>
|
||||||
|
<div ref="trendChartRef" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
<!-- 时段统计 -->
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title">时段分布</div>
|
||||||
|
<div ref="timeSlotChartRef" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日历视图 -->
|
||||||
|
<div class="calendar-section">
|
||||||
|
<div class="calendar-header">
|
||||||
|
<span class="calendar-title">日历视图</span>
|
||||||
|
<div class="calendar-nav">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button @click="changeMonth(-1)" icon="ArrowLeft">上月</el-button>
|
||||||
|
<el-button>{{ currentMonth }}</el-button>
|
||||||
|
<el-button @click="changeMonth(1)">下月<el-icon class="el-icon--right"><ArrowRight /></el-icon></el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-grid" v-loading="calendarLoading">
|
||||||
|
<div class="calendar-weekdays">
|
||||||
|
<div v-for="day in weekdays" :key="day" class="weekday">{{ day }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-days">
|
||||||
|
<div
|
||||||
|
v-for="(day, index) in calendarDays"
|
||||||
|
:key="index"
|
||||||
|
class="calendar-day"
|
||||||
|
:class="{ 'other-month': day.otherMonth, 'today': day.isToday }"
|
||||||
|
>
|
||||||
|
<div class="day-number">{{ day.day }}</div>
|
||||||
|
<div class="day-stats" v-if="day.data && day.data.totalTasks > 0">
|
||||||
|
<span class="stat-dot completed" :title="`已服药: ${day.data.completedTasks}`"></span>
|
||||||
|
<span class="stat-dot missed" v-if="day.data.missedTasks > 0" :title="`漏服: ${day.data.missedTasks}`"></span>
|
||||||
|
<span class="stat-rate">{{ day.data.adherenceRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="MedicationStatistic">
|
||||||
|
import { ref, reactive, onMounted, watch, computed, nextTick } from 'vue'
|
||||||
|
import { getOverallAdherence, getDailyAdherence, getCalendarData, getTimeSlotAdherence } from '@/api/health/medicationAdherence'
|
||||||
|
import { listPerson } from '@/api/health/person'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const calendarLoading = ref(false)
|
||||||
|
const personList = ref([])
|
||||||
|
const queryParams = reactive({
|
||||||
|
personId: null
|
||||||
|
})
|
||||||
|
const dateRange = ref([])
|
||||||
|
const overview = ref({})
|
||||||
|
|
||||||
|
// 日历相关
|
||||||
|
const currentMonth = ref('')
|
||||||
|
const currentYearMonth = ref('')
|
||||||
|
const calendarData = ref([])
|
||||||
|
const weekdays = ['一', '二', '三', '四', '五', '六', '日']
|
||||||
|
|
||||||
|
// 图表
|
||||||
|
const trendChartRef = ref(null)
|
||||||
|
const timeSlotChartRef = ref(null)
|
||||||
|
let trendChart = null
|
||||||
|
let timeSlotChart = null
|
||||||
|
|
||||||
|
// 计算日历天数
|
||||||
|
const calendarDays = computed(() => {
|
||||||
|
const days = []
|
||||||
|
if (!currentYearMonth.value) return days
|
||||||
|
|
||||||
|
const [year, month] = currentYearMonth.value.split('-').map(Number)
|
||||||
|
const firstDay = new Date(year, month - 1, 1)
|
||||||
|
const lastDay = new Date(year, month, 0)
|
||||||
|
const startWeekday = (firstDay.getDay() + 6) % 7 // 周一为0
|
||||||
|
|
||||||
|
// 上月填充
|
||||||
|
const prevMonthLastDay = new Date(year, month - 1, 0).getDate()
|
||||||
|
for (let i = startWeekday - 1; i >= 0; i--) {
|
||||||
|
days.push({
|
||||||
|
day: prevMonthLastDay - i,
|
||||||
|
otherMonth: true,
|
||||||
|
isToday: false,
|
||||||
|
data: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当月
|
||||||
|
const today = new Date()
|
||||||
|
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
||||||
|
|
||||||
|
const dataMap = {}
|
||||||
|
calendarData.value.forEach(d => {
|
||||||
|
dataMap[d.date] = d
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 1; i <= lastDay.getDate(); i++) {
|
||||||
|
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`
|
||||||
|
days.push({
|
||||||
|
day: i,
|
||||||
|
otherMonth: false,
|
||||||
|
isToday: dateStr === todayStr,
|
||||||
|
data: dataMap[dateStr] || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下月填充
|
||||||
|
const remaining = 42 - days.length
|
||||||
|
for (let i = 1; i <= remaining; i++) {
|
||||||
|
days.push({
|
||||||
|
day: i,
|
||||||
|
otherMonth: true,
|
||||||
|
isToday: false,
|
||||||
|
data: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return days
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取人员列表
|
||||||
|
const getPersonList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await listPerson({})
|
||||||
|
personList.value = res.rows || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询统计数据
|
||||||
|
const getStatistics = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = { ...queryParams }
|
||||||
|
if (dateRange.value && dateRange.value.length === 2) {
|
||||||
|
params.startDate = dateRange.value[0]
|
||||||
|
params.endDate = dateRange.value[1]
|
||||||
|
} else {
|
||||||
|
// 默认最近7天
|
||||||
|
const today = new Date()
|
||||||
|
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
|
params.startDate = weekAgo.toISOString().split('T')[0]
|
||||||
|
params.endDate = today.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总体统计
|
||||||
|
const overviewRes = await getOverallAdherence(params)
|
||||||
|
overview.value = overviewRes.data || {}
|
||||||
|
|
||||||
|
// 获取每日趋势
|
||||||
|
const dailyRes = await getDailyAdherence(params)
|
||||||
|
renderTrendChart(dailyRes.data || [])
|
||||||
|
|
||||||
|
// 获取时段统计
|
||||||
|
const timeSlotRes = await getTimeSlotAdherence(params)
|
||||||
|
renderTimeSlotChart(timeSlotRes.data || {})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染趋势图
|
||||||
|
const renderTrendChart = (data) => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (!trendChartRef.value) return
|
||||||
|
if (!trendChart) {
|
||||||
|
trendChart = echarts.init(trendChartRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['服药率', '准时率']
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(d => d.date?.substring(5) || '')
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
max: 100
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '服药率',
|
||||||
|
type: 'line',
|
||||||
|
data: data.map(d => d.adherenceRate || 0),
|
||||||
|
smooth: true,
|
||||||
|
itemStyle: { color: '#409EFF' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '准时率',
|
||||||
|
type: 'line',
|
||||||
|
data: data.map(d => d.ontimeRate || 0),
|
||||||
|
smooth: true,
|
||||||
|
itemStyle: { color: '#67C23A' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
trendChart.setOption(option)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染时段统计图
|
||||||
|
const renderTimeSlotChart = (data) => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (!timeSlotChartRef.value) return
|
||||||
|
if (!timeSlotChart) {
|
||||||
|
timeSlotChart = echarts.init(timeSlotChartRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotNames = {
|
||||||
|
morning: '上午(6-12)',
|
||||||
|
afternoon: '下午(12-18)',
|
||||||
|
evening: '晚间(18-24)',
|
||||||
|
night: '凌晨(0-6)'
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{b}: {c}%'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
data: Object.entries(data).map(([key, value]) => ({
|
||||||
|
name: slotNames[key] || key,
|
||||||
|
value: value.adherenceRate || 0
|
||||||
|
})),
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
timeSlotChart.setOption(option)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日历数据
|
||||||
|
const getCalendar = async () => {
|
||||||
|
calendarLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getCalendarData(queryParams.personId, currentYearMonth.value)
|
||||||
|
calendarData.value = res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
calendarLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换月份
|
||||||
|
const changeMonth = (delta) => {
|
||||||
|
const [year, month] = currentYearMonth.value.split('-').map(Number)
|
||||||
|
const newDate = new Date(year, month - 1 + delta, 1)
|
||||||
|
currentYearMonth.value = `${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}`
|
||||||
|
currentMonth.value = `${newDate.getFullYear()}年${newDate.getMonth() + 1}月`
|
||||||
|
getCalendar()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuery = () => {
|
||||||
|
getStatistics()
|
||||||
|
getCalendar()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.personId = null
|
||||||
|
dateRange.value = []
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化日期
|
||||||
|
const today = new Date()
|
||||||
|
currentYearMonth.value = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`
|
||||||
|
currentMonth.value = `${today.getFullYear()}年${today.getMonth() + 1}月`
|
||||||
|
|
||||||
|
getPersonList()
|
||||||
|
getStatistics()
|
||||||
|
getCalendar()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.overview-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 180px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
flex: 1;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.calendar-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
.calendar-weekdays {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.weekday {
|
||||||
|
padding: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-days {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
min-height: 80px;
|
||||||
|
border: 1px solid #EBEEF5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
&.other-month {
|
||||||
|
background: #F5F7FA;
|
||||||
|
.day-number { color: #C0C4CC; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.today {
|
||||||
|
border-color: #409EFF;
|
||||||
|
.day-number {
|
||||||
|
color: #409EFF;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-stats {
|
||||||
|
margin-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.stat-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.completed { background: #67C23A; }
|
||||||
|
&.missed { background: #F56C6C; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-rate {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user