style(ui): 优化页面容器及布局样式,调整头部组件图标和交互

- 为多个页面容器添加最小高度类,保证页面满屏显示
- 调整部分主内容区高度样式,增强布局一致性和视觉整洁
- 替换头部组件Logo为自定义SVG图标,提升品牌识别度
- 优化头部用户菜单按钮交互和样式,统一暗黑模式视觉效果
- 调整TTS页面词汇列表布局,支持移动端和桌面端不同显示方式
- 修改学生详情页面样式,提升各模块容器的统一性和分隔感
- 修正历史数据日期格式,将“T”替换为空格以增强可读性
This commit is contained in:
lbw
2026-01-05 11:21:55 +08:00
parent deabd5f7f5
commit bf2a80917c
8 changed files with 140 additions and 105 deletions

View File

@@ -1 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
fill="#0056D2" />
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 865 B

View File

@@ -5,8 +5,14 @@
<nav class="fluent-nav px-4 lg:px-6 py-2.5">
<div class="flex flex-wrap justify-between items-center">
<a href="#" class="flex items-center">
<img src="https://flowbite.com/docs/images/logo.svg" class="mr-3 h-6 sm:h-9" alt="Flowbite Logo" width="24" height="24" />
<span class="self-center text-xl font-semibold whitespace-nowrap">Flowbite</span>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
fill="#0056D2" />
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
</svg>
<span class="self-center text-xl font-semibold whitespace-nowrap">英语教育</span>
</a>
<div class="flex items-center lg:order-2">
<template v-if="userName">
@@ -20,10 +26,12 @@
</svg>
</button>
<div v-if="menuOpen" class="fluent-card absolute right-0 mt-2 z-50">
<router-link to="/admid" @click="menuOpen = false" class="block px-4 py-2 fluent-link">
<router-link to="/admid" @click="menuOpen = false"
class="block px-4 py-2 fluent-link">
后台
</router-link>
<button @click="handleLogout" class="w-full text-left block px-4 py-2 fluent-link">
<button @click="handleLogout"
class="w-full text-left block px-4 py-2 fluent-link">
登出
</button>
</div>
@@ -33,7 +41,8 @@
class="inline-flex items-center p-2 ml-1 text-sm rounded-lg lg:hidden fluent-btn"
aria-controls="mobile-menu-2" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"></path>
@@ -46,7 +55,8 @@
</svg>
</button>
</div>
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1" id="mobile-menu-2">
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1"
id="mobile-menu-2">
</div>
</div>
@@ -111,10 +121,12 @@ onBeforeUnmount(() => {
backdrop-filter: none;
min-height: 56px;
}
:global(.dark) .fluent-nav {
background: transparent;
border-bottom: 0;
}
.fluent-card {
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.4);
@@ -123,14 +135,17 @@ onBeforeUnmount(() => {
backdrop-filter: blur(16px);
transition: box-shadow 200ms ease, transform 200ms ease;
}
:global(.dark) .fluent-card {
background: rgba(55, 65, 81, 0.4);
border-color: rgba(148, 163, 184, 0.25);
}
.fluent-card:hover {
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
transform: translateY(-1px);
}
.fluent-btn {
color: #0f172a;
background: rgba(255, 255, 255, 0.6);
@@ -139,32 +154,39 @@ onBeforeUnmount(() => {
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
transition: background 200ms ease, box-shadow 200ms ease, transform 200ms ease;
}
.fluent-btn:hover {
background: rgba(255, 255, 255, 0.7);
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
}
:global(.dark) .fluent-btn {
color: #e5e7eb;
background: rgba(55, 65, 81, 0.4);
border-color: rgba(148, 163, 184, 0.25);
}
.fluent-link {
color: #2563eb;
border-radius: 10px;
transition: color 200ms ease, background 200ms ease, box-shadow 200ms ease;
}
.fluent-link:hover {
color: #1d4ed8;
background: rgba(255, 255, 255, 0.35);
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
}
:global(.dark) .fluent-link:hover {
background: rgba(55, 65, 81, 0.35);
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
}
.fluent-card {
overflow: visible;
}
:global(.el-header) {
overflow: visible;
padding: 0;

View File

@@ -25,7 +25,7 @@ function sortData(arr) {
function toSource(arr) {
return sortData(arr).map(it => ({
startTime: it.startTime,
startTime: it.startTime.replace('T', ' '),
totalCount: Number(it.totalCount) || 0,
planId: it.planId ?? null,
id: it.id ?? null

View File

@@ -1,6 +1,6 @@
<template>
<div class="common-layout">
<el-container>
<el-container class="min-h-screen">
<el-header>
<Header></Header>
</el-header>

View File

@@ -1,54 +1,63 @@
<template>
<div class="common-layout">
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-container>
<el-aside width="200px" class="pt-4">
<Sidebar />
</el-aside>
<el-main class="p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<el-container class="pt-4">
<el-main class="p-2">
<div class="panel-shell p-6">
<div class="text-lg font-semibold mb-4">TTS</div>
<div class="flex items-center gap-3 mb-4">
<el-input v-model="planIdInput" placeholder="planId" style="max-width: 220px" />
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-4">
<!-- <el-input v-model="planIdInput" placeholder="planId" class="w-full sm:w-auto" style="max-width: 220px" /> -->
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
<el-select v-model="voice" placeholder="选择声线" style="max-width: 160px">
<!-- <el-select v-model="voice" placeholder="选择声线" class="w-full sm:w-auto" style="max-width: 160px">
<el-option label="alloy" value="alloy" />
<el-option label="verse" value="verse" />
<el-option label="nova" value="nova" />
</el-select>
<el-select v-model="format" placeholder="格式" style="max-width: 120px">
</el-select> -->
<!-- <el-select v-model="format" placeholder="格式" class="w-full sm:w-auto" style="max-width: 120px">
<el-option label="mp3" value="mp3" />
<el-option label="wav" value="wav" />
<el-option label="ogg" value="ogg" />
</el-select>
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll"
@click="onGenerateAll">生成全部音频</el-button>
</el-select> -->
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll" class="!ml-0"
@click="onGenerateAll">生成音频</el-button>
</div>
<el-table :data="tableData" border class="w-full" v-loading="loadingWords">
<el-table-column prop="word" label="词汇/短语" min-width="260" />
<el-table-column label="状态" width="160">
<div class="sm:hidden">
<div v-for="row in tableData" :key="row.word" class="panel-shell p-4 mb-3">
<div class="flex items-center justify-between">
<div class="font-medium">{{ row.word }}</div>
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
{{ row.audioUrl ? '已生成' : '未生成' }}
</el-tag>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
<el-button size="small" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
<el-button size="small" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
</div>
</div>
</div>
<div class="hidden sm:block overflow-x-auto">
<el-table :data="tableData" border class="min-w-[640px]" v-loading="loadingWords" size="small">
<el-table-column prop="word" label="词汇/短语" min-width="200" />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
{{ row.audioUrl ? '已生成' : '未生成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="360" fixed="right">
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" :loading="row.loading"
@click="onGenerateOne(row)">生成音频</el-button>
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
@click="onPlay(row)">播放</el-button>
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
@click="onDownload(row)">下载</el-button>
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="mt-3 text-sm text-gray-500">
{{ words.length }}
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="common-layout">
<el-container>
<el-container class="min-h-screen">
<el-header>
<Header></Header>
</el-header>
@@ -10,7 +10,7 @@
<Sidebar></Sidebar>
</el-aside>
<el-main class="">
<el-main class="h-full">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="lg:col-span-1 flex flex-col gap-6">
<div class="panel-shell p-6">

View File

@@ -1,17 +1,14 @@
<template>
<div class="common-layout">
<el-container>
<el-container class="min-h-screen">
<el-header>
<Header></Header>
</el-header>
<el-container>
<el-aside width="200px" class="pt-4">
<Sidebar />
</el-aside>
<el-main class="p-4">
<el-container class="pt-4">
<el-main class="h-full">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="panel-shell p-6">
<div class="text-lg font-semibold mb-4">学生详情</div>
<template v-if="detail">
<el-descriptions :column="1" border>
@@ -19,46 +16,43 @@
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade
}}</el-descriptions-item>
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
</el-descriptions>
</template>
<template v-else>
<el-empty description="请从班级页跳转" />
</template>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="panel-shell p-6">
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
<template v-if="wordStat">
<el-descriptions :column="1" border>
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount
}}</el-descriptions-item>
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount
}}</el-descriptions-item>
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount
}}</el-descriptions-item>
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount }}</el-descriptions-item>
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
</el-descriptions>
</template>
<template v-else>
<el-empty description="暂无统计" />
</template>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="panel-shell p-6">
<div class="text-md font-semibold mb-3">学生考试记录</div>
<ExamHistoryChart :data="history" />
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="panel-shell p-6">
<div class="text-md font-semibold mb-3">学生学案记录</div>
<PlanHistoryChart :student-id="route.params.id" />
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="panel-shell p-6 lg:col-span-2">
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
</div>
<div class="panel-shell p-6 lg:col-span-2">
<div class="text-md font-semibold mb-3">学情分析</div>
<StudyAnalysis :student-id="route.params.id" />
</div>
</div>
</el-main>
</el-container>
</el-container>
@@ -105,6 +99,10 @@ async function fetchExamHistory() {
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
}) : []
// 遍历 history 中的 startDate 去掉其中的 T
history.value.forEach(item => {
item.startDate = item.startDate.replace('T', ' ')
})
}
async function fetchWordStat() {

View File

@@ -1,6 +1,6 @@
<template>
<div class="common-layout">
<el-container>
<el-container class="min-h-screen">
<el-header>
<Header></Header>
</el-header>