mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 17:43:55 +08:00
Merge branch 'xinnan-tech:main' into bug_fix_funshine
This commit is contained in:
@@ -42,7 +42,7 @@ config WEBSOCKET_URL
|
|||||||
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
||||||
|
|
||||||
找到`OTA_VERSION_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
找到`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
|
config OTA_VERSION_URL
|
||||||
string "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
|
help
|
||||||
The application will access this URL to check for updates.
|
The application will access this URL to check for updates.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_SECRET = "server.secret";
|
String SERVER_SECRET = "server.secret";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许用户注册
|
||||||
|
*/
|
||||||
|
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发六位验证码时显示的控制面板地址
|
||||||
|
*/
|
||||||
|
String SERVER_FRONTED_URL = "server.fronted_url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径分割符
|
* 路径分割符
|
||||||
*/
|
*/
|
||||||
|
|||||||
+25
-23
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.service.impl;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||||
@@ -39,29 +40,30 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
|||||||
@Override
|
@Override
|
||||||
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
||||||
modelType = modelType.toUpperCase();
|
modelType = modelType.toUpperCase();
|
||||||
AgentTemplateEntity defaultTemplate = getDefaultTemplate();
|
|
||||||
if (defaultTemplate != null) {
|
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
|
||||||
switch (modelType) {
|
switch (modelType) {
|
||||||
case "ASR":
|
case "ASR":
|
||||||
defaultTemplate.setAsrModelId(modelId);
|
wrapper.set("asr_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "VAD":
|
case "VAD":
|
||||||
defaultTemplate.setVadModelId(modelId);
|
wrapper.set("vad_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "LLM":
|
case "LLM":
|
||||||
defaultTemplate.setLlmModelId(modelId);
|
wrapper.set("llm_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "TTS":
|
case "TTS":
|
||||||
defaultTemplate.setTtsModelId(modelId);
|
wrapper.set("tts_model_id", modelId);
|
||||||
break;
|
wrapper.set("tts_voice_id", null);
|
||||||
case "Memory":
|
break;
|
||||||
defaultTemplate.setMemModelId(modelId);
|
case "MEMORY":
|
||||||
break;
|
wrapper.set("mem_model_id", modelId);
|
||||||
case "Intent":
|
break;
|
||||||
defaultTemplate.setIntentModelId(modelId);
|
case "INTENT":
|
||||||
break;
|
wrapper.set("intent_model_id", modelId);
|
||||||
}
|
break;
|
||||||
this.updateById(defaultTemplate);
|
|
||||||
}
|
}
|
||||||
|
wrapper.ge("sort", 0);
|
||||||
|
update(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,6 @@ import java.util.TimeZone;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
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.service.DeviceService;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -49,11 +49,11 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
// 添加构造函数来初始化 deviceMapper
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
SysParamsService sysParamsService,
|
||||||
RedisTemplate<String, Object> redisTemplate) {
|
RedisTemplate<String, Object> redisTemplate) {
|
||||||
this.deviceDao = deviceDao;
|
this.deviceDao = deviceDao;
|
||||||
this.sysUserUtilService = sysUserUtilService;
|
this.sysUserUtilService = sysUserUtilService;
|
||||||
this.frontedUrl = frontedUrl;
|
this.frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ public class ModelController {
|
|||||||
}
|
}
|
||||||
// 将其他模型设置为非默认
|
// 将其他模型设置为非默认
|
||||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||||
|
entity.setIsEnabled(1);
|
||||||
entity.setIsDefault(1);
|
entity.setIsDefault(1);
|
||||||
modelConfigService.updateById(entity);
|
modelConfigService.updateById(entity);
|
||||||
|
|
||||||
|
|||||||
+1
@@ -42,6 +42,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||||
new QueryWrapper<ModelConfigEntity>()
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
.eq("model_type", modelType)
|
.eq("model_type", modelType)
|
||||||
|
.eq("is_enabled", 1)
|
||||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||||
.select("id", "model_name"));
|
.select("id", "model_name"));
|
||||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||||
|
|||||||
+4
@@ -77,6 +77,9 @@ public class LoginController {
|
|||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@Operation(summary = "注册")
|
@Operation(summary = "注册")
|
||||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||||
|
if (!sysUserService.getAllowUserRegister()) {
|
||||||
|
throw new RenException("当前不允许普通用户注册");
|
||||||
|
}
|
||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
@@ -119,6 +122,7 @@ public class LoginController {
|
|||||||
public Result<Map<String, Object>> pubConfig() {
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
Map<String, Object> config = new HashMap<>();
|
Map<String, Object> config = new HashMap<>();
|
||||||
config.put("version", "0.3.3");
|
config.put("version", "0.3.3");
|
||||||
|
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||||
return new Result<Map<String, Object>>().ok(config);
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-3
@@ -27,6 +27,7 @@ import xiaozhi.common.validator.ValidatorUtils;
|
|||||||
import xiaozhi.common.validator.group.AddGroup;
|
import xiaozhi.common.validator.group.AddGroup;
|
||||||
import xiaozhi.common.validator.group.DefaultGroup;
|
import xiaozhi.common.validator.group.DefaultGroup;
|
||||||
import xiaozhi.common.validator.group.UpdateGroup;
|
import xiaozhi.common.validator.group.UpdateGroup;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class SysParamsController {
|
public class SysParamsController {
|
||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
|
private final ConfigService configService;
|
||||||
|
|
||||||
@GetMapping("page")
|
@GetMapping("page")
|
||||||
@Operation(summary = "分页")
|
@Operation(summary = "分页")
|
||||||
@@ -77,7 +79,7 @@ public class SysParamsController {
|
|||||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.save(dto);
|
sysParamsService.save(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ public class SysParamsController {
|
|||||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.update(dto);
|
sysParamsService.update(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ public class SysParamsController {
|
|||||||
AssertUtils.isArrayEmpty(ids, "id");
|
AssertUtils.isArrayEmpty(ids, "id");
|
||||||
|
|
||||||
sysParamsService.delete(ids);
|
sysParamsService.delete(ids);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,11 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
|||||||
* @param userIds 用户ID数组
|
* @param userIds 用户ID数组
|
||||||
*/
|
*/
|
||||||
void changeStatus(Integer status, String[] userIds);
|
void changeStatus(Integer status, String[] userIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否允许用户注册
|
||||||
|
*
|
||||||
|
* @return 是否允许用户注册
|
||||||
|
*/
|
||||||
|
boolean getAllowUserRegister();
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -30,6 +30,7 @@ import xiaozhi.modules.sys.dto.PasswordDTO;
|
|||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
|
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysUserDTO getByUsername(String username) {
|
public SysUserDTO getByUsername(String username) {
|
||||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
@@ -205,4 +208,17 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
updateById(entity);
|
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:
|
configuration-properties:
|
||||||
prefix:
|
prefix:
|
||||||
blobType: BLOB
|
blobType: BLOB
|
||||||
boolValue: TRUE
|
boolValue: TRUE
|
||||||
|
|
||||||
app:
|
|
||||||
fronted-url: http://localhost:8001
|
|
||||||
+6
-1
@@ -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_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_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_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
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504112058.sql
|
path: classpath:db/changelog/202504112058.sql
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 202504131543
|
id: 202504151205
|
||||||
author: John
|
author: John
|
||||||
changes:
|
changes:
|
||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504131543.sql
|
path: classpath:db/changelog/202504151205.sql
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
VUE_APP_API_BASE_URL=/xiaozhi
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
|
# 是否开启CDN
|
||||||
|
VUE_APP_USE_CDN=false
|
||||||
Generated
+3249
-11
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,8 @@
|
|||||||
"sass": "^1.32.7",
|
"sass": "^1.32.7",
|
||||||
"sass-loader": "^12.0.0",
|
"sass-loader": "^12.0.0",
|
||||||
"vue-template-compiler": "^2.6.14",
|
"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": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
@@ -43,4 +44,4 @@
|
|||||||
"*.scss",
|
"*.scss",
|
||||||
"*.vue"
|
"*.vue"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<title>
|
<title>
|
||||||
<%= process.env.VUE_APP_TITLE %>
|
<%= process.env.VUE_APP_TITLE %>
|
||||||
</title>
|
</title>
|
||||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
|
||||||
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
||||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- 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) { %>
|
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<cache-viewer v-if="isCDNEnabled" :visible.sync="showCacheViewer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,4 +46,118 @@ nav {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<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 Worker,CDN资源缓存功能不可用');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center custom-class="custom-dialog" :show-close="false"
|
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center
|
||||||
class="center-dialog">
|
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="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;">
|
<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>
|
<span style="margin-right: 8px;">是否启用</span>
|
||||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<span style="margin-right: 8px;">设为默认</span>
|
||||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,18 +65,10 @@
|
|||||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item
|
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||||
v-for="field in row"
|
|
||||||
:key="field.prop"
|
|
||||||
:label="field.label"
|
|
||||||
:prop="field.prop"
|
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
|
||||||
v-model="formData.configJson[field.prop]"
|
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:type="field.type || 'text'"
|
|
||||||
class="custom-input-bg"
|
|
||||||
:show-password="field.type === 'password'">
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +115,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
visible(val) {
|
visible(val) {
|
||||||
this.dialogVisible = val;
|
this.dialogVisible = val;
|
||||||
if(val) {
|
if (val) {
|
||||||
this.initConfigJson();
|
this.initConfigJson();
|
||||||
} else {
|
} else {
|
||||||
this.resetForm();
|
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>
|
<span style="margin-right: 8px;">是否启用</span>
|
||||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<span style="margin-right: 8px;">设为默认</span>
|
||||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,18 +64,10 @@
|
|||||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item
|
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||||
v-for="field in row"
|
|
||||||
:key="field.prop"
|
|
||||||
:label="field.label"
|
|
||||||
:prop="field.prop"
|
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||||
v-model="form.configJson[field.prop]"
|
class="custom-input-bg" :show-password="field.type === 'password'">
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:type="field.type"
|
|
||||||
class="custom-input-bg"
|
|
||||||
:show-password="field.type === 'password'">
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,57 +157,57 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.form = {
|
this.form = {
|
||||||
id: "",
|
id: "",
|
||||||
modelType: "",
|
modelType: "",
|
||||||
modelCode: "",
|
modelCode: "",
|
||||||
modelName: "",
|
modelName: "",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
docLink: "",
|
docLink: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
configJson: {}
|
configJson: {}
|
||||||
};
|
};
|
||||||
this.dynamicCallInfoFields.forEach(field => {
|
this.dynamicCallInfoFields.forEach(field => {
|
||||||
this.$set(this.form.configJson, field.prop, '');
|
this.$set(this.form.configJson, field.prop, '');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetProviders() {
|
resetProviders() {
|
||||||
this.providers = [];
|
this.providers = [];
|
||||||
this.providersLoaded = false;
|
this.providersLoaded = false;
|
||||||
},
|
},
|
||||||
loadModelData() {
|
loadModelData() {
|
||||||
if (this.modelData.id) {
|
if (this.modelData.id) {
|
||||||
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
||||||
if (data.code === 0 && data.data) {
|
if (data.code === 0 && data.data) {
|
||||||
const model = data.data;
|
const model = data.data;
|
||||||
this.pendingProviderType = model.configJson.type;
|
this.pendingProviderType = model.configJson.type;
|
||||||
this.pendingModelData = model;
|
this.pendingModelData = model;
|
||||||
|
|
||||||
if (this.providersLoaded) {
|
if (this.providersLoaded) {
|
||||||
this.loadProviderFields(model.configJson.type);
|
this.loadProviderFields(model.configJson.type);
|
||||||
} else {
|
} else {
|
||||||
this.loadProviders();
|
this.loadProviders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSave() {
|
handleSave() {
|
||||||
const provideCode = this.form.configJson.type;
|
const provideCode = this.form.configJson.type;
|
||||||
const formData = {
|
const formData = {
|
||||||
id: this.modelData.id,
|
id: this.modelData.id,
|
||||||
modelCode: this.form.modelCode,
|
modelCode: this.form.modelCode,
|
||||||
modelName: this.form.modelName,
|
modelName: this.form.modelName,
|
||||||
isDefault: this.form.isDefault ? 1 : 0,
|
isDefault: this.form.isDefault ? 1 : 0,
|
||||||
isEnabled: this.form.isEnabled ? 1 : 0,
|
isEnabled: this.form.isEnabled ? 1 : 0,
|
||||||
docLink: this.form.docLink,
|
docLink: this.form.docLink,
|
||||||
remark: this.form.remark,
|
remark: this.form.remark,
|
||||||
sort: this.form.sort || 0,
|
sort: this.form.sort || 0,
|
||||||
configJson: {
|
configJson: {
|
||||||
...this.form.configJson,
|
...this.form.configJson,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.$emit("save", { provideCode, formData });
|
this.$emit("save", { provideCode, formData });
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ export default {
|
|||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: scroll;
|
overflow: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 16px);
|
||||||
@@ -641,8 +641,8 @@ export default {
|
|||||||
|
|
||||||
/* 音频播放器容器样式 */
|
/* 音频播放器容器样式 */
|
||||||
.custom-audio-container {
|
.custom-audio-container {
|
||||||
width: 280px;
|
width: 90%;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新增按钮组样式 */
|
/* 新增按钮组样式 */
|
||||||
@@ -668,4 +668,40 @@ export default {
|
|||||||
.save-btn {
|
.save-btn {
|
||||||
color: #5cca8e !important;
|
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>
|
</style>
|
||||||
@@ -5,33 +5,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VersionFooter',
|
name: 'VersionFooter',
|
||||||
data() {
|
computed: {
|
||||||
return {
|
...mapState({
|
||||||
version: ''
|
version: state => state.pubConfig.version
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getSystemVersion();
|
this.$store.dispatch('fetchPubConfig');
|
||||||
},
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ import App from './App.vue';
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import './styles/global.scss';
|
import './styles/global.scss';
|
||||||
|
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||||
|
|
||||||
Vue.use(ElementUI);
|
Vue.use(ElementUI);
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// 注册Service Worker
|
||||||
|
registerServiceWorker();
|
||||||
|
|
||||||
|
// 创建Vue实例
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
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 Worker,URL: ${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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { goToPage } from "@/utils";
|
import { goToPage } from "@/utils";
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
|
import Api from '../apis/api';
|
||||||
import Constant from '../utils/constant';
|
import Constant from '../utils/constant';
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
@@ -9,7 +10,11 @@ export default new Vuex.Store({
|
|||||||
state: {
|
state: {
|
||||||
token: '',
|
token: '',
|
||||||
userInfo: {}, // 添加用户信息存储
|
userInfo: {}, // 添加用户信息存储
|
||||||
isSuperAdmin: false // 添加superAdmin状态
|
isSuperAdmin: false, // 添加superAdmin状态
|
||||||
|
pubConfig: { // 添加公共配置存储
|
||||||
|
version: '',
|
||||||
|
allowUserRegister: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getToken(state) {
|
getToken(state) {
|
||||||
@@ -26,6 +31,9 @@ export default new Vuex.Store({
|
|||||||
return state.isSuperAdmin
|
return state.isSuperAdmin
|
||||||
}
|
}
|
||||||
return localStorage.getItem('isSuperAdmin') === 'true'
|
return localStorage.getItem('isSuperAdmin') === 'true'
|
||||||
|
},
|
||||||
|
getPubConfig(state) {
|
||||||
|
return state.pubConfig
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@@ -39,6 +47,9 @@ export default new Vuex.Store({
|
|||||||
state.isSuperAdmin = isSuperAdmin
|
state.isSuperAdmin = isSuperAdmin
|
||||||
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
||||||
},
|
},
|
||||||
|
setPubConfig(state, config) {
|
||||||
|
state.pubConfig = config
|
||||||
|
},
|
||||||
clearAuth(state) {
|
clearAuth(state) {
|
||||||
state.token = ''
|
state.token = ''
|
||||||
state.userInfo = {}
|
state.userInfo = {}
|
||||||
@@ -55,6 +66,17 @@ export default new Vuex.Store({
|
|||||||
goToPage(Constant.PAGE.LOGIN, true);
|
goToPage(Constant.PAGE.LOGIN, true);
|
||||||
window.location.reload(); // 彻底重置状态
|
window.location.reload(); // 彻底重置状态
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
// 添加获取公共配置的 action
|
||||||
|
fetchPubConfig({ commit }) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Api.user.getPubConfig(({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
commit('setPubConfig', data.data);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
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>
|
</div>
|
||||||
<div class="login-btn" @click="login">登录</div>
|
<div class="login-btn" @click="login">登录</div>
|
||||||
@@ -61,12 +61,18 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import VersionFooter from '@/components/VersionFooter.vue';
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
components: {
|
components: {
|
||||||
VersionFooter
|
VersionFooter
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
allowUserRegister: state => state.pubConfig.allowUserRegister
|
||||||
|
})
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeName: "username",
|
activeName: "username",
|
||||||
@@ -77,13 +83,12 @@ export default {
|
|||||||
captchaId: ''
|
captchaId: ''
|
||||||
},
|
},
|
||||||
captchaUuid: '',
|
captchaUuid: '',
|
||||||
captchaUrl: '',
|
captchaUrl: ''
|
||||||
version: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
this.getSystemVersion();
|
this.$store.dispatch('fetchPubConfig');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchCaptcha() {
|
fetchCaptcha() {
|
||||||
@@ -148,21 +153,6 @@ export default {
|
|||||||
goToRegister() {
|
goToRegister() {
|
||||||
goToPage('/register')
|
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>
|
</script>
|
||||||
|
|||||||
@@ -82,12 +82,18 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import VersionFooter from '@/components/VersionFooter.vue';
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'register',
|
name: 'register',
|
||||||
components: {
|
components: {
|
||||||
VersionFooter
|
VersionFooter
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
allowUserRegister: state => state.pubConfig.allowUserRegister
|
||||||
|
})
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -101,6 +107,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||||
|
if (!this.allowUserRegister) {
|
||||||
|
showDanger('当前不允许普通用户注册');
|
||||||
|
setTimeout(() => {
|
||||||
|
goToPage('/login');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -6,10 +6,38 @@ const TerserPlugin = require('terser-webpack-plugin');
|
|||||||
const CompressionPlugin = require('compression-webpack-plugin')
|
const CompressionPlugin = require('compression-webpack-plugin')
|
||||||
// BundleAnalyzerPlugin 用于分析打包后的文件
|
// BundleAnalyzerPlugin 用于分析打包后的文件
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
// WorkboxPlugin 用于生成Service Worker
|
||||||
|
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||||
// 引入 path 模块
|
// 引入 path 模块
|
||||||
const path = require('path');
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, dir)
|
||||||
|
}
|
||||||
|
|
||||||
// 确保加载 .env 文件
|
// 确保加载 .env 文件
|
||||||
dotenv.config();
|
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({
|
module.exports = defineConfig({
|
||||||
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
||||||
devServer: {
|
devServer: {
|
||||||
@@ -29,21 +57,9 @@ module.exports = defineConfig({
|
|||||||
// 修改 HTML 插件配置,动态插入 CDN 链接
|
// 修改 HTML 插件配置,动态插入 CDN 链接
|
||||||
config.plugin('html')
|
config.plugin('html')
|
||||||
.tap(args => {
|
.tap(args => {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
// 根据配置决定是否使用CDN
|
||||||
args[0].cdn = {
|
if (process.env.NODE_ENV === 'production' && useCDN) {
|
||||||
css: [
|
args[0].cdn = cdnResources;
|
||||||
'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'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
});
|
});
|
||||||
@@ -101,14 +117,38 @@ module.exports = defineConfig({
|
|||||||
minRatio: 0.8
|
minRatio: 0.8
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
config.externals = {
|
|
||||||
'vue': 'Vue',
|
// 根据是否使用CDN来决定是否添加Service Worker
|
||||||
'vue-router': 'VueRouter',
|
config.plugins.push(
|
||||||
'vuex': 'Vuex',
|
new InjectManifest({
|
||||||
'element-ui': 'ELEMENT',
|
swSrc: path.resolve(__dirname, 'src/service-worker.js'),
|
||||||
'axios': 'axios',
|
swDest: 'service-worker.js',
|
||||||
'opus-decoder': 'OpusDecoder'
|
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') { // 通过环境变量控制
|
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new BundleAnalyzerPlugin({
|
new BundleAnalyzerPlugin({
|
||||||
@@ -128,13 +168,13 @@ module.exports = defineConfig({
|
|||||||
config: [__filename] // 每次配置文件修改时缓存失效
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user