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 |
65
src/App.vue
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, computed } from 'vue'
|
import { onMounted, computed, ref } from 'vue'
|
||||||
import { RouterView, useRoute } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
@ -65,19 +65,32 @@ const themeOverrides: GlobalThemeOverrides = {
|
|||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const isInitializing = ref(true)
|
||||||
|
|
||||||
// 检查是否为登录页面
|
// 检查是否为登录页面
|
||||||
const isLoginPage = computed(() => route.name === 'Login')
|
const isLoginPage = computed(() => route.name === 'Login')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
// 初始化用户认证状态
|
// 初始化用户认证状态
|
||||||
userStore.initializeAuth()
|
await userStore.initializeAuth()
|
||||||
|
} finally {
|
||||||
|
// 无论初始化成功与否,都显示页面
|
||||||
|
isInitializing.value = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<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>
|
<n-dialog-provider>
|
||||||
<!-- 登录页面不使用 AppLayout,但需要 message provider -->
|
<!-- 登录页面不使用 AppLayout,但需要 message provider -->
|
||||||
<template v-if="isLoginPage">
|
<template v-if="isLoginPage">
|
||||||
@ -113,7 +126,8 @@ html {
|
|||||||
|
|
||||||
/* 移除全屏相关样式,使用正常布局 */
|
/* 移除全屏相关样式,使用正常布局 */
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -184,4 +198,45 @@ body {
|
|||||||
.d-block {
|
.d-block {
|
||||||
display: block !important;
|
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>
|
</style>
|
||||||
|
@ -28,7 +28,7 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i
|
|||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/jeecgboot',
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/jeecgboot',
|
||||||
timeout: 30000, // 增加到30秒
|
timeout: 10000, // 减少到10秒,避免长时间等待
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -1,14 +1,263 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="personal-center">
|
||||||
<h1>个人中心</h1>
|
<!-- 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-dropdown" v-if="visible">
|
<div class="search-dropdown" v-if="isVisible">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<!-- 热门搜索 -->
|
<!-- 热门搜索 -->
|
||||||
<div class="search-section">
|
<div class="search-section">
|
||||||
@ -62,6 +62,9 @@ interface Props {
|
|||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
// 使用props.visible来避免未使用警告
|
||||||
|
const isVisible = computed(() => props.visible)
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
search: [keyword: string]
|
search: [keyword: string]
|
||||||
@ -159,7 +162,7 @@ const saveRecentSearch = (keyword: string) => {
|
|||||||
const storageKey = `recent_search_${userId}`
|
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条
|
recentSearchList.value = [keyword, ...filtered].slice(0, 10) // 最多保存10条
|
||||||
|
|
||||||
// 保存到localStorage
|
// 保存到localStorage
|
||||||
|
@ -59,6 +59,8 @@ import NotificationManagement from '@/views/teacher/course/NotificationManagemen
|
|||||||
import GeneralManagement from '@/views/teacher/course/GeneralManagement.vue'
|
import GeneralManagement from '@/views/teacher/course/GeneralManagement.vue'
|
||||||
import UserAgreement from '@/views/UserAgreement.vue'
|
import UserAgreement from '@/views/UserAgreement.vue'
|
||||||
import RecycleBin from '@/views/teacher/resource/RecycleBin.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'
|
import HomeworkLibrary from '@/views/teacher/course/HomeworkLibrary.vue'
|
||||||
@ -243,7 +245,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: '证书管理' }
|
meta: { title: '证书管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'certificate/detail/:id',
|
path: 'certificate/detail/:certificateId',
|
||||||
name: 'CertificateDetail',
|
name: 'CertificateDetail',
|
||||||
component: () => import('@/views/teacher/certificate/CertificateDetail.vue'),
|
component: () => import('@/views/teacher/certificate/CertificateDetail.vue'),
|
||||||
meta: { title: '证书详情' }
|
meta: { title: '证书详情' }
|
||||||
@ -268,7 +270,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: '添加讨论' }
|
meta: { title: '添加讨论' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'comment/:id',
|
path: 'comment/:commentId',
|
||||||
name: 'CommentView',
|
name: 'CommentView',
|
||||||
component: CommentView,
|
component: CommentView,
|
||||||
meta: { title: '评论详情' }
|
meta: { title: '评论详情' }
|
||||||
@ -317,6 +319,18 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: RecycleBin,
|
component: RecycleBin,
|
||||||
meta: { title: '回收站' }
|
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',
|
path: 'student-management',
|
||||||
name: 'StudentManagement',
|
name: 'StudentManagement',
|
||||||
|
@ -181,10 +181,11 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
user.value = JSON.parse(savedUser)
|
user.value = JSON.parse(savedUser)
|
||||||
token.value = savedToken
|
token.value = savedToken
|
||||||
|
|
||||||
// 验证token是否仍然有效,并强制刷新用户信息
|
// 不强制刷新用户信息,避免API超时导致白屏
|
||||||
await getCurrentUser(true)
|
// 如果需要验证token有效性,可以在用户操作时进行
|
||||||
|
console.log('✅ 用户认证状态已恢复')
|
||||||
} catch (error) {
|
} 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()
|
await logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,11 +65,13 @@
|
|||||||
<!-- 学员中心子菜单 -->
|
<!-- 学员中心子菜单 -->
|
||||||
<div class="submenu-container" :class="{ expanded: studentMenuExpanded }">
|
<div class="submenu-container" :class="{ expanded: studentMenuExpanded }">
|
||||||
<router-link to="/teacher/student-management/student-library" class="submenu-item"
|
<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>
|
<span>学员库</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/teacher/student-management/class-management" class="submenu-item"
|
<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>
|
<span>班级管理</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@ -89,6 +91,16 @@
|
|||||||
<span>个人中心</span>
|
<span>个人中心</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧路由视图 -->
|
<!-- 右侧路由视图 -->
|
||||||
@ -120,6 +132,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// @ts-nocheck
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ChevronDownOutline } from '@vicons/ionicons5'
|
import { ChevronDownOutline } from '@vicons/ionicons5'
|
||||||
@ -137,10 +150,13 @@ const studentMenuExpanded = ref(false); // 学员中心菜单展开状态
|
|||||||
const showTopImage = ref(true); // 控制顶部图片显示/隐藏
|
const showTopImage = ref(true); // 控制顶部图片显示/隐藏
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isAiHovered = ref(false);
|
||||||
|
const isAiActive = computed(() => route.path.includes('/teacher/ai-assistant'));
|
||||||
const breadcrumbDisplay = computed(() => {
|
const breadcrumbDisplay = computed(() => {
|
||||||
const currentPath = route.path;
|
const currentPath = route.path;
|
||||||
// 在新建证书页面不显示面包屑
|
let arr = ['certificate/new', 'ai-assistant'];
|
||||||
if (currentPath.includes('certificate/new')) {
|
let found = arr.find(item => currentPath.includes(item));
|
||||||
|
if (found) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -632,6 +648,7 @@ watch(route, () => {
|
|||||||
// 更新激活的导航项
|
// 更新激活的导航项
|
||||||
const updateActiveNavItem = () => {
|
const updateActiveNavItem = () => {
|
||||||
const path = route.path;
|
const path = route.path;
|
||||||
|
console.log('当前路径:', path); // 添加调试信息
|
||||||
if (path.includes('course-management')) {
|
if (path.includes('course-management')) {
|
||||||
activeNavItem.value = 0; // 课程管理
|
activeNavItem.value = 0; // 课程管理
|
||||||
} else if (path.includes('student-management')) {
|
} else if (path.includes('student-management')) {
|
||||||
@ -655,8 +672,15 @@ const updateActiveNavItem = () => {
|
|||||||
const arr = ['question-bank', 'exam-library', 'marking-center'];
|
const arr = ['question-bank', 'exam-library', 'marking-center'];
|
||||||
const found = arr.find(item => path.includes(item));
|
const found = arr.find(item => path.includes(item));
|
||||||
activeSubNavItem.value = found || '';
|
activeSubNavItem.value = found || '';
|
||||||
} else if(path.includes('message-center')){
|
} else if (path.includes('message-center')) {
|
||||||
activeNavItem.value = 5; // 消息中心
|
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;
|
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 {
|
.router-view-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px 25px;
|
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>
|
const component: DefineComponent<{}, {}, any>
|
||||||
export default component
|
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']
|
||||||
|
}
|