fix: 修复白屏问题、路由冲突和TypeScript错误,打包问题,调整tab悬停样式
BIN
public/images/aiAssistant/AI助教1.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
public/images/aiAssistant/AI助教2.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
public/images/aiAssistant/bg.png
Normal file
After Width: | Height: | Size: 16 MiB |
BIN
public/images/aiAssistant/points.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/images/aiAssistant/upload.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
public/images/aiAssistant/多维度分析.png
Normal file
After Width: | Height: | Size: 295 KiB |
BIN
public/images/aiAssistant/失败icon.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
public/images/aiAssistant/教学内容分析.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/images/aiAssistant/教学动作分析.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/images/aiAssistant/教学情感分析.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/images/aiAssistant/教学语言分析.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/images/aiAssistant/教师-女.png
Normal file
After Width: | Height: | Size: 4.1 MiB |
BIN
public/images/aiAssistant/教师-男.png
Normal file
After Width: | Height: | Size: 3.3 MiB |
BIN
public/images/aiAssistant/数据支持.png
Normal file
After Width: | Height: | Size: 263 KiB |
BIN
public/images/aiAssistant/智能评估.png
Normal file
After Width: | Height: | Size: 352 KiB |
BIN
public/images/aiAssistant/编组 3备份.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
67
src/App.vue
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { onMounted, computed, ref } from 'vue'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
@ -65,19 +65,32 @@ const themeOverrides: GlobalThemeOverrides = {
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const isInitializing = ref(true)
|
||||
|
||||
// 检查是否为登录页面
|
||||
const isLoginPage = computed(() => route.name === 'Login')
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化用户认证状态
|
||||
userStore.initializeAuth()
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 初始化用户认证状态
|
||||
await userStore.initializeAuth()
|
||||
} finally {
|
||||
// 无论初始化成功与否,都显示页面
|
||||
isInitializing.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<n-config-provider :theme-overrides="themeOverrides" :locale="naiveLocale" :date-locale="naiveDateLocale">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isInitializing" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载...</p>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<n-config-provider v-else :theme-overrides="themeOverrides" :locale="naiveLocale" :date-locale="naiveDateLocale">
|
||||
<n-dialog-provider>
|
||||
<!-- 登录页面不使用 AppLayout,但需要 message provider -->
|
||||
<template v-if="isLoginPage">
|
||||
@ -113,7 +126,8 @@ html {
|
||||
|
||||
/* 移除全屏相关样式,使用正常布局 */
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
@ -184,4 +198,45 @@ body {
|
||||
.d-block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e3e3e3;
|
||||
border-top: 4px solid #0288D1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -28,7 +28,7 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/jeecgboot',
|
||||
timeout: 30000, // 增加到30秒
|
||||
timeout: 10000, // 减少到10秒,避免长时间等待
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -1,14 +1,263 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>个人中心</h1>
|
||||
<div class="personal-center">
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="{ active: activeTab === 'base' }" @click="activeTab = 'base'">基础信息</div>
|
||||
<div class="tab" :class="{ active: activeTab === 'password' }" @click="activeTab = 'password'">密码修改</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: 基础信息 -->
|
||||
<div v-if="activeTab === 'base'" class="card">
|
||||
<div class="card-title">基础信息</div>
|
||||
<div class="form">
|
||||
<div class="form-row stack">
|
||||
<label class="label">姓名:</label>
|
||||
<input class="input" type="text" v-model="name" :disabled="!isEditing" />
|
||||
</div>
|
||||
<div class="form-row align-start stack">
|
||||
<label class="label">自我介绍:</label>
|
||||
<textarea class="textarea" v-model="intro" :disabled="!isEditing" rows="4"></textarea>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" @click="startEdit">编辑资料</button>
|
||||
<button class="btn primary" @click="save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: 密码修改 -->
|
||||
<div v-else class="card">
|
||||
<div class="card-title">密码修改</div>
|
||||
<div class="p-form">
|
||||
<div class="p-row stack">
|
||||
<label class="p-label">帐号:</label>
|
||||
<input class="p-input p-input-wide" type="text" v-model="account" disabled />
|
||||
</div>
|
||||
<div class="p-row stack">
|
||||
<label class="p-label">原密码:</label>
|
||||
<input class="p-input p-input-wide" type="password" v-model="oldPassword" placeholder="请输入原密码" />
|
||||
</div>
|
||||
<div class="p-row stack">
|
||||
<label class="p-label">新密码:</label>
|
||||
<input class="p-input p-input-wide" type="password" v-model="newPassword" placeholder="请输入新密码" />
|
||||
</div>
|
||||
<div class="p-row stack">
|
||||
<label class="p-label">确认密码:</label>
|
||||
<input class="p-input p-input-wide" type="password" v-model="confirmPassword" placeholder="请确认密码" />
|
||||
</div>
|
||||
<div class="p-actions">
|
||||
<button class="btn primary" @click="savePassword">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isEditing = ref(false)
|
||||
const activeTab = ref<'base' | 'password'>('base')
|
||||
const name = ref('张成学')
|
||||
const intro = ref(
|
||||
'复旦大学经济学院教师,长期从事西方经济学的教学,主要讲授经济学原理、微观经济学、宏观经济学、管理经济学等基础课程,参与的“宏观经济学课程”被评为上海市精品课程与国家级精品课程'
|
||||
)
|
||||
|
||||
// 密码修改表单
|
||||
const account = ref('16568855622')
|
||||
const oldPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
function startEdit() {
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
function save() {
|
||||
// 可在此接入后端提交逻辑
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
function savePassword() {
|
||||
if (!oldPassword.value || !newPassword.value || !confirmPassword.value) {
|
||||
return
|
||||
}
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
return
|
||||
}
|
||||
// 对接后端修改密码接口
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.personal-center {
|
||||
background: #fff;
|
||||
min-height: 100%;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
border-bottom: 1.5px solid #F1F3F4;
|
||||
padding: 0 0 12px 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
padding-bottom: 8px;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #0288D1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -13px;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
background: #0288D1;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
padding: 16px 16px 20px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1.5px solid #E6E6E6;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 130px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-row.stack {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0 90px;
|
||||
}
|
||||
|
||||
.form-row.align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 72px;
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 340px;
|
||||
height: 41px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
background: #F5F8FB;
|
||||
}
|
||||
|
||||
.input-wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.textarea {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
background: #F5F8FB;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 90px;
|
||||
}
|
||||
|
||||
.p-form {
|
||||
padding: 0 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.p-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.p-row.stack {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.p-label {
|
||||
width: 72px;
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.p-input {
|
||||
width: 320px;
|
||||
height: 41px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
background: #F5F8FB;
|
||||
}
|
||||
|
||||
.p-input-wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
min-width: 92px;
|
||||
height: 32px;
|
||||
border-radius: 1px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #0288D1;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="search-dropdown" v-if="visible">
|
||||
<div class="search-dropdown" v-if="isVisible">
|
||||
<div class="search-container">
|
||||
<!-- 热门搜索 -->
|
||||
<div class="search-section">
|
||||
@ -62,6 +62,9 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 使用props.visible来避免未使用警告
|
||||
const isVisible = computed(() => props.visible)
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
search: [keyword: string]
|
||||
@ -159,7 +162,7 @@ const saveRecentSearch = (keyword: string) => {
|
||||
const storageKey = `recent_search_${userId}`
|
||||
|
||||
// 移除重复项并添加到开头
|
||||
const filtered = recentSearchList.value.filter(item => item !== keyword)
|
||||
const filtered = recentSearchList.value.filter((item: string) => item !== keyword)
|
||||
recentSearchList.value = [keyword, ...filtered].slice(0, 10) // 最多保存10条
|
||||
|
||||
// 保存到localStorage
|
||||
|
@ -59,6 +59,8 @@ import NotificationManagement from '@/views/teacher/course/NotificationManagemen
|
||||
import GeneralManagement from '@/views/teacher/course/GeneralManagement.vue'
|
||||
import UserAgreement from '@/views/UserAgreement.vue'
|
||||
import RecycleBin from '@/views/teacher/resource/RecycleBin.vue'
|
||||
import AIAssistant from '@/views/teacher/ai/Assistant.vue'
|
||||
import AIAssistantDetail from '@/views/teacher/ai/AssistantDetail.vue'
|
||||
|
||||
// 作业子组件
|
||||
import HomeworkLibrary from '@/views/teacher/course/HomeworkLibrary.vue'
|
||||
@ -243,7 +245,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: { title: '证书管理' }
|
||||
},
|
||||
{
|
||||
path: 'certificate/detail/:id',
|
||||
path: 'certificate/detail/:certificateId',
|
||||
name: 'CertificateDetail',
|
||||
component: () => import('@/views/teacher/certificate/CertificateDetail.vue'),
|
||||
meta: { title: '证书详情' }
|
||||
@ -268,7 +270,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: { title: '添加讨论' }
|
||||
},
|
||||
{
|
||||
path: 'comment/:id',
|
||||
path: 'comment/:commentId',
|
||||
name: 'CommentView',
|
||||
component: CommentView,
|
||||
meta: { title: '评论详情' }
|
||||
@ -317,6 +319,18 @@ const routes: RouteRecordRaw[] = [
|
||||
component: RecycleBin,
|
||||
meta: { title: '回收站' }
|
||||
},
|
||||
{
|
||||
path: 'ai-assistant',
|
||||
name: 'AIAssistant',
|
||||
component: AIAssistant,
|
||||
meta: { title: 'AI助教' }
|
||||
},
|
||||
{
|
||||
path: 'ai-assistant-detail/:id',
|
||||
name: 'AIAssistantDetail',
|
||||
component: AIAssistantDetail,
|
||||
meta: { title: '查看详情' }
|
||||
},
|
||||
{
|
||||
path: 'student-management',
|
||||
name: 'StudentManagement',
|
||||
|
@ -181,10 +181,11 @@ export const useUserStore = defineStore('user', () => {
|
||||
user.value = JSON.parse(savedUser)
|
||||
token.value = savedToken
|
||||
|
||||
// 验证token是否仍然有效,并强制刷新用户信息
|
||||
await getCurrentUser(true)
|
||||
// 不强制刷新用户信息,避免API超时导致白屏
|
||||
// 如果需要验证token有效性,可以在用户操作时进行
|
||||
console.log('✅ 用户认证状态已恢复')
|
||||
} catch (error) {
|
||||
console.error('Failed to parse saved user data or token expired:', error)
|
||||
console.error('Failed to parse saved user data:', error)
|
||||
await logout()
|
||||
}
|
||||
}
|
||||
|
@ -65,11 +65,13 @@
|
||||
<!-- 学员中心子菜单 -->
|
||||
<div class="submenu-container" :class="{ expanded: studentMenuExpanded }">
|
||||
<router-link to="/teacher/student-management/student-library" class="submenu-item"
|
||||
:class="{ active: activeSubNavItem === 'student-library' }" @click="setActiveSubNavItem('student-library')">
|
||||
:class="{ active: activeSubNavItem === 'student-library' }"
|
||||
@click="setActiveSubNavItem('student-library')">
|
||||
<span>学员库</span>
|
||||
</router-link>
|
||||
<router-link to="/teacher/student-management/class-management" class="submenu-item"
|
||||
:class="{ active: activeSubNavItem === 'class-management' }" @click="setActiveSubNavItem('class-management')">
|
||||
:class="{ active: activeSubNavItem === 'class-management' }"
|
||||
@click="setActiveSubNavItem('class-management')">
|
||||
<span>班级管理</span>
|
||||
</router-link>
|
||||
</div>
|
||||
@ -89,6 +91,16 @@
|
||||
<span>个人中心</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- ai助教 -->
|
||||
<div class="ai-container">
|
||||
<router-link to="/teacher/ai-assistant" class="ai-tab" @mouseenter="isAiHovered = true"
|
||||
@mouseleave="isAiHovered = false">
|
||||
<img :src="(isAiActive || isAiHovered) ? '/images/aiAssistant/AI助教1.png' : '/images/aiAssistant/AI助教2.png'"
|
||||
alt="ai" />
|
||||
<span>AI助教</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧路由视图 -->
|
||||
@ -120,6 +132,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ChevronDownOutline } from '@vicons/ionicons5'
|
||||
@ -137,10 +150,13 @@ const studentMenuExpanded = ref(false); // 学员中心菜单展开状态
|
||||
const showTopImage = ref(true); // 控制顶部图片显示/隐藏
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const isAiHovered = ref(false);
|
||||
const isAiActive = computed(() => route.path.includes('/teacher/ai-assistant'));
|
||||
const breadcrumbDisplay = computed(() => {
|
||||
const currentPath = route.path;
|
||||
// 在新建证书页面不显示面包屑
|
||||
if (currentPath.includes('certificate/new')) {
|
||||
let arr = ['certificate/new', 'ai-assistant'];
|
||||
let found = arr.find(item => currentPath.includes(item));
|
||||
if (found) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -632,6 +648,7 @@ watch(route, () => {
|
||||
// 更新激活的导航项
|
||||
const updateActiveNavItem = () => {
|
||||
const path = route.path;
|
||||
console.log('当前路径:', path); // 添加调试信息
|
||||
if (path.includes('course-management')) {
|
||||
activeNavItem.value = 0; // 课程管理
|
||||
} else if (path.includes('student-management')) {
|
||||
@ -655,8 +672,15 @@ const updateActiveNavItem = () => {
|
||||
const arr = ['question-bank', 'exam-library', 'marking-center'];
|
||||
const found = arr.find(item => path.includes(item));
|
||||
activeSubNavItem.value = found || '';
|
||||
} else if(path.includes('message-center')){
|
||||
} else if (path.includes('message-center')) {
|
||||
activeNavItem.value = 5; // 消息中心
|
||||
} else if (path.includes('ai-assistant')) {
|
||||
// AI助教页面,清空所有导航项选中状态
|
||||
console.log('检测到AI助教页面,清空导航选中状态');
|
||||
activeNavItem.value = -1;
|
||||
activeSubNavItem.value = '';
|
||||
examMenuExpanded.value = false;
|
||||
studentMenuExpanded.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -965,6 +989,41 @@ const updateActiveNavItem = () => {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.ai-container {
|
||||
margin: 0 20px;
|
||||
border-top: 1.5px solid #E6E6E6;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.ai-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 54px;
|
||||
padding-left: 16px;
|
||||
color: #666666;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ai-tab.router-link-active,
|
||||
.ai-tab.router-link-exact-active {
|
||||
background: #E2F5FF;
|
||||
color: #0088D1;
|
||||
}
|
||||
|
||||
.ai-tab:hover {
|
||||
background: rgba(2, 136, 209, 0.06);
|
||||
color: #0088D1;
|
||||
}
|
||||
|
||||
.ai-tab img {
|
||||
margin-left: 30px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.router-view-container {
|
||||
flex: 1;
|
||||
padding: 10px 25px;
|
||||
|
574
src/views/teacher/ai/Assistant.vue
Normal file
@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<div class="ai-page">
|
||||
<div class="hero">
|
||||
<h1 class="title">AI教学分析</h1>
|
||||
<p class="subtitle">欢迎使用人工智能课堂教学分析评估系统</p>
|
||||
</div>
|
||||
|
||||
<!-- 功能介绍卡片 -->
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<img class="feature-icon" src="/images/aiAssistant/多维度分析.png" alt="多维度分析" />
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">多维度分析</div>
|
||||
<div class="feature-desc">通过课堂语言、视频、课件等,进行教学动作、教学言语、授课内容、教学情感等全方位教学数据分析</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<img class="feature-icon" src="/images/aiAssistant/智能评估.png" alt="智能评估" />
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">智能评估</div>
|
||||
<div class="feature-desc">围绕多维度评价领域,提供AI驱动的教学评估报告</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<img class="feature-icon" src="/images/aiAssistant/数据支持.png" alt="数据支持" />
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">数据支持</div>
|
||||
<div class="feature-desc">为教研人员提供数据支持,助力教学复盘、优化</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择与上传 tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab" :class="{ active: activeTab === 'library' }" @click="activeTab = 'library'">
|
||||
选择资源库视频
|
||||
</button>
|
||||
<button class="tab" :class="{ active: activeTab === 'upload' }" @click="activeTab = 'upload'">
|
||||
上传分析视频
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 上传/选择区域占位 -->
|
||||
<div class="upload-panel">
|
||||
<div class="upload-cloud">
|
||||
<img src="/images/aiAssistant/upload.png" alt="cloud" />
|
||||
<div class="upload-title">将文件拖到此处,或<span class="link" @click="openFilePicker">点击上传</span></div>
|
||||
<div class="upload-tip">支持mp4、mkv、avi、rmvb、mov,文件需小于2GB,时长不超过60分钟,建议分辨率为720p或1080p
|
||||
每次只能上传一个视频,分析完成后,视频将按每15秒生成一帧进行保存</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常驻隐藏的文件输入框:用于触发系统选择器 -->
|
||||
<input ref="fileInputRef" type="file" multiple accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
|
||||
style="display: none;" @change="onFilesSelected" />
|
||||
|
||||
<!-- 上传模态框 -->
|
||||
<div v-if="showUploadModal" class="modal-mask">
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-content">
|
||||
<!-- 弹框标题 -->
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">视频上传</h3>
|
||||
</div>
|
||||
|
||||
<!-- 文件上传列表 -->
|
||||
<div class="file-list">
|
||||
<table class="file-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>文件名</th>
|
||||
<th>大小</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in uploadList" :key="item.name">
|
||||
<td>
|
||||
<div class="file-info">
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.size }}</td>
|
||||
<td>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :class="item.status" :style="{ width: (item.progress || 0) + '%' }">
|
||||
</div>
|
||||
</div>
|
||||
<span class="status-text" :class="item.status">
|
||||
{{ item.status === 'success' ? `上传成功${item.progress}%` : item.status === 'failed' ?
|
||||
`上传失败${item.progress}%` : `上传中${item.progress}%` }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="modal-footer" v-if="hasAnyFailed">
|
||||
<button class="btn btn-secondary" @click="closeUploadModal">取消</button>
|
||||
<button class="btn btn-primary" @click="retryFailed">重试</button>
|
||||
</div>
|
||||
<div class="modal-footer" v-else>
|
||||
<button class="btn btn-secondary" @click="closeUploadModal">取消</button>
|
||||
<button class="btn btn-analyze" @click="startAnalyze">
|
||||
<img class="btn-icon" src="/images/aiAssistant/points.png" alt="points" />
|
||||
开始分析(32点)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 页面顶部提示横幅(水平居中、垂直上方) -->
|
||||
<div v-if="showPointsToast" class="points-banner" @click="showPointsToast = false">
|
||||
<img class="toast-icon" src="/images/aiAssistant/失败icon.png" alt="error" />
|
||||
<span class="toast-text">智点不足,无法进行分析</span>
|
||||
<router-link class="toast-link" to="/learning-center">积分中心 >></router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { NTooltip } from 'naive-ui'
|
||||
const activeTab = ref<'library' | 'upload'>('library')
|
||||
const showUploadModal = ref(false)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
const hasAnyFailed = ref(false)
|
||||
const showPointsToast = ref(false)
|
||||
const currentPoints = ref(0)
|
||||
const requiredPoints = ref(32)
|
||||
|
||||
type UploadItem = {
|
||||
name: string
|
||||
size: string
|
||||
progress: number
|
||||
status: 'success' | 'failed' | 'uploading'
|
||||
}
|
||||
|
||||
const uploadList = ref<UploadItem[]>([
|
||||
{ name: '视频名称.mp4', size: '172.6MB', progress: 100, status: 'success' },
|
||||
{ name: '视频名称.mp4', size: '172.6MB', progress: 22.3, status: 'failed' }
|
||||
])
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const onFilesSelected = (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const files = input.files
|
||||
if (!files || files.length === 0) return
|
||||
// 这里接入真实上传逻辑前,先构造演示数据
|
||||
uploadList.value = Array.from(files).map((f, idx) => ({
|
||||
name: f.name,
|
||||
size: (f.size / (1024 * 1024)).toFixed(1) + 'MB',
|
||||
progress: idx % 2 === 0 ? 100 : 22.3,
|
||||
status: idx % 2 === 0 ? 'success' : 'failed'
|
||||
}))
|
||||
hasAnyFailed.value = uploadList.value.some(i => i.status === 'failed')
|
||||
showUploadModal.value = true
|
||||
}
|
||||
|
||||
const closeUploadModal = () => { showUploadModal.value = false }
|
||||
const retryFailed = () => {
|
||||
uploadList.value = uploadList.value.map(i => i.status === 'failed' ? { ...i, status: 'success', progress: 100 } : i)
|
||||
hasAnyFailed.value = false
|
||||
}
|
||||
const startAnalyze = () => {
|
||||
if (currentPoints.value < requiredPoints.value) {
|
||||
showPointsToast.value = true
|
||||
window.clearTimeout((startAnalyze as any)._t)
|
||||
; (startAnalyze as any)._t = window.setTimeout(() => { showPointsToast.value = false }, 2500)
|
||||
return
|
||||
}
|
||||
// TODO: 接入“开始分析”业务
|
||||
showUploadModal.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const container = document.querySelector('.router-view-container') as HTMLElement | null
|
||||
if (container) {
|
||||
container.classList.add('no-padding')
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const container = document.querySelector('.router-view-container') as HTMLElement | null
|
||||
if (container) {
|
||||
container.classList.remove('no-padding')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-page {
|
||||
min-height: 100vh;
|
||||
background: url('/images/aiAssistant/bg.png') no-repeat center top / cover;
|
||||
padding: 90px 56px 56px;
|
||||
}
|
||||
|
||||
:global(.router-view-container.no-padding) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 32px;
|
||||
margin: 54px auto 32px;
|
||||
max-width: 1420px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 2px;
|
||||
padding: 24px 24px 30px 24px;
|
||||
flex-direction: column;
|
||||
border: 1.5px solid #FFFFFF;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-right: 16px;
|
||||
object-fit: contain;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
margin-top: -20px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 18px;
|
||||
color: #0088D1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin: 40px 0 30px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
color: #000;
|
||||
padding: 8px 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #0088D1;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -8px;
|
||||
height: 2px;
|
||||
background: #0088D1;
|
||||
}
|
||||
|
||||
.upload-panel {
|
||||
max-width: 1420px;
|
||||
margin: 24px auto;
|
||||
background: none;
|
||||
border: 1.5px dashed #D8D8D8;
|
||||
border-radius: 2px;
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-cloud {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.upload-cloud img {
|
||||
width: 128px;
|
||||
height: 77px;
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
margin-top: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.upload-title .link {
|
||||
color: #0C99DA;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
width: 560px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
/* 覆盖上传模态框样式,参考提供页面 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
width: 1070px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin: 0 24px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1.5px solid #E6E6E6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.file-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
font-size: 14px;
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
.file-table th,
|
||||
.file-table td {
|
||||
padding: 12px 10px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #062333;
|
||||
border: 1.5px solid #F1F3F4;
|
||||
}
|
||||
|
||||
.file-table thead th {
|
||||
color: #062333;
|
||||
font-weight: 600;
|
||||
background: #FCFCFC;
|
||||
}
|
||||
|
||||
.file-table tbody tr {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-type-icon-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100px;
|
||||
height: 9px;
|
||||
background: #F0F0F0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #0288D1;
|
||||
border-radius: 8px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-fill.success {
|
||||
background: #0288D1;
|
||||
}
|
||||
|
||||
.progress-fill.failed {
|
||||
background: #ED1C1C;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
margin-left: 12px;
|
||||
font-size: 14px;
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
.status-text.success {
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
.status-text.failed {
|
||||
color: #ED1C1C;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 24px 35px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
min-width: 98px;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0288D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #E2F5FF;
|
||||
color: #0288D1;
|
||||
border-color: #0288D1;
|
||||
}
|
||||
|
||||
.btn-analyze {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 156px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #33C4FF 0%, #0088D1 100%);
|
||||
}
|
||||
|
||||
.btn-analyze .btn-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.points-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
background: #FFF3F3;
|
||||
border: 1px solid #F3DADA;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toast-text {
|
||||
color: #ED1C1C;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toast-link {
|
||||
color: #000000;
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.points-banner {
|
||||
position: fixed;
|
||||
min-width: 560px;
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #FFF3F3;
|
||||
border: 1px solid #F3DADA;
|
||||
color: #ED1C1C;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
z-index: 2100;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0a8bc6;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: #ccc;
|
||||
}
|
||||
</style>
|
3
src/views/teacher/ai/AssistantDetail.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>查看分析页面</div>
|
||||
</template>
|
8
src/vite-env.d.ts
vendored
@ -5,3 +5,11 @@ declare module '*.vue' {
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
// Vue 3 全局类型声明
|
||||
declare global {
|
||||
const defineProps: typeof import('vue')['defineProps']
|
||||
const defineEmits: typeof import('vue')['defineEmits']
|
||||
const defineExpose: typeof import('vue')['defineExpose']
|
||||
const defineOptions: typeof import('vue')['defineOptions']
|
||||
}
|