899 lines
28 KiB
Vue
899 lines
28 KiB
Vue
<template>
|
||
<view class="dashboard-page" :style="safeAreaStyle">
|
||
<!-- 左侧面板:用户信息 -->
|
||
<view class="left-panel">
|
||
<view class="brand">
|
||
<view class="brand-title" data-content="学小乐AI">学小乐AI</view>
|
||
<view class="brand-sub">教师端</view>
|
||
</view>
|
||
|
||
<view class="user-card" v-if="isLoggedIn">
|
||
<image class="user-avatar" :src="teacherInfo.avatarUrl || defaultAvatar" mode="aspectFill" />
|
||
<text class="user-name">{{ teacherInfo.nickName || teacherInfo.account || '老师' }}</text>
|
||
<text class="user-school">{{ teacherInfo.schoolName || '' }}</text>
|
||
</view>
|
||
<view class="user-card login-card" v-else @click="goLogin">
|
||
<image class="user-avatar" :src="defaultAvatar" mode="aspectFill" />
|
||
<text class="user-name">点击登录</text>
|
||
</view>
|
||
|
||
<!-- 班级选择 -->
|
||
<picker :range="classNames" @change="onClassPickerChange" :value="classPickerIndex" class="class-picker-wrap">
|
||
<view class="class-picker-btn" :class="{ active: !!selectedClassId }">
|
||
<text class="class-picker-text">{{ selectedClassName || '选择班级' }}</text>
|
||
<text class="class-picker-arrow">▼</text>
|
||
</view>
|
||
</picker>
|
||
|
||
<view class="left-actions">
|
||
<view class="action-item" @click="handleNavigate('/pages/teacher/homework/filter')">
|
||
<text class="action-icon">📊</text>
|
||
<text class="action-label">学情报告</text>
|
||
</view>
|
||
<view class="action-item logout" v-if="isLoggedIn" @click="handleLogout">
|
||
<text class="action-label">退出登录</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右侧面板:数据看板 -->
|
||
<view class="right-panel">
|
||
<!-- 顶部筛选栏 -->
|
||
<view class="filter-bar" :style="topRightPaddingStyle">
|
||
<!-- 学科标签(横向滚动) -->
|
||
<scroll-view v-if="subjectList.length > 0" class="subject-scroll" scroll-x enhanced :show-scrollbar="false">
|
||
<view class="subject-tags">
|
||
<view
|
||
v-for="sub in subjectList"
|
||
:key="sub.value"
|
||
class="subject-tag"
|
||
:class="{ active: selectedSubjectIds.includes(sub.value) }"
|
||
@click="toggleSubject(sub.value)"
|
||
>
|
||
<text>{{ sub.label }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 日期选择 -->
|
||
<view class="date-group">
|
||
<picker mode="date" :value="startTime" @change="onStartChange">
|
||
<view class="date-chip" :class="{ filled: !!startTime }">
|
||
<text>{{ startTime || '开始' }}</text>
|
||
</view>
|
||
</picker>
|
||
<text class="date-sep">~</text>
|
||
<picker mode="date" :value="endTime" @change="onEndChange">
|
||
<view class="date-chip" :class="{ filled: !!endTime }">
|
||
<text>{{ endTime || '结束' }}</text>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 查询按钮 -->
|
||
<view class="query-btn" @click="fetchOverview">
|
||
<text>查询</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 看板内容区 -->
|
||
<scroll-view class="dashboard-body" scroll-y enhanced :show-scrollbar="false">
|
||
<!-- 加载 / 空状态 -->
|
||
<view v-if="loading" class="status-box">
|
||
<text class="status-text">加载中...</text>
|
||
</view>
|
||
<view v-else-if="!selectedClassId" class="status-box">
|
||
<text class="status-text">请选择班级查看数据</text>
|
||
</view>
|
||
<view v-else-if="errorMsg" class="status-box error">
|
||
<text class="status-text">{{ errorMsg }}</text>
|
||
</view>
|
||
<view v-else-if="overviewData" class="dashboard-content">
|
||
<!-- 核心指标卡片 -->
|
||
<view class="metrics-row">
|
||
<view class="metric-card paper">
|
||
<view class="metric-icon">📄</view>
|
||
<view class="metric-info">
|
||
<text class="metric-value">{{ overviewData.aiPaperCount || 0 }}<text class="metric-unit">份</text></text>
|
||
<text class="metric-label">AI小助手为您批改试卷</text>
|
||
</view>
|
||
</view>
|
||
<view class="metric-card title">
|
||
<view class="metric-icon">📝</view>
|
||
<view class="metric-info">
|
||
<text class="metric-value">{{ overviewData.aiTitleCount || 0 }}<text class="metric-unit">个</text></text>
|
||
<text class="metric-label">AI小助手为您批改题目</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 节约时间提示横幅 -->
|
||
<view class="save-time-banner">
|
||
<text class="save-time-icon">🎉</text>
|
||
<text class="save-time-text">{{ saveTimeText }}</text>
|
||
</view>
|
||
|
||
<!-- 成绩分布(全宽) -->
|
||
<view class="grade-section">
|
||
<view class="section-head">
|
||
<text class="section-title">成绩分布</text>
|
||
</view>
|
||
<view class="grade-cards">
|
||
<view class="grade-card excellent">
|
||
<view class="grade-card-top">
|
||
<text class="grade-card-count">{{ overviewData.excellentCount || 0 }}</text>
|
||
<text class="grade-card-rate">{{ formatRate(overviewData.excellentRate) }}</text>
|
||
</view>
|
||
<view class="grade-card-bar">
|
||
<view class="grade-card-fill" :style="{ width: (overviewData.excellentRate || 0) + '%' }" />
|
||
</view>
|
||
<text class="grade-card-label">优秀</text>
|
||
</view>
|
||
<view class="grade-card good">
|
||
<view class="grade-card-top">
|
||
<text class="grade-card-count">{{ overviewData.goodCount || 0 }}</text>
|
||
<text class="grade-card-rate">{{ formatRate(overviewData.goodRate) }}</text>
|
||
</view>
|
||
<view class="grade-card-bar">
|
||
<view class="grade-card-fill" :style="{ width: (overviewData.goodRate || 0) + '%' }" />
|
||
</view>
|
||
<text class="grade-card-label">良好</text>
|
||
</view>
|
||
<view class="grade-card pass">
|
||
<view class="grade-card-top">
|
||
<text class="grade-card-count">{{ overviewData.passCount || 0 }}</text>
|
||
<text class="grade-card-rate">{{ formatRate(overviewData.passRate) }}</text>
|
||
</view>
|
||
<view class="grade-card-bar">
|
||
<view class="grade-card-fill" :style="{ width: (overviewData.passRate || 0) + '%' }" />
|
||
</view>
|
||
<text class="grade-card-label">合格</text>
|
||
</view>
|
||
<view class="grade-card unsub">
|
||
<view class="grade-card-top">
|
||
<text class="grade-card-count">{{ overviewData.unsubmittedCount || 0 }}</text>
|
||
<text class="grade-card-rate">{{ formatRate(overviewData.unsubmittedRate) }}</text>
|
||
</view>
|
||
<view class="grade-card-bar">
|
||
<view class="grade-card-fill" :style="{ width: (overviewData.unsubmittedRate || 0) + '%' }" />
|
||
</view>
|
||
<text class="grade-card-label">未提交</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue';
|
||
import { onShow } from '@dcloudio/uni-app';
|
||
import { teacher } from '@/store/teacher';
|
||
import { user } from '@/store';
|
||
import { storeToRefs } from 'pinia';
|
||
import {
|
||
getClassListByTeacher,
|
||
getSubjectListByClassId,
|
||
getAiCorrectionOverview,
|
||
} from '@/api/teacher';
|
||
import type { AiCorrectionOverview } from '@/api/teacher';
|
||
|
||
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
||
const defaultAvatar = `${OSS_URL}/urm/logo.png`;
|
||
|
||
const teacherStore = teacher();
|
||
const userStore = user();
|
||
const { teacherInfo, schoolId } = storeToRefs(teacherStore);
|
||
const { token } = storeToRefs(userStore);
|
||
|
||
const isLoggedIn = computed(() => !!token.value && !!teacherInfo.value.id);
|
||
|
||
// ==================== 安全区域 / 胶囊 ====================
|
||
const topPaddingRightRpx = ref(0);
|
||
const safeAreaLeftRpx = ref(0);
|
||
const topRightPaddingStyle = computed(() =>
|
||
topPaddingRightRpx.value > 0 ? { paddingRight: topPaddingRightRpx.value + 'rpx' } : {},
|
||
);
|
||
const safeAreaStyle = computed(() => {
|
||
const style: Record<string, string> = {};
|
||
if (safeAreaLeftRpx.value > 0) style.paddingLeft = safeAreaLeftRpx.value + 'rpx';
|
||
return style;
|
||
});
|
||
function getCapsulePadding() {
|
||
try {
|
||
// #ifdef MP-WEIXIN
|
||
const menuButton = uni.getMenuButtonBoundingClientRect();
|
||
const sys = uni.getSystemInfoSync();
|
||
if (menuButton && sys?.windowWidth) {
|
||
const r = 750 / sys.windowWidth;
|
||
topPaddingRightRpx.value = Math.ceil((sys.windowWidth - menuButton.left) * r) + 24;
|
||
} else {
|
||
topPaddingRightRpx.value = 140;
|
||
}
|
||
if (sys?.safeArea && sys.safeArea.left > 0) {
|
||
safeAreaLeftRpx.value = Math.ceil(sys.safeArea.left * (750 / (sys.windowWidth || sys.screenWidth))) + 4;
|
||
}
|
||
// #endif
|
||
} catch {
|
||
topPaddingRightRpx.value = 140;
|
||
}
|
||
}
|
||
|
||
// ==================== 导航 / 操作 ====================
|
||
const handleNavigate = (url: string) => {
|
||
if (!token.value) {
|
||
uni.navigateTo({ url: `/pages/login/index?redirect=${encodeURIComponent(url)}` });
|
||
return;
|
||
}
|
||
uni.navigateTo({ url });
|
||
};
|
||
const goLogin = () => uni.navigateTo({ url: '/pages/login/index' });
|
||
const handleLogout = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要退出登录吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
teacherStore.clear();
|
||
userStore.logout();
|
||
}
|
||
},
|
||
});
|
||
};
|
||
|
||
// ==================== 筛选状态 ====================
|
||
// 班级
|
||
const classList = ref<{ id: string; name: string }[]>([]);
|
||
const classPickerIndex = ref(0);
|
||
const selectedClassId = ref('');
|
||
const selectedClassName = computed(() => {
|
||
const cls = classList.value.find((c) => c.id === selectedClassId.value);
|
||
return cls?.name || '';
|
||
});
|
||
const classNames = computed(() => classList.value.map((c) => c.name));
|
||
|
||
function onClassPickerChange(e: any) {
|
||
const idx = Number(e.detail.value);
|
||
classPickerIndex.value = idx;
|
||
if (classList.value[idx]) {
|
||
selectedClassId.value = classList.value[idx].id;
|
||
}
|
||
}
|
||
|
||
// 学科
|
||
const subjectList = ref<{ value: number; label: string }[]>([]);
|
||
const selectedSubjectIds = ref<number[]>([]);
|
||
|
||
function toggleSubject(value: number) {
|
||
const idx = selectedSubjectIds.value.indexOf(value);
|
||
if (idx >= 0) {
|
||
selectedSubjectIds.value = selectedSubjectIds.value.filter((v) => v !== value);
|
||
} else {
|
||
selectedSubjectIds.value = [...selectedSubjectIds.value, value];
|
||
}
|
||
}
|
||
|
||
// 监听班级变化 → 加载学科
|
||
watch(() => selectedClassId.value, async (newId) => {
|
||
selectedSubjectIds.value = [];
|
||
subjectList.value = [];
|
||
if (!newId) return;
|
||
try {
|
||
const res = await getSubjectListByClassId(newId);
|
||
const data = res?.data ?? res;
|
||
if (Array.isArray(data)) {
|
||
subjectList.value = data.map((item: any) => ({
|
||
value: Number(item.subjectId ?? item.id ?? item.value),
|
||
label: item.subjectName ?? item.name ?? item.label ?? '',
|
||
}));
|
||
}
|
||
// 自动选中老师对应学科
|
||
const tid = Number(teacherInfo.value?.subjectId);
|
||
if (!isNaN(tid) && tid && subjectList.value.some((s) => s.value === tid)) {
|
||
selectedSubjectIds.value = [tid];
|
||
}
|
||
} catch (e) {
|
||
console.error('获取学科列表失败', e);
|
||
}
|
||
// 选择班级后自动查询
|
||
fetchOverview();
|
||
});
|
||
|
||
// 日期(默认当天)
|
||
function getTodayStr() {
|
||
const d = new Date();
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
}
|
||
const startTime = ref(getTodayStr());
|
||
const endTime = ref(getTodayStr());
|
||
function onStartChange(e: any) { startTime.value = e.detail?.value || ''; }
|
||
function onEndChange(e: any) { endTime.value = e.detail?.value || ''; }
|
||
|
||
// ==================== 看板数据 ====================
|
||
const overviewData = ref<AiCorrectionOverview | null>(null);
|
||
const loading = ref(false);
|
||
const errorMsg = ref('');
|
||
|
||
// 节约时间友好文案
|
||
const saveTimeText = computed(() => {
|
||
const mins = overviewData.value?.teacherSaveTimeMinutes || 0;
|
||
const suffix = ',请您注意休息!';
|
||
if (mins <= 0) return 'AI小助手已就绪,随时为您节省批阅时间' + suffix;
|
||
if (mins >= 1440) {
|
||
const days = (mins / 1440).toFixed(1).replace(/\.0$/, '');
|
||
return 'AI小助手已为您节省了 ' + days + ' 天的批阅时间' + suffix;
|
||
}
|
||
if (mins >= 60) {
|
||
const hours = (mins / 60).toFixed(1).replace(/\.0$/, '');
|
||
return 'AI小助手已为您节省了 ' + hours + ' 小时的批阅时间' + suffix;
|
||
}
|
||
return 'AI小助手已为您节省了 ' + mins + ' 分钟的批阅时间' + suffix;
|
||
});
|
||
|
||
function formatRate(rate: number | undefined | null): string {
|
||
if (rate == null) return '0%';
|
||
return (typeof rate === 'number' ? (rate > 1 ? rate.toFixed(1) : (rate * 100).toFixed(1)) : rate) + '%';
|
||
}
|
||
|
||
async function fetchOverview() {
|
||
if (!selectedClassId.value) return;
|
||
loading.value = true;
|
||
errorMsg.value = '';
|
||
try {
|
||
const params: any = { classId: selectedClassId.value };
|
||
if (schoolId.value) params.schoolId = schoolId.value;
|
||
if (selectedSubjectIds.value.length > 0) params.subjectIds = selectedSubjectIds.value;
|
||
if (startTime.value?.trim()) params.startTime = startTime.value.trim();
|
||
if (endTime.value?.trim()) params.endTime = endTime.value.trim();
|
||
|
||
const res = await getAiCorrectionOverview(params);
|
||
overviewData.value = (res?.data ?? res) as AiCorrectionOverview;
|
||
} catch (e) {
|
||
console.error('获取看板数据失败', e);
|
||
errorMsg.value = '加载失败,请重试';
|
||
overviewData.value = null;
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// ==================== 初始化 ====================
|
||
const fetchClassList = async () => {
|
||
try {
|
||
const res = await getClassListByTeacher();
|
||
const data = res?.data ?? res;
|
||
classList.value = Array.isArray(data) ? data : data?.list ?? data?.rows ?? [];
|
||
// 如果只有一个班级,自动选中
|
||
if (classList.value.length === 1) {
|
||
selectedClassId.value = classList.value[0].id;
|
||
classPickerIndex.value = 0;
|
||
}
|
||
} catch (e) {
|
||
console.error('获取班级列表失败', e);
|
||
}
|
||
};
|
||
|
||
const fetchTeacherInfo = async () => {
|
||
if (!token.value) return;
|
||
try {
|
||
await teacherStore.getLoginUser();
|
||
} catch (e) {
|
||
console.error('获取老师信息失败', e);
|
||
}
|
||
};
|
||
|
||
onShow(async () => {
|
||
getCapsulePadding();
|
||
if (token.value) {
|
||
await fetchTeacherInfo();
|
||
if (classList.value.length === 0) {
|
||
await fetchClassList();
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
$teacher-bg: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/main_bg.svg';
|
||
|
||
.dashboard-page {
|
||
height: 100vh;
|
||
width: 100vw;
|
||
background-image: url($teacher-bg);
|
||
background-size: cover;
|
||
background-position: center;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: row;
|
||
overflow: hidden;
|
||
}
|
||
|
||
// ========== 左侧面板 ==========
|
||
.left-panel {
|
||
width: 140rpx;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 16rpx 8rpx;
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.brand {
|
||
text-align: center;
|
||
margin-bottom: 12rpx;
|
||
|
||
.brand-title {
|
||
font-family: $font-special;
|
||
font-size: 20rpx;
|
||
color: #fff;
|
||
@include font-stroke($font-color, 2rpx);
|
||
}
|
||
.brand-sub {
|
||
font-size: 12rpx;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
margin-top: 2rpx;
|
||
}
|
||
}
|
||
|
||
.user-card {
|
||
background: rgba(255, 255, 255, 0.92);
|
||
border-radius: 12rpx;
|
||
padding: 10rpx 6rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 100%;
|
||
border: 1rpx solid rgba(196, 181, 255, 0.25);
|
||
|
||
.user-avatar {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border-radius: 50%;
|
||
border: 2rpx solid rgba(196, 181, 255, 0.4);
|
||
margin-bottom: 6rpx;
|
||
}
|
||
.user-name {
|
||
font-family: $font-special;
|
||
font-size: 14rpx;
|
||
color: $font-color;
|
||
@include single-ellipsis;
|
||
max-width: 100%;
|
||
text-align: center;
|
||
}
|
||
.user-school {
|
||
font-size: 10rpx;
|
||
color: #888;
|
||
margin-top: 2rpx;
|
||
@include single-ellipsis;
|
||
max-width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
&.login-card {
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
// 班级选择器
|
||
.class-picker-wrap {
|
||
width: 100%;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.class-picker-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4rpx;
|
||
padding: 8rpx 6rpx;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
border-radius: 8rpx;
|
||
border: 1rpx solid rgba(196, 181, 255, 0.3);
|
||
|
||
.class-picker-text {
|
||
font-family: $font-special;
|
||
font-size: 13rpx;
|
||
color: #666;
|
||
@include single-ellipsis;
|
||
max-width: 100rpx;
|
||
}
|
||
.class-picker-arrow {
|
||
font-size: 10rpx;
|
||
color: #aaa;
|
||
}
|
||
|
||
&.active {
|
||
background: linear-gradient(135deg, #8f9df7 0%, #cab5ff 100%);
|
||
border-color: #8f9df7;
|
||
.class-picker-text { color: #fff; font-weight: 600; }
|
||
.class-picker-arrow { color: rgba(255, 255, 255, 0.8); }
|
||
}
|
||
|
||
&:active { opacity: 0.85; }
|
||
}
|
||
|
||
.left-actions {
|
||
margin-top: auto;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.action-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 8rpx 4rpx;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
border-radius: 8rpx;
|
||
border: 1rpx solid rgba(196, 181, 255, 0.2);
|
||
|
||
.action-icon {
|
||
font-size: 18rpx;
|
||
margin-bottom: 2rpx;
|
||
}
|
||
.action-label {
|
||
font-family: $font-special;
|
||
font-size: 11rpx;
|
||
color: #666;
|
||
}
|
||
|
||
&.logout {
|
||
background: rgba(255, 255, 255, 0.6);
|
||
.action-label { color: #999; }
|
||
}
|
||
|
||
&:active { opacity: 0.8; }
|
||
}
|
||
|
||
// ========== 右侧面板 ==========
|
||
.right-panel {
|
||
flex: 1;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0 12rpx 0 6rpx;
|
||
}
|
||
|
||
// ========== 筛选栏 ==========
|
||
.filter-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
padding: 6rpx 0 4rpx;
|
||
flex-shrink: 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 3rpx;
|
||
padding: 4rpx 10rpx;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border: 1rpx solid rgba(196, 181, 255, 0.3);
|
||
border-radius: 6rpx;
|
||
flex-shrink: 0;
|
||
|
||
.filter-chip-text {
|
||
font-family: $font-special;
|
||
font-size: 13rpx;
|
||
color: #666;
|
||
max-width: 120rpx;
|
||
@include single-ellipsis;
|
||
}
|
||
.filter-chip-arrow {
|
||
font-size: 10rpx;
|
||
color: #aaa;
|
||
}
|
||
|
||
&.active {
|
||
background: linear-gradient(135deg, #8f9df7 0%, #cab5ff 100%);
|
||
border-color: #8f9df7;
|
||
.filter-chip-text { color: #fff; font-weight: 600; }
|
||
.filter-chip-arrow { color: rgba(255, 255, 255, 0.8); }
|
||
}
|
||
}
|
||
|
||
.subject-scroll {
|
||
flex: 1;
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.subject-tags {
|
||
display: inline-flex;
|
||
gap: 4rpx;
|
||
padding-right: 4rpx;
|
||
}
|
||
|
||
.subject-tag {
|
||
padding: 3rpx 8rpx;
|
||
border-radius: 6rpx;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border: 1rpx solid rgba(196, 181, 255, 0.25);
|
||
flex-shrink: 0;
|
||
|
||
text {
|
||
font-family: $font-special;
|
||
font-size: 12rpx;
|
||
color: #888;
|
||
}
|
||
|
||
&.active {
|
||
background: linear-gradient(135deg, #5b6ef7 0%, #8f6df7 100%);
|
||
border-color: #5b6ef7;
|
||
text { color: #fff; font-weight: 600; }
|
||
}
|
||
}
|
||
|
||
.date-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 3rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.date-chip {
|
||
padding: 3rpx 8rpx;
|
||
border-radius: 6rpx;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border: 1rpx solid rgba(196, 181, 255, 0.25);
|
||
|
||
text {
|
||
font-size: 12rpx;
|
||
color: #999;
|
||
}
|
||
|
||
&.filled {
|
||
border-color: #8f9df7;
|
||
text { color: #8f9df7; }
|
||
}
|
||
}
|
||
|
||
.date-sep {
|
||
font-size: 11rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.query-btn {
|
||
padding: 4rpx 14rpx;
|
||
border-radius: 6rpx;
|
||
background: linear-gradient(135deg, #8f9df7 0%, #cab5ff 100%);
|
||
flex-shrink: 0;
|
||
|
||
text {
|
||
font-family: $font-special;
|
||
font-size: 12rpx;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&:active { opacity: 0.8; }
|
||
}
|
||
|
||
// ========== 看板主体 ==========
|
||
.dashboard-body {
|
||
flex: 1;
|
||
height: 0;
|
||
}
|
||
|
||
.status-box {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60rpx 20rpx;
|
||
|
||
.status-text {
|
||
font-family: $font-special;
|
||
font-size: 16rpx;
|
||
color: #fff;
|
||
text-shadow: 0 1rpx 4rpx rgba(143, 157, 247, 0.5);
|
||
}
|
||
|
||
&.error .status-text { color: #ffb3b3; text-shadow: 0 1rpx 4rpx rgba(224, 80, 64, 0.4); }
|
||
}
|
||
|
||
.dashboard-content {
|
||
padding: 2rpx 0 12rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
// ========== 核心指标卡片 ==========
|
||
.metrics-row {
|
||
display: flex;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.metric-card {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
padding: 16rpx 14rpx;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid rgba(196, 181, 255, 0.18);
|
||
background: rgba(255, 255, 255, 0.95);
|
||
|
||
.metric-icon {
|
||
font-size: 32rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.metric-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.metric-value {
|
||
font-family: $font-special;
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: $font-color;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.metric-unit {
|
||
font-size: 14rpx;
|
||
font-weight: 400;
|
||
color: #999;
|
||
margin-left: 2rpx;
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 13rpx;
|
||
color: #999;
|
||
margin-top: 3rpx;
|
||
}
|
||
|
||
&.paper {
|
||
background: linear-gradient(135deg, #eee8ff 0%, #fff 100%);
|
||
border-color: rgba(143, 157, 247, 0.3);
|
||
.metric-value { color: #6c5ce7; }
|
||
}
|
||
&.title {
|
||
background: linear-gradient(135deg, #e3ecff 0%, #fff 100%);
|
||
border-color: rgba(91, 141, 239, 0.3);
|
||
.metric-value { color: #3867d6; }
|
||
}
|
||
}
|
||
|
||
// ========== 节约时间横幅 ==========
|
||
.save-time-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 10rpx 16rpx;
|
||
background: linear-gradient(135deg, rgba(47, 173, 99, 0.1) 0%, rgba(80, 216, 128, 0.08) 100%);
|
||
border: 1rpx solid rgba(47, 173, 99, 0.2);
|
||
border-radius: 10rpx;
|
||
|
||
.save-time-icon {
|
||
font-size: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.save-time-text {
|
||
font-family: $font-special;
|
||
font-size: 15rpx;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
// ========== 成绩分布标题 ==========
|
||
.section-head {
|
||
margin-bottom: 8rpx;
|
||
|
||
.section-title {
|
||
font-family: $font-special;
|
||
font-size: 18rpx;
|
||
font-weight: 600;
|
||
color: $font-color;
|
||
}
|
||
}
|
||
|
||
// ========== 成绩分布 ==========
|
||
.grade-section {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 12rpx;
|
||
padding: 14rpx 16rpx;
|
||
border: 1rpx solid rgba(196, 181, 255, 0.18);
|
||
flex: 1;
|
||
}
|
||
|
||
.grade-cards {
|
||
display: flex;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.grade-card {
|
||
flex: 1;
|
||
padding: 12rpx 14rpx;
|
||
border-radius: 10rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.grade-card-top {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6rpx;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.grade-card-count {
|
||
font-family: $font-special;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.grade-card-rate {
|
||
font-size: 13rpx;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.grade-card-bar {
|
||
height: 6rpx;
|
||
background: rgba(255, 255, 255, 0.4);
|
||
border-radius: 6rpx;
|
||
overflow: hidden;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.grade-card-fill {
|
||
height: 100%;
|
||
border-radius: 6rpx;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
transition: width 0.5s ease;
|
||
}
|
||
|
||
.grade-card-label {
|
||
font-family: $font-special;
|
||
font-size: 13rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&.excellent {
|
||
background: linear-gradient(135deg, #e0fae9 0%, #c8f5d8 100%);
|
||
border: 1rpx solid rgba(47, 173, 99, 0.15);
|
||
.grade-card-count { color: #20a04b; }
|
||
.grade-card-rate { color: #20a04b; }
|
||
.grade-card-label { color: #2fad63; }
|
||
.grade-card-fill { background: #2fad63; }
|
||
}
|
||
&.good {
|
||
background: linear-gradient(135deg, #e3ecff 0%, #d0dfff 100%);
|
||
border: 1rpx solid rgba(91, 141, 239, 0.15);
|
||
.grade-card-count { color: #3867d6; }
|
||
.grade-card-rate { color: #3867d6; }
|
||
.grade-card-label { color: #5b8def; }
|
||
.grade-card-fill { background: #5b8def; }
|
||
}
|
||
&.pass {
|
||
background: linear-gradient(135deg, #fff5e0 0%, #ffeccc 100%);
|
||
border: 1rpx solid rgba(247, 166, 53, 0.15);
|
||
.grade-card-count { color: #d68f00; }
|
||
.grade-card-rate { color: #d68f00; }
|
||
.grade-card-label { color: #f7a635; }
|
||
.grade-card-fill { background: #f7a635; }
|
||
}
|
||
&.unsub {
|
||
background: linear-gradient(135deg, #ffe8e5 0%, #ffd6d2 100%);
|
||
border: 1rpx solid rgba(224, 80, 64, 0.15);
|
||
.grade-card-count { color: #c0392b; }
|
||
.grade-card-rate { color: #c0392b; }
|
||
.grade-card-label { color: #e05040; }
|
||
.grade-card-fill { background: #e05040; }
|
||
}
|
||
}
|
||
|
||
</style>
|