Merge branch 'xinnan-tech:main' into bug_fix_funshine

This commit is contained in:
Guijie Hong
2025-04-15 16:11:18 +08:00
committed by GitHub
32 changed files with 4381 additions and 199 deletions
+2 -2
View File
@@ -42,7 +42,7 @@ config WEBSOCKET_URL
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
找到`OTA_VERSION_URL``default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8000/xiaozhi/ota/`,就把内容改成这个。
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
修改前:
```
@@ -56,7 +56,7 @@ config OTA_VERSION_URL
```
config OTA_VERSION_URL
string "OTA Version URL"
default "http://192.168.1.25:8000/xiaozhi/ota/"
default "http://192.168.1.25:8002/xiaozhi/ota/"
help
The application will access this URL to check for updates.
```
@@ -84,6 +84,16 @@ public interface Constant {
*/
String SERVER_SECRET = "server.secret";
/**
* 是否允许用户注册
*/
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
/**
* 下发六位验证码时显示的控制面板地址
*/
String SERVER_FRONTED_URL = "server.fronted_url";
/**
* 路径分割符
*/
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.modules.agent.dao.AgentTemplateDao;
@@ -39,29 +40,30 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
@Override
public void updateDefaultTemplateModelId(String modelType, String modelId) {
modelType = modelType.toUpperCase();
AgentTemplateEntity defaultTemplate = getDefaultTemplate();
if (defaultTemplate != null) {
switch (modelType) {
case "ASR":
defaultTemplate.setAsrModelId(modelId);
break;
case "VAD":
defaultTemplate.setVadModelId(modelId);
break;
case "LLM":
defaultTemplate.setLlmModelId(modelId);
break;
case "TTS":
defaultTemplate.setTtsModelId(modelId);
break;
case "Memory":
defaultTemplate.setMemModelId(modelId);
break;
case "Intent":
defaultTemplate.setIntentModelId(modelId);
break;
}
this.updateById(defaultTemplate);
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
switch (modelType) {
case "ASR":
wrapper.set("asr_model_id", modelId);
break;
case "VAD":
wrapper.set("vad_model_id", modelId);
break;
case "LLM":
wrapper.set("llm_model_id", modelId);
break;
case "TTS":
wrapper.set("tts_model_id", modelId);
wrapper.set("tts_voice_id", null);
break;
case "MEMORY":
wrapper.set("mem_model_id", modelId);
break;
case "INTENT":
wrapper.set("intent_model_id", modelId);
break;
}
wrapper.ge("sort", 0);
update(wrapper);
}
}
@@ -9,7 +9,6 @@ import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@@ -34,6 +33,7 @@ import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserUtilService;
@Service
@@ -49,11 +49,11 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
// 添加构造函数来初始化 deviceMapper
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
SysParamsService sysParamsService,
RedisTemplate<String, Object> redisTemplate) {
this.deviceDao = deviceDao;
this.sysUserUtilService = sysUserUtilService;
this.frontedUrl = frontedUrl;
this.frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
this.redisTemplate = redisTemplate;
}
@@ -135,6 +135,7 @@ public class ModelController {
}
// 将其他模型设置为非默认
modelConfigService.setDefaultModel(entity.getModelType(), 0);
entity.setIsEnabled(1);
entity.setIsDefault(1);
modelConfigService.updateById(entity);
@@ -42,6 +42,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
List<ModelConfigEntity> entities = modelConfigDao.selectList(
new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType)
.eq("is_enabled", 1)
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
.select("id", "model_name"));
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
@@ -77,6 +77,9 @@ public class LoginController {
@PostMapping("/register")
@Operation(summary = "注册")
public Result<Void> register(@RequestBody LoginDTO login) {
if (!sysUserService.getAllowUserRegister()) {
throw new RenException("当前不允许普通用户注册");
}
// 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
if (!validate) {
@@ -119,6 +122,7 @@ public class LoginController {
public Result<Map<String, Object>> pubConfig() {
Map<String, Object> config = new HashMap<>();
config.put("version", "0.3.3");
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
return new Result<Map<String, Object>>().ok(config);
}
}
@@ -27,6 +27,7 @@ import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.common.validator.group.AddGroup;
import xiaozhi.common.validator.group.DefaultGroup;
import xiaozhi.common.validator.group.UpdateGroup;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.sys.dto.SysParamsDTO;
import xiaozhi.modules.sys.service.SysParamsService;
@@ -42,6 +43,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
@AllArgsConstructor
public class SysParamsController {
private final SysParamsService sysParamsService;
private final ConfigService configService;
@GetMapping("page")
@Operation(summary = "分页")
@@ -77,7 +79,7 @@ public class SysParamsController {
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
sysParamsService.save(dto);
configService.getConfig(false);
return new Result<Void>();
}
@@ -90,7 +92,7 @@ public class SysParamsController {
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
sysParamsService.update(dto);
configService.getConfig(false);
return new Result<Void>();
}
@@ -103,7 +105,7 @@ public class SysParamsController {
AssertUtils.isArrayEmpty(ids, "id");
sysParamsService.delete(ids);
configService.getConfig(false);
return new Result<Void>();
}
}
@@ -65,4 +65,11 @@ public interface SysUserService extends BaseService<SysUserEntity> {
* @param userIds 用户ID数组
*/
void changeStatus(Integer status, String[] userIds);
/**
* 获取是否允许用户注册
*
* @return 是否允许用户注册
*/
boolean getAllowUserRegister();
}
@@ -30,6 +30,7 @@ import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.sys.vo.AdminPageUserVO;
@@ -45,6 +46,8 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
private final AgentService agentService;
private final SysParamsService sysParamsService;
@Override
public SysUserDTO getByUsername(String username) {
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
@@ -205,4 +208,17 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
updateById(entity);
}
}
@Override
public boolean getAllowUserRegister() {
String allowUserRegister = sysParamsService.getValue(Constant.SERVER_ALLOW_USER_REGISTER, true);
if (allowUserRegister.equals("true")) {
return true;
}
Long userCount = baseDao.selectCount(new QueryWrapper<SysUserEntity>());
if (userCount == 0) {
return true;
}
return false;
}
}
@@ -63,7 +63,4 @@ mybatis-plus:
configuration-properties:
prefix:
blobType: BLOB
boolValue: TRUE
app:
fronted-url: http://localhost:8001
boolValue: TRUE
@@ -15,4 +15,9 @@ INSERT INTO `ai_tts_voice` VALUES
('TTS_EdgeTTS0008', 'TTS_EdgeTTS', 'EdgeTTS女声-陕西小妮', 'zh-CN-shaanxi-XiaoniNeural', '陕西', NULL, NULL, 1, NULL, NULL, NULL, NULL),
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
-- 增加是否允许用户注册参数
delete from `sys_params` where id in (103,104);
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (103, 'server.allow_user_register', 'false', 'boolean', 1, '是否运行管理员以外的人注册');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (104, 'server.fronted_url', 'http://xiaozhi.server.com', 'string', 1, '下发六位验证码时显示的控制面板地址');
@@ -52,9 +52,9 @@ databaseChangeLog:
encoding: utf8
path: classpath:db/changelog/202504112058.sql
- changeSet:
id: 202504131543
id: 202504151205
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504131543.sql
path: classpath:db/changelog/202504151205.sql
+3 -1
View File
@@ -1 +1,3 @@
VUE_APP_API_BASE_URL=/xiaozhi
VUE_APP_API_BASE_URL=/xiaozhi
# 是否开启CDN
VUE_APP_USE_CDN=false
+3249 -11
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -31,7 +31,8 @@
"sass": "^1.32.7",
"sass-loader": "^12.0.0",
"vue-template-compiler": "^2.6.14",
"webpack-bundle-analyzer": "^4.10.2"
"webpack-bundle-analyzer": "^4.10.2",
"workbox-webpack-plugin": "^7.3.0"
},
"browserslist": [
"> 1%",
@@ -43,4 +44,4 @@
"*.scss",
"*.vue"
]
}
}
+2 -2
View File
@@ -9,7 +9,7 @@
<title>
<%= process.env.VUE_APP_TITLE %>
</title>
<% if (htmlWebpackPlugin.options.cdn) { %>
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
<% } %>
@@ -23,7 +23,7 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<% if (htmlWebpackPlugin.options.cdn) { %>
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
+70
View File
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>离线模式 - 小智控制台</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f7fa;
color: #333;
}
.container {
text-align: center;
padding: 2rem;
background: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
max-width: 80%;
}
h1 {
margin-bottom: 1rem;
color: #409EFF;
}
p {
margin-bottom: 1.5rem;
line-height: 1.6;
}
.btn {
background-color: #409EFF;
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #337ecc;
}
.icon {
font-size: 4rem;
margin-bottom: 1rem;
color: #409EFF;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">📶</div>
<h1>您当前处于离线模式</h1>
<p>看起来您的网络连接有问题,无法连接到小智控制台服务器。</p>
<p>部分已缓存的内容和静态资源可能仍然可用,但功能可能受到限制。</p>
<button class="btn" onclick="window.location.reload()">重新尝试连接</button>
</div>
<script>
// 检测网络状态变化
window.addEventListener('online', () => {
window.location.reload();
});
</script>
</body>
</html>
+115
View File
@@ -1,6 +1,7 @@
<template>
<div id="app">
<router-view />
<cache-viewer v-if="isCDNEnabled" :visible.sync="showCacheViewer" />
</div>
</template>
@@ -45,4 +46,118 @@ nav {
}
</style>
<script>
import CacheViewer from '@/components/CacheViewer.vue';
import { logCacheStatus } from '@/utils/cacheViewer';
export default {
components: {
CacheViewer
},
data() {
return {
showCacheViewer: false,
isCDNEnabled: process.env.VUE_APP_USE_CDN === 'true'
};
},
mounted() {
// 只有在启用CDN时才添加相关事件和功能
if (this.isCDNEnabled) {
// 添加全局快捷键Alt+C用于显示缓存查看器
document.addEventListener('keydown', this.handleKeyDown);
// 在全局对象上添加缓存检查方法,便于调试
window.checkCDNCacheStatus = () => {
this.showCacheViewer = true;
};
// 在控制台输出提示信息
console.info(
'%c[小智服务] CDN缓存检查工具已加载',
'color: #409EFF; font-weight: bold;'
);
console.info(
'按下 Alt+C 组合键或在控制台运行 checkCDNCacheStatus() 可以查看CDN缓存状态'
);
// 检查Service Worker状态
this.checkServiceWorkerStatus();
} else {
console.info(
'%c[小智服务] CDN模式已禁用,使用本地打包资源',
'color: #67C23A; font-weight: bold;'
);
}
},
beforeDestroy() {
// 只有在启用CDN时才需要移除事件监听
if (this.isCDNEnabled) {
document.removeEventListener('keydown', this.handleKeyDown);
}
},
methods: {
handleKeyDown(e) {
// Alt+C 快捷键
if (e.altKey && e.key === 'c') {
this.showCacheViewer = true;
}
},
async checkServiceWorkerStatus() {
// 检查Service Worker是否已注册
if ('serviceWorker' in navigator) {
try {
const registrations = await navigator.serviceWorker.getRegistrations();
if (registrations.length > 0) {
console.info(
'%c[小智服务] Service Worker已注册',
'color: #67C23A; font-weight: bold;'
);
// 输出缓存状态到控制台
setTimeout(async () => {
const hasCaches = await logCacheStatus();
if (!hasCaches) {
console.info(
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
'color: #E6A23C; font-weight: bold;'
);
// 开发环境下提供额外提示
if (process.env.NODE_ENV === 'development') {
console.info(
'%c[小智服务] 在开发环境中,Service Worker可能无法正常初始化缓存',
'color: #E6A23C; font-weight: bold;'
);
console.info('请尝试以下方法检查Service Worker是否生效:');
console.info('1. 在开发者工具的Application/Application标签页中查看Service Worker状态');
console.info('2. 在开发者工具的Application/Cache/Cache Storage中查看缓存内容');
console.info('3. 使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能');
}
}
}, 2000);
} else {
console.info(
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
'color: #F56C6C; font-weight: bold;'
);
if (process.env.NODE_ENV === 'development') {
console.info(
'%c[小智服务] 在开发环境中,这是正常现象',
'color: #E6A23C; font-weight: bold;'
);
console.info('Service Worker通常只在生产环境中生效');
console.info('要测试Service Worker功能:');
console.info('1. 运行npm run build构建生产版本');
console.info('2. 通过HTTP服务器访问构建后的页面');
}
}
} catch (error) {
console.error('检查Service Worker状态失败:', error);
}
} else {
console.warn('当前浏览器不支持Service WorkerCDN资源缓存功能不可用');
}
}
}
};
</script>
@@ -1,6 +1,6 @@
<template>
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center custom-class="custom-dialog" :show-close="false"
class="center-dialog">
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center
custom-class="custom-dialog" :show-close="false" class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
添加模型
@@ -18,7 +18,7 @@
<span style="margin-right: 8px;">是否启用</span>
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
</div>
<div style="display: flex; align-items: center;">
<div style="display: none; align-items: center;">
<span style="margin-right: 8px;">设为默认</span>
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
</div>
@@ -65,18 +65,10 @@
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item
v-for="field in row"
:key="field.prop"
:label="field.label"
:prop="field.prop"
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
style="flex: 1;">
<el-input
v-model="formData.configJson[field.prop]"
:placeholder="field.placeholder"
:type="field.type || 'text'"
class="custom-input-bg"
:show-password="field.type === 'password'">
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
</el-input>
</el-form-item>
</div>
@@ -123,7 +115,7 @@ export default {
watch: {
visible(val) {
this.dialogVisible = val;
if(val) {
if (val) {
this.initConfigJson();
} else {
this.resetForm();
@@ -0,0 +1,207 @@
<template>
<el-dialog
title="CDN资源缓存状态"
:visible.sync="visible"
width="70%"
:before-close="handleClose"
>
<div v-if="isLoading" class="loading-container">
<p>正在加载缓存信息...</p>
</div>
<div v-else>
<div v-if="!cacheAvailable" class="no-cache-message">
<i class="el-icon-warning-outline"></i>
<p>您的浏览器不支持Cache API或Service Worker未安装</p>
<el-button type="primary" @click="refreshPage">刷新页面</el-button>
</div>
<div v-else>
<el-alert
v-if="cacheData.totalCached === 0"
title="未发现缓存的CDN资源"
type="warning"
:closable="false"
show-icon
>
<p>Service Worker可能尚未完成初始化或缓存尚未建立请刷新页面或等待一会后再试</p>
</el-alert>
<div v-else>
<el-alert
title="CDN资源缓存状态"
type="success"
:closable="false"
show-icon
>
共发现 {{ cacheData.totalCached }} 个缓存资源
</el-alert>
<h3>JavaScript 资源 ({{ cacheData.js.length }})</h3>
<el-table :data="cacheData.js" stripe style="width: 100%">
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
<el-table-column prop="cached" label="状态" width="100">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
<el-tag type="danger" v-else>未缓存</el-tag>
</template>
</el-table-column>
</el-table>
<h3>CSS 资源 ({{ cacheData.css.length }})</h3>
<el-table :data="cacheData.css" stripe style="width: 100%">
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
<el-table-column prop="cached" label="状态" width="100">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
<el-tag type="danger" v-else>未缓存</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button>
<el-button type="danger" @click="clearCache">清除缓存</el-button>
</span>
</el-dialog>
</template>
<script>
import {
getCacheNames,
checkCdnCacheStatus,
clearAllCaches,
logCacheStatus
} from '../utils/cacheViewer';
export default {
name: 'CacheViewer',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
isLoading: true,
cacheAvailable: false,
cacheData: {
css: [],
js: [],
totalCached: 0,
totalNotCached: 0
}
};
},
watch: {
visible(newVal) {
if (newVal) {
this.loadCacheData();
}
}
},
methods: {
async loadCacheData() {
this.isLoading = true;
try {
// 先检查是否支持缓存API
if (!('caches' in window)) {
this.cacheAvailable = false;
this.isLoading = false;
return;
}
// 检查是否有Service Worker缓存
const cacheNames = await getCacheNames();
this.cacheAvailable = cacheNames.length > 0;
if (this.cacheAvailable) {
// 获取CDN缓存状态
this.cacheData = await checkCdnCacheStatus();
// 在控制台输出完整缓存状态
await logCacheStatus();
}
} catch (error) {
console.error('加载缓存数据失败:', error);
this.$message.error('加载缓存数据失败');
} finally {
this.isLoading = false;
}
},
async refreshCache() {
this.loadCacheData();
this.$message.success('正在刷新缓存状态');
},
async clearCache() {
this.$confirm('确定要清除所有缓存吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const success = await clearAllCaches();
if (success) {
this.$message.success('缓存已清除');
await this.loadCacheData();
} else {
this.$message.error('清除缓存失败');
}
} catch (error) {
console.error('清除缓存失败:', error);
this.$message.error('清除缓存失败');
}
}).catch(() => {
this.$message.info('已取消清除');
});
},
refreshPage() {
window.location.reload();
},
handleClose() {
this.$emit('update:visible', false);
}
}
};
</script>
<style scoped>
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.loading-spinner {
margin-bottom: 10px;
}
.no-cache-message {
text-align: center;
padding: 20px;
}
.no-cache-message i {
font-size: 48px;
color: #E6A23C;
margin-bottom: 10px;
}
h3 {
margin-top: 20px;
margin-bottom: 10px;
font-weight: 500;
}
</style>
@@ -17,7 +17,7 @@
<span style="margin-right: 8px;">是否启用</span>
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div>
<div style="display: flex; align-items: center;">
<div style="display: none; align-items: center;">
<span style="margin-right: 8px;">设为默认</span>
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div>
@@ -64,18 +64,10 @@
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item
v-for="field in row"
:key="field.prop"
:label="field.label"
:prop="field.prop"
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
style="flex: 1;">
<el-input
v-model="form.configJson[field.prop]"
:placeholder="field.placeholder"
:type="field.type"
class="custom-input-bg"
:show-password="field.type === 'password'">
<el-input v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
class="custom-input-bg" :show-password="field.type === 'password'">
</el-input>
</el-form-item>
</div>
@@ -165,57 +157,57 @@ export default {
},
methods: {
resetForm() {
this.form = {
id: "",
modelType: "",
modelCode: "",
modelName: "",
isDefault: false,
isEnabled: false,
docLink: "",
remark: "",
sort: 0,
configJson: {}
};
this.dynamicCallInfoFields.forEach(field => {
this.$set(this.form.configJson, field.prop, '');
});
this.form = {
id: "",
modelType: "",
modelCode: "",
modelName: "",
isDefault: false,
isEnabled: false,
docLink: "",
remark: "",
sort: 0,
configJson: {}
};
this.dynamicCallInfoFields.forEach(field => {
this.$set(this.form.configJson, field.prop, '');
});
},
resetProviders() {
this.providers = [];
this.providersLoaded = false;
},
loadModelData() {
if (this.modelData.id) {
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
if (data.code === 0 && data.data) {
const model = data.data;
this.pendingProviderType = model.configJson.type;
this.pendingModelData = model;
if (this.modelData.id) {
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
if (data.code === 0 && data.data) {
const model = data.data;
this.pendingProviderType = model.configJson.type;
this.pendingModelData = model;
if (this.providersLoaded) {
this.loadProviderFields(model.configJson.type);
} else {
this.loadProviders();
}
}
});
}
if (this.providersLoaded) {
this.loadProviderFields(model.configJson.type);
} else {
this.loadProviders();
}
}
});
}
},
handleSave() {
const provideCode = this.form.configJson.type;
const formData = {
id: this.modelData.id,
modelCode: this.form.modelCode,
modelName: this.form.modelName,
isDefault: this.form.isDefault ? 1 : 0,
isEnabled: this.form.isEnabled ? 1 : 0,
docLink: this.form.docLink,
remark: this.form.remark,
sort: this.form.sort || 0,
configJson: {
...this.form.configJson,
}
const provideCode = this.form.configJson.type;
const formData = {
id: this.modelData.id,
modelCode: this.form.modelCode,
modelName: this.form.modelName,
isDefault: this.form.isDefault ? 1 : 0,
isEnabled: this.form.isEnabled ? 1 : 0,
docLink: this.form.docLink,
remark: this.form.remark,
sort: this.form.sort || 0,
configJson: {
...this.form.configJson,
}
};
this.$emit("save", { provideCode, formData });
this.dialogVisible = false;
+39 -3
View File
@@ -582,7 +582,7 @@ export default {
.table-container {
flex: 1;
overflow-y: scroll;
overflow: auto;
scrollbar-width: none;
padding-right: 15px;
width: calc(100% - 16px);
@@ -641,8 +641,8 @@ export default {
/* 音频播放器容器样式 */
.custom-audio-container {
width: 280px;
margin: auto;
width: 90%;
margin: 0 auto;
}
/* 新增按钮组样式 */
@@ -668,4 +668,40 @@ export default {
.save-btn {
color: #5cca8e !important;
}
/* 表格单元格自适应 */
::v-deep .el-table__body-wrapper {
overflow-x: hidden !important;
}
::v-deep .el-table td {
white-space: pre-wrap !important;
word-break: break-all !important;
}
/* 按钮组定位调整 */
.action-buttons {
position: static;
padding: 15px 0;
background: white;
box-shadow: 0 -2px 12px rgba(0,0,0,0.05);
}
/* 输入框自适应 */
::v-deep .el-input__inner,
::v-deep .el-textarea__inner {
width: 100% !important;
min-width: 120px;
}
/* 音频输入框特殊处理 */
.audio-input ::v-deep .el-input__inner {
min-width: 200px;
}
/* 操作按钮弹性布局 */
::v-deep .el-table__row .el-button {
flex-shrink: 0;
margin: 2px !important;
}
</style>
@@ -5,33 +5,17 @@
</template>
<script>
import Api from '@/apis/api';
import { mapState } from 'vuex';
export default {
name: 'VersionFooter',
data() {
return {
version: ''
}
computed: {
...mapState({
version: state => state.pubConfig.version
})
},
mounted() {
this.getSystemVersion();
},
methods: {
getSystemVersion() {
const storedVersion = sessionStorage.getItem('systemVersion');
if (storedVersion) {
this.version = storedVersion;
return;
}
Api.user.getPubConfig(({ data }) => {
if (data.code === 0 && data.data.version) {
this.version = data.data.version;
sessionStorage.setItem('systemVersion', data.data.version);
}
});
}
this.$store.dispatch('fetchPubConfig');
}
}
</script>
+5
View File
@@ -6,11 +6,16 @@ import App from './App.vue';
import router from './router';
import store from './store';
import './styles/global.scss';
import { register as registerServiceWorker } from './registerServiceWorker';
Vue.use(ElementUI);
Vue.config.productionTip = false
// 注册Service Worker
registerServiceWorker();
// 创建Vue实例
new Vue({
router,
store,
@@ -0,0 +1,113 @@
/* eslint-disable no-console */
export const register = () => {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
const swUrl = `${process.env.BASE_URL}service-worker.js`;
console.info(`[小智服务] 正在尝试注册Service WorkerURL: ${swUrl}`);
// 先检查Service Worker是否已注册
navigator.serviceWorker.getRegistrations().then(registrations => {
if (registrations.length > 0) {
console.info('[小智服务] 发现已有Service Worker注册,正在检查更新');
}
// 继续注册Service Worker
navigator.serviceWorker
.register(swUrl)
.then(registration => {
console.info('[小智服务] Service Worker注册成功');
// 更新处理
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// 内容已缓存更新,通知用户刷新
console.log('[小智服务] 新内容可用,请刷新页面');
// 可以在这里展示更新提示
const updateNotification = document.createElement('div');
updateNotification.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #409EFF;
color: white;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
z-index: 9999;
`;
updateNotification.innerHTML = `
<div style="display: flex; align-items: center;">
<span style="margin-right: 10px;">发现新版本,点击刷新应用</span>
<button style="background: white; color: #409EFF; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">刷新</button>
</div>
`;
document.body.appendChild(updateNotification);
updateNotification.querySelector('button').addEventListener('click', () => {
window.location.reload();
});
} else {
// 一切正常,Service Worker已成功安装
console.log('[小智服务] 内容已缓存供离线使用');
// 可以在这里初始化缓存
setTimeout(() => {
// 预热CDN缓存
const cdnUrls = [
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css',
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
];
// 预热缓存
cdnUrls.forEach(url => {
fetch(url, { mode: 'no-cors' }).catch(err => {
console.log(`预热缓存 ${url} 失败`, err);
});
});
}, 2000);
}
}
};
};
})
.catch(error => {
console.error('Service Worker 注册失败:', error);
if (error.name === 'TypeError' && error.message.includes('Failed to register a ServiceWorker')) {
console.warn('[小智服务] 注册Service Worker时出现网络错误,CDN资源可能无法缓存');
if (process.env.NODE_ENV === 'production') {
console.info(
'可能原因:1. 服务器未配置正确的MIME类型 2. 服务器SSL证书问题 3. 服务器未返回service-worker.js文件'
);
}
}
});
});
});
}
};
export const unregister = () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
};
+174
View File
@@ -0,0 +1,174 @@
/* global self, workbox */
// 自定义Service Worker安装和激活的处理逻辑
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// CDN资源列表
const CDN_CSS = [
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
];
const CDN_JS = [
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
];
// 当Service Worker被注入manifest后会自动执行
const manifest = self.__WB_MANIFEST || [];
// 检查是否启用CDN模式
const isCDNEnabled = manifest.some(entry =>
entry.url === 'cdn-mode' && entry.revision === 'enabled'
);
console.log(`Service Worker 已初始化, CDN模式: ${isCDNEnabled ? '启用' : '禁用'}`);
// 注入workbox相关代码
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
workbox.setConfig({ debug: false });
// 开启workbox
workbox.core.skipWaiting();
workbox.core.clientsClaim();
// 预缓存离线页面
const OFFLINE_URL = '/offline.html';
workbox.precaching.precacheAndRoute([
{ url: OFFLINE_URL, revision: null }
]);
// 添加安装完成事件处理器,在控制台显示安装消息
self.addEventListener('install', event => {
if (isCDNEnabled) {
console.log('Service Worker 已安装,开始缓存CDN资源');
} else {
console.log('Service Worker 已安装,CDN模式禁用,仅缓存本地资源');
}
// 确保离线页面被缓存
event.waitUntil(
caches.open('offline-cache').then((cache) => {
return cache.add(OFFLINE_URL);
})
);
});
// 添加激活事件处理器
self.addEventListener('activate', event => {
console.log('Service Worker 已激活,现在控制着页面');
// 清理旧版本缓存
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(cacheName => {
// 清理除当前版本外的缓存
return cacheName.startsWith('workbox-') && !workbox.core.cacheNames.runtime.includes(cacheName);
}).map(cacheName => {
return caches.delete(cacheName);
})
);
})
);
});
// 添加fetch事件拦截器,用于查看CDN资源是否命中缓存
self.addEventListener('fetch', event => {
// 只有启用CDN模式时才进行CDN资源缓存监控
if (isCDNEnabled) {
const url = new URL(event.request.url);
// 针对CDN资源,输出是否命中缓存的信息
if ([...CDN_CSS, ...CDN_JS].includes(url.href)) {
// 不干扰正常的fetch流程,只添加日志
console.log(`请求CDN资源: ${url.href}`);
}
}
});
// 仅在CDN模式下缓存CDN资源
if (isCDNEnabled) {
// 缓存CDN的CSS资源
workbox.routing.registerRoute(
({ url }) => CDN_CSS.includes(url.href),
new workbox.strategies.CacheFirst({
cacheName: 'cdn-stylesheets',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
maxEntries: 10, // 最多缓存10个CSS文件
}),
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200], // 缓存成功响应
}),
],
})
);
// 缓存CDN的JS资源
workbox.routing.registerRoute(
({ url }) => CDN_JS.includes(url.href),
new workbox.strategies.CacheFirst({
cacheName: 'cdn-scripts',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
maxEntries: 20, // 最多缓存20个JS文件
}),
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200], // 缓存成功响应
}),
],
})
);
}
// 无论是否启用CDN模式,都缓存本地静态资源
workbox.routing.registerRoute(
/\.(?:js|css|png|jpg|jpeg|svg|gif|ico|woff|woff2|eot|ttf|otf)$/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'static-resources',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 7 * 24 * 60 * 60, // 7天缓存
maxEntries: 50, // 最多缓存50个文件
}),
],
})
);
// 缓存HTML页面
workbox.routing.registerRoute(
/\.html$/,
new workbox.strategies.NetworkFirst({
cacheName: 'html-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 1 * 24 * 60 * 60, // 1天缓存
maxEntries: 10, // 最多缓存10个HTML文件
}),
],
})
);
// 离线页面 - 使用更可靠的处理方式
workbox.routing.setCatchHandler(async ({ event }) => {
// 根据请求类型返回适当的默认页面
switch (event.request.destination) {
case 'document':
// 如果是网页请求,返回离线页面
return caches.match(OFFLINE_URL);
default:
// 所有其他请求返回错误
return Response.error();
}
});
+23 -1
View File
@@ -1,6 +1,7 @@
import { goToPage } from "@/utils";
import Vue from 'vue';
import Vuex from 'vuex';
import Api from '../apis/api';
import Constant from '../utils/constant';
Vue.use(Vuex)
@@ -9,7 +10,11 @@ export default new Vuex.Store({
state: {
token: '',
userInfo: {}, // 添加用户信息存储
isSuperAdmin: false // 添加superAdmin状态
isSuperAdmin: false, // 添加superAdmin状态
pubConfig: { // 添加公共配置存储
version: '',
allowUserRegister: false
}
},
getters: {
getToken(state) {
@@ -26,6 +31,9 @@ export default new Vuex.Store({
return state.isSuperAdmin
}
return localStorage.getItem('isSuperAdmin') === 'true'
},
getPubConfig(state) {
return state.pubConfig
}
},
mutations: {
@@ -39,6 +47,9 @@ export default new Vuex.Store({
state.isSuperAdmin = isSuperAdmin
localStorage.setItem('isSuperAdmin', isSuperAdmin)
},
setPubConfig(state, config) {
state.pubConfig = config
},
clearAuth(state) {
state.token = ''
state.userInfo = {}
@@ -55,6 +66,17 @@ export default new Vuex.Store({
goToPage(Constant.PAGE.LOGIN, true);
window.location.reload(); // 彻底重置状态
})
},
// 添加获取公共配置的 action
fetchPubConfig({ commit }) {
return new Promise((resolve) => {
Api.user.getPubConfig(({ data }) => {
if (data.code === 0) {
commit('setPubConfig', data.data);
}
resolve();
});
});
}
},
modules: {
+142
View File
@@ -0,0 +1,142 @@
/**
* 缓存查看工具 - 用于检查CDN资源是否已被Service Worker缓存
*/
/**
* 获取所有Service Worker缓存的名称
* @returns {Promise<string[]>} 缓存名称列表
*/
export const getCacheNames = async () => {
if (!('caches' in window)) {
return [];
}
try {
return await caches.keys();
} catch (error) {
console.error('获取缓存名称失败:', error);
return [];
}
};
/**
* 获取指定缓存中的所有URL
* @param {string} cacheName 缓存名称
* @returns {Promise<string[]>} 缓存的URL列表
*/
export const getCacheUrls = async (cacheName) => {
if (!('caches' in window)) {
return [];
}
try {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
return requests.map(request => request.url);
} catch (error) {
console.error(`获取缓存 ${cacheName} 的URL失败:`, error);
return [];
}
};
/**
* 检查特定URL是否已被缓存
* @param {string} url 要检查的URL
* @returns {Promise<boolean>} 是否已缓存
*/
export const isUrlCached = async (url) => {
if (!('caches' in window)) {
return false;
}
try {
const cacheNames = await getCacheNames();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const match = await cache.match(url);
if (match) {
return true;
}
}
return false;
} catch (error) {
console.error(`检查URL ${url} 是否缓存失败:`, error);
return false;
}
};
/**
* 获取当前页面所有CDN资源的缓存状态
* @returns {Promise<Object>} 缓存状态对象
*/
export const checkCdnCacheStatus = async () => {
// 从CDN缓存中查找资源
const cdnCaches = ['cdn-stylesheets', 'cdn-scripts'];
const results = {
css: [],
js: [],
totalCached: 0,
totalNotCached: 0
};
for (const cacheName of cdnCaches) {
try {
const urls = await getCacheUrls(cacheName);
// 区分CSS和JS资源
for (const url of urls) {
if (url.endsWith('.css')) {
results.css.push({ url, cached: true });
} else if (url.endsWith('.js')) {
results.js.push({ url, cached: true });
}
results.totalCached++;
}
} catch (error) {
console.error(`获取 ${cacheName} 缓存信息失败:`, error);
}
}
return results;
};
/**
* 清除所有Service Worker缓存
* @returns {Promise<boolean>} 是否成功清除
*/
export const clearAllCaches = async () => {
if (!('caches' in window)) {
return false;
}
try {
const cacheNames = await getCacheNames();
for (const cacheName of cacheNames) {
await caches.delete(cacheName);
}
return true;
} catch (error) {
console.error('清除所有缓存失败:', error);
return false;
}
};
/**
* 将缓存状态输出到控制台
*/
export const logCacheStatus = async () => {
console.group('Service Worker 缓存状态');
const cacheNames = await getCacheNames();
console.log('已发现的缓存:', cacheNames);
for (const cacheName of cacheNames) {
const urls = await getCacheUrls(cacheName);
console.group(`缓存: ${cacheName} (${urls.length} 项)`);
urls.forEach(url => console.log(url));
console.groupEnd();
}
console.groupEnd();
return cacheNames.length > 0;
};
+9 -19
View File
@@ -38,7 +38,7 @@
</div>
<div
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
</div>
</div>
<div class="login-btn" @click="login">登录</div>
@@ -61,12 +61,18 @@
import Api from '@/apis/api';
import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
import { mapState } from 'vuex';
export default {
name: 'login',
components: {
VersionFooter
},
computed: {
...mapState({
allowUserRegister: state => state.pubConfig.allowUserRegister
})
},
data() {
return {
activeName: "username",
@@ -77,13 +83,12 @@ export default {
captchaId: ''
},
captchaUuid: '',
captchaUrl: '',
version: ''
captchaUrl: ''
}
},
mounted() {
this.fetchCaptcha();
this.getSystemVersion();
this.$store.dispatch('fetchPubConfig');
},
methods: {
fetchCaptcha() {
@@ -148,21 +153,6 @@ export default {
goToRegister() {
goToPage('/register')
},
getSystemVersion() {
const storedVersion = sessionStorage.getItem('systemVersion');
if (storedVersion) {
this.version = storedVersion;
return;
}
Api.user.getPubConfig(({ data }) => {
if (data.code === 0 && data.data.version) {
this.version = data.data.version;
sessionStorage.setItem('systemVersion', data.data.version);
}
});
}
}
}
</script>
+14
View File
@@ -82,12 +82,18 @@
import Api from '@/apis/api';
import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
import { mapState } from 'vuex';
export default {
name: 'register',
components: {
VersionFooter
},
computed: {
...mapState({
allowUserRegister: state => state.pubConfig.allowUserRegister
})
},
data() {
return {
form: {
@@ -101,6 +107,14 @@ export default {
}
},
mounted() {
this.$store.dispatch('fetchPubConfig').then(() => {
if (!this.allowUserRegister) {
showDanger('当前不允许普通用户注册');
setTimeout(() => {
goToPage('/login');
}, 1500);
}
});
this.fetchCaptcha();
},
methods: {
+71 -31
View File
@@ -6,10 +6,38 @@ const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin')
// BundleAnalyzerPlugin 用于分析打包后的文件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// WorkboxPlugin 用于生成Service Worker
const { InjectManifest } = require('workbox-webpack-plugin');
// 引入 path 模块
const path = require('path');
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
// 确保加载 .env 文件
dotenv.config();
// 定义CDN资源列表,确保Service Worker也能访问
const cdnResources = {
css: [
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
],
js: [
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
]
};
// 判断是否使用CDN
const useCDN = process.env.VUE_APP_USE_CDN === 'true';
module.exports = defineConfig({
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
devServer: {
@@ -29,21 +57,9 @@ module.exports = defineConfig({
// 修改 HTML 插件配置,动态插入 CDN 链接
config.plugin('html')
.tap(args => {
if (process.env.NODE_ENV === 'production') {
args[0].cdn = {
css: [
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.css',
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
],
js: [
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js',
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/index.js',
'https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js',
'https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/dist/opus-decoder.min.js'
]
};
// 根据配置决定是否使用CDN
if (process.env.NODE_ENV === 'production' && useCDN) {
args[0].cdn = cdnResources;
}
return args;
});
@@ -101,14 +117,38 @@ module.exports = defineConfig({
minRatio: 0.8
})
);
config.externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'element-ui': 'ELEMENT',
'axios': 'axios',
'opus-decoder': 'OpusDecoder'
};
// 根据是否使用CDN来决定是否添加Service Worker
config.plugins.push(
new InjectManifest({
swSrc: path.resolve(__dirname, 'src/service-worker.js'),
swDest: 'service-worker.js',
exclude: [/\.map$/, /asset-manifest\.json$/],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
// 自定义Service Worker注入点
injectionPoint: 'self.__WB_MANIFEST',
// 添加额外信息传递给Service Worker
additionalManifestEntries: useCDN ?
[{ url: 'cdn-mode', revision: 'enabled' }] :
[{ url: 'cdn-mode', revision: 'disabled' }]
})
);
// 如果使用CDN,则配置externals排除依赖包
if (useCDN) {
config.externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'element-ui': 'ELEMENT',
'axios': 'axios',
'opus-decoder': 'OpusDecoder'
};
} else {
// 确保不使用CDN时不设置externals,让webpack打包所有依赖
config.externals = {};
}
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
config.plugins.push(
new BundleAnalyzerPlugin({
@@ -128,13 +168,13 @@ module.exports = defineConfig({
config: [__filename] // 每次配置文件修改时缓存失效
}
};
config.resolve.alias = {
'@': path.resolve(__dirname, 'src'), // 让 '@' 代表 'src' 目录
'@assets': path.resolve(__dirname, 'src/assets'), // 设置 '@assets' 为 'src/assets' 目录
'@components': path.resolve(__dirname, 'src/components'), // 设置 '@components' 为 'src/components' 目录
'@views': path.resolve(__dirname, 'src/views'), // 设置 '@views' 为 'src/views' 目录
}
}
},
// 将CDN资源信息暴露给service-worker.js
pwa: {
workboxOptions: {
skipWaiting: true,
clientsClaim: true
}
}
});