feat(frontend): Element Plus 改按需引入,首屏 425.1->158.8KB(R36)

- vite.config.js: 新增 unplugin-vue-components + ElementPlusResolver;
  optimizeDeps.include 动态生成 118 项无扩展名 specifier(glob/.mjs 皆静默失效)
- main.js: 删 app.use(ElementPlus) 与全量 CSS;改 provideGlobalConfig(locale, app, true);
  显式引入 4 个函数式组件样式(message/message-box/notification/loading)
- views/Upload.vue: 3 处 h(resolveComponent('el-xxx')) 改显式组件引用;清 2 处死导入
- devDep: unplugin-vue-components@^32.1.0
- .gitignore: 忽略 vite.config.js.timestamp-*.mjs

验证:dist 渲染 9/9 · dist 登录后 9/9 · dev 通道致命错误 0/告警 0
瘦身:首屏 425.1->158.8KB(-62.6%);主包 JS 1112.9->281.09KB
This commit is contained in:
2026-09-29 11:44:15 +08:00
parent 042e0a8e1f
commit ea350fb2e0
7 changed files with 471 additions and 16 deletions

20
src/components.d.ts vendored
View File

@@ -12,39 +12,51 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

View File

@@ -1,10 +1,18 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
// Element Plus 按需引入(见 vite.config.js 的 Components 插件):
// 不再全量注册组件,也不再引入 element-plus/dist/index.css。
// 此处只保留「语言包」与「全局 locale 设置」两项职责。
import { provideGlobalConfig } from 'element-plus'
// Element Plus 中文语言包
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// Element Plus 样式
import 'element-plus/dist/index.css'
// 🔴 函数式组件不经过模板编译,按需插件(静态扫描)无法为它们注入样式 ⇒ 必须显式引入。
// 这四类在项目中的调用次数:ElMessage 147 / ElMessageBox 23 / ElNotification 4 / ElLoading 2。
// 漏掉任一条的后果是「弹窗能弹出但无样式」(裸文本),构建不报错,只有真浏览器可见。
import 'element-plus/es/components/message/style/css'
import 'element-plus/es/components/message-box/style/css'
import 'element-plus/es/components/notification/style/css'
import 'element-plus/es/components/loading/style/css'
// 全局样式(含 Element Plus 主题色变量,防止 getThemeColors 解构 undefined 报错)
import './styles/global.css'
@@ -34,10 +42,15 @@ app.use(router)
// 启动主动 token 续期:活跃用户不会因 token 过期被强制踢出登录
initProactiveTokenRefresh()
// 注册 Element Plus(使用中文语言包)
app.use(ElementPlus, {
locale: zhCn,
})
// 设置 Element Plus 全局配置(中文语言包)。
// 与原 app.use(ElementPlus, { locale: zhCn }) 语义等价:Element Plus 的 install
// 在注册完组件后做的正是 provideGlobalConfig(options, app, true)
// (见 node_modules/element-plus/es/make-installer.mjs)。
// 第三个参数 global=true 是关键 —— 它把 locale 写入 globalConfig,
// 使 ElMessageBox / ElPagination 等「脱离组件树的函数式 API」也能取到中文文案;
// 少了它,确认框按钮会退回英文 OK / Cancel。
// 组件注册职责已移交 vite 的按需插件,此处不再注册任何组件。
provideGlobalConfig({ locale: zhCn }, app, true)
// 全局指令 v-guard:为按钮/可点击元素提供「防重入 + 节流」,
// 防止狂点触发大量并发请求打爆连接池(2026-08-10 账号列表超时根因)。

View File

@@ -136,7 +136,7 @@
* 6. 上传历史列表
* 7. 教师组双上传区(教学方案 + 说课PPT)
*/
import { ref, computed, onMounted, onUnmounted, h, resolveComponent } from 'vue'
import { ref, computed, onMounted, onUnmounted, h } from 'vue'
import { ElMessage, ElMessageBox, ElUpload, ElProgress, ElButton, ElTag, ElIcon } from 'element-plus'
import { ArrowLeft, HomeFilled, Clock, UploadFilled, Delete } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth'
@@ -305,7 +305,7 @@ onUnmounted(() => {
* file-upload-area 是一个内联组件
* 为了保持 Vue SFC 的简洁性,我们用 defineComponent
*/
import { defineComponent, ref, reactive, h, resolveComponent, computed } from 'vue'
import { defineComponent, ref, reactive, h, computed } from 'vue'
import { ElMessage, ElUpload, ElProgress, ElButton, ElTag, ElIcon } from 'element-plus'
import { UploadFilled, Delete } from '@element-plus/icons-vue'
import * as uploadApi from '@/api/upload'
@@ -455,7 +455,7 @@ const FileUploadArea = defineComponent({
e.currentTarget.previousElementSibling?.click()
},
}, [
h(resolveComponent('el-icon'), { size: 48 }, () => h(UploadFilled)),
h(ElIcon, { size: 48 }, () => h(UploadFilled)),
h('div', { class: 'drag-text' }, '拖拽文件到此处或 '),
h('em', null, '点击上传'),
h('div', { class: 'drag-hint' },
@@ -488,7 +488,7 @@ const FileUploadArea = defineComponent({
// 上传进度
uploading.value ? h('div', { class: 'upload-progress' }, [
h(resolveComponent('el-progress'), {
h(ElProgress, {
percentage: uploadPercent.value,
status: uploadPercent.value >= 100 ? 'success' : undefined,
}),
@@ -496,7 +496,7 @@ const FileUploadArea = defineComponent({
// 已上传文件信息
uploadedInfo.value ? h('div', { class: 'uploaded-card' }, [
h(resolveComponent('el-tag'), { type: 'success', size: 'small' }, () => '上传成功'),
h(ElTag, { type: 'success', size: 'small' }, () => '上传成功'),
h('div', { class: 'uploaded-name' }, uploadedInfo.value.fileName),
h('div', { class: 'uploaded-meta' }, [
h('span', null, formatFileSize(uploadedInfo.value.fileSize)),