+
+ {{ error ? '设置页面加载失败,请刷新后重试。' : '正在加载设置页面…' }}
+
+
+
+
+
+
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index b6eb9cf..fbdd0f8 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -74,13 +74,27 @@ export default defineConfig(({ mode }) => {
build: {
outDir: fileURLToPath(new URL('../out/frontend', import.meta.url)),
emptyOutDir: false,
+ manifest: true,
rollupOptions: {
output: {
- manualChunks: {
- 'vue-core': ['vue', 'vue-router', 'pinia'],
- 'element-plus': ['element-plus'],
- 'editor': ['@wangeditor/editor', '@wangeditor/editor-for-vue'],
- 'icons': ['@element-plus/icons-vue', 'vue-icons-plus']
+ manualChunks(id) {
+ const modulePath = id.replaceAll('\\', '/')
+ if (!modulePath.includes('/node_modules/')) return
+ if (/\/node_modules\/(?:@vue\/|vue\/|vue-router\/|pinia\/)/.test(modulePath)) {
+ return 'vue-core'
+ }
+ // 编辑器仅在写信页面加载;按包拆分其依赖,避免单个异步块超过浏览器缓存阈值。
+ const packagePath = modulePath.split('/node_modules/')[1]
+ const packageParts = packagePath.split('/')
+ const packageName = packageParts[0].startsWith('@')
+ ? `${packageParts[0]}-${packageParts[1]}`
+ : packageParts[0]
+ if (packageName.startsWith('@wangeditor-')) {
+ return `editor-${packageName.slice('@wangeditor-'.length)}`
+ }
+ if (['@uppy-core', '@uppy-xhr-upload', 'slate', 'snabbdom'].includes(packageName)) {
+ return `editor-${packageName.replace(/^@uppy-/, '')}`
+ }
}
}
}
diff --git a/test/frontend/output-paths.test.mjs b/test/frontend/output-paths.test.mjs
index fd54219..c2edef8 100644
--- a/test/frontend/output-paths.test.mjs
+++ b/test/frontend/output-paths.test.mjs
@@ -28,6 +28,18 @@ test('前端构建必须自动同步 Go 嵌入资源', async () => {
assert.match(await read('plugins/pmail_telegram_push/vite.config.ts'), /\.\.\/\.\.\/out\/plugins\/telegram-ui/)
})
+test('前端入口按需加载 Element Plus,并延迟设置页和写信编辑器', async () => {
+ const main = await read('frontend/src/main.js')
+ const vite = await read('frontend/vite.config.js')
+ const header = await read('frontend/src/views/components/HomeHeader/HomeHeader.js')
+ assert.doesNotMatch(main, /app\.use\(ElementPlus\)/)
+ assert.doesNotMatch(main, /element-plus\/dist\/index\.css/)
+ assert.match(main, /element-services\.css/)
+ assert.match(vite, /manualChunks\(id\)/)
+ assert.match(header, /defineAsyncComponent/)
+ assert.match(header, /import\('\.\.\/ConfigSettings\/ConfigSettings\.vue'\)/)
+})
+
test('开发启动必须固定运行配置根目录并集中保存日志', async () => {
const content = await read('deploy/start-dev.ps1')
assert.match(content, /\$env:SMAIL_ROOT = \$root\r?\n/)
@@ -38,6 +50,9 @@ test('开发启动必须固定运行配置根目录并集中保存日志', async
assert.match(content, /out\\backend\\smail-dev\.exe/)
assert.match(content, /test-log\\runtime/)
assert.match(content, /-WindowStyle Hidden/)
+ assert.match(content, /\[switch\]\$SkipBuild/)
+ assert.match(content, /--dev-disable-admin-reset/)
+ assert.match(content, /默认先编译当前工作区/)
assert.ok(content.charCodeAt(0) === 0xfeff, 'Windows PowerShell 5 脚本应有 UTF-8 BOM')
})
diff --git a/test/frontend/settings-menu.test.mjs b/test/frontend/settings-menu.test.mjs
index 2e53d17..2c3cc84 100644
--- a/test/frontend/settings-menu.test.mjs
+++ b/test/frontend/settings-menu.test.mjs
@@ -36,13 +36,12 @@ test('一级菜单保留能力过滤和当前分类标题联动', () => {
const source = read('HomeHeader/HomeHeader.js')
const names = [
'EpMenu', 'Lock', 'Connection', 'Setting', 'FolderOpened', 'Filter', 'User', 'Grid',
- 'SecuritySettings', 'ThirdPartyAuthorization', 'ConfigSettings', 'GroupSettings',
- 'RuleSettings', 'UserManagement', 'PluginSettings', 'BrandLogo'
+ 'SettingsPageStatus', 'BrandLogo'
]
const createPage = compileFunction(
source.replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage')
.replace(/export \{[\s\S]*?\}\s*$/, '') + '\nreturn usePage()',
- ['computed', 'ref', 'onMounted', 'onUnmounted', 'window', 'useGlobalStatusStore', 'lang', ...names]
+ ['computed', 'ref', 'onMounted', 'onUnmounted', 'window', 'useGlobalStatusStore', 'lang', 'defineAsyncComponent', ...names]
)
const capabilities = reactive(new Set(['password.change_own', 'user.read']))
const store = reactive({
@@ -50,7 +49,7 @@ test('一级菜单保留能力过滤和当前分类标题联动', () => {
hasCapability: value => capabilities.has(value)
})
const page = createPage(computed, ref, () => {}, () => {}, { innerWidth: 1440 }, () => store,
- { security: '安全' }, ...names.map(name => ({ name })))
+ { security: '安全' }, () => ({}), ...names.map(name => ({ name })))
assert.deepEqual(page.settingsMenu.value.map(item => item.key), ['security', 'third-party', 'group', 'rule'])
assert.equal(page.settingsMenu.value.find(item => item.key === 'third-party').label, '邮件客户端')
assert.equal(page.activeSettings.value.label, '安全')