fix: 修复白屏问题、路由冲突和TypeScript错误,打包问题,调整tab悬停样式

This commit is contained in:
QDKF 2025-09-10 16:14:51 +08:00
parent b283f4e3c7
commit 2a3e6ccd91
26 changed files with 989 additions and 23 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -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>

View File

@ -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',
},

View File

@ -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>

View File

@ -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

View File

@ -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',

View File

@ -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()
}
}

View File

@ -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;

View 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">支持mp4mkvavirmvbmov文件需小于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>

View File

@ -0,0 +1,3 @@
<template>
<div>查看分析页面</div>
</template>

8
src/vite-env.d.ts vendored
View File

@ -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']
}