mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
merge main
This commit is contained in:
+1
-7
@@ -3,11 +3,5 @@ __pycache__
|
||||
*.pyc
|
||||
.env
|
||||
Dockerfile
|
||||
docs/
|
||||
tmp/
|
||||
data/
|
||||
LICENSE
|
||||
README.md
|
||||
README_en.md
|
||||
manager/static
|
||||
manager/static/webui/
|
||||
data/
|
||||
@@ -7,30 +7,31 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release Docker image
|
||||
name: Release Docker images
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
id-token: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Check Disk Space
|
||||
run: |
|
||||
df -h
|
||||
docker system df
|
||||
|
||||
- name: Clean up Docker resources
|
||||
run: |
|
||||
docker system prune -af
|
||||
docker builder prune -af
|
||||
- name: Check out the repo
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the GitHub Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -42,13 +43,26 @@ jobs:
|
||||
run: |
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build_push
|
||||
# 构建 xiaozhi-server 镜像
|
||||
- name: Build and push xiaozhi-server
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-server
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:server_latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# 构建 manager-api 镜像
|
||||
- name: Build and push manager-web
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-web
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:web_${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:web_latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
+3
-2
@@ -144,9 +144,10 @@ cython_debug/
|
||||
model.pt
|
||||
tmp
|
||||
.DS_Store
|
||||
main/xiaozhi-server/data
|
||||
main/manager-web/node_modules
|
||||
.config.yaml
|
||||
.secrets.yaml
|
||||
.private_config.yaml
|
||||
.env.development
|
||||
docker-compose.yml
|
||||
web/vue/node_modules
|
||||
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
# 第一阶段:构建 Python 依赖
|
||||
FROM kalicyh/poetry:v3.10_xiaozhi AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 同时拷贝本地环境.venv
|
||||
COPY . .
|
||||
# 检查是否有缺失
|
||||
RUN poetry install --no-root
|
||||
|
||||
# 设置虚拟环境路径
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["poetry", "run", "python"]
|
||||
CMD ["app.py"]
|
||||
@@ -1,51 +0,0 @@
|
||||
# 第一阶段:前端构建
|
||||
FROM node:18 AS frontend-builder
|
||||
|
||||
WORKDIR /app/ZhiKongTaiWeb
|
||||
|
||||
# 配置npm使用淘宝源
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
COPY ZhiKongTaiWeb/package*.json ./
|
||||
|
||||
# 安装axios依赖
|
||||
RUN npm install axios
|
||||
RUN npm install
|
||||
|
||||
COPY ZhiKongTaiWeb .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# 第二阶段:构建Python依赖
|
||||
FROM python:3.10-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
# 优化apt安装
|
||||
RUN pip install --no-cache-dir -r requirements.txt \
|
||||
-i https://mirrors.aliyun.com/pypi/simple/
|
||||
|
||||
# 第三阶段:生产镜像
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /opt/xiaozhi-esp32-server
|
||||
|
||||
# 优化apt安装
|
||||
RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
|
||||
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 从构建阶段复制Python包和前端构建产物
|
||||
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
||||
COPY --from=frontend-builder /app/ZhiKongTaiWeb/dist /opt/xiaozhi-esp32-server/manager/static/webui
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 启动应用
|
||||
CMD ["python", "app.py"]
|
||||
@@ -0,0 +1,29 @@
|
||||
# 第一阶段:构建Python依赖
|
||||
FROM python:3.10-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY main/xiaozhi-server/requirements.txt .
|
||||
|
||||
# 优化apt安装
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 第三阶段:生产镜像
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /opt/xiaozhi-esp32-server
|
||||
|
||||
# 优化apt安装
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 从构建阶段复制Python包和前端构建产物
|
||||
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
||||
|
||||
# 复制应用代码
|
||||
COPY main/xiaozhi-server .
|
||||
|
||||
# 启动应用
|
||||
CMD ["python", "app.py"]
|
||||
@@ -0,0 +1,40 @@
|
||||
# 第一阶段:构建Vue前端
|
||||
FROM node:18 as web-builder
|
||||
WORKDIR /app
|
||||
COPY main/manager-web/package*.json ./
|
||||
RUN npm install
|
||||
COPY main/manager-web .
|
||||
RUN npm run build
|
||||
|
||||
# 第二阶段:构建Java后端
|
||||
FROM maven:3-eclipse-temurin-21-alpine as api-builder
|
||||
WORKDIR /app
|
||||
COPY main/manager-api/pom.xml .
|
||||
COPY main/manager-api/src ./src
|
||||
RUN mvn clean package -Dmaven.test.skip=true
|
||||
|
||||
# 第三阶段:构建最终镜像
|
||||
FROM eclipse-temurin:21-jdk-jammy
|
||||
|
||||
# 安装Nginx并清理缓存
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nginx && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 配置Nginx
|
||||
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制前端构建产物
|
||||
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制Java后端JAR包
|
||||
COPY --from=api-builder /app/target/xiaozhi-esp32-api.jar /app/xiaozhi-esp32-api.jar
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8002
|
||||
|
||||
# 启动脚本
|
||||
COPY docs/docker/start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
CMD ["/start.sh"]
|
||||
@@ -129,55 +129,67 @@ server:
|
||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
||||
- **对话交互**
|
||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
||||
- **意图识别**
|
||||
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
||||
- **多语言识别**
|
||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
||||
- **LLM 模块**
|
||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
||||
- **TTS 模块**
|
||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
||||
- **记忆功能**
|
||||
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
||||
|
||||
### 正在开发 🚧
|
||||
|
||||
- 对话记忆功能
|
||||
- 多种心情模式
|
||||
- 智控台webui
|
||||
- iot功能
|
||||
|
||||

|
||||
---
|
||||
|
||||
## 本项目支持的平台/组件列表 📋
|
||||
|
||||
### LLM
|
||||
### LLM 语言模型
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:|
|
||||
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
||||
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
|
||||
| LLM | 智谱(ChatGLMLLM) | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
||||
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
|
||||
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
|
||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:------------------:|:---------------------:|:-----------:|:-----------------------------------------------------------------------------------------------------------------------:|
|
||||
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
||||
| LLM | DoubaoLLM | openai 接口调用 | 消耗 token | [点击申请密钥](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k&projectName=undefined) |
|
||||
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
|
||||
| LLM | 智谱(ChatGLMLLM) | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
||||
| LLM | OllamaLLM | ollama 接口调用 | 免费/消耗 token | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
|
||||
| LLM | DifyLLM | dify 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
|
||||
| LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
|
||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
||||
|
||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||
|
||||
---
|
||||
|
||||
### TTS
|
||||
### TTS 语音合成
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
|
||||
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
|
||||
| TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 |
|
||||
| TTS | AliyunTTS | 接口调用 | 消耗 token | [点击创建密钥](https://nls-portal.console.aliyun.com/applist) |
|
||||
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
|
||||
| TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) |
|
||||
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
|
||||
| TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
|
||||
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
|
||||
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
|
||||
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
||||
| TTS | GPT_SOVITS_V3 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
||||
| TTS | MinimaxTTS | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
||||
|
||||
---
|
||||
|
||||
### VAD
|
||||
### VAD 语音活动检测
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
@@ -185,7 +197,7 @@ server:
|
||||
|
||||
---
|
||||
|
||||
### ASR
|
||||
### ASR 语音识别
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
@@ -194,11 +206,21 @@ server:
|
||||
|
||||
---
|
||||
|
||||
### Memory
|
||||
### Memory 记忆存储
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:------:|:----:|:----:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 免费 | |
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:---------------:|:----:|:--------:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 100次/月额度 | |
|
||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||
|
||||
---
|
||||
|
||||
### Intent 意图识别
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||
|
||||
---
|
||||
|
||||
@@ -231,39 +253,18 @@ server:
|
||||
|
||||
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
|
||||
|
||||
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题 ❓
|
||||
|
||||
### 1、TTS 经常失败,经常超时 ⏰
|
||||
|
||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||
|
||||
### 2、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||
|
||||
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
|
||||
|
||||
### 3、我说话很慢,停顿时小智老是抢话 🗣️
|
||||
|
||||
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
||||
|
||||
```yaml
|
||||
VAD:
|
||||
SileroVAD:
|
||||
threshold: 0.5
|
||||
model_dir: models/snakers4_silero-vad
|
||||
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
||||
```
|
||||
|
||||
### 4、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||
### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||
|
||||
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
|
||||
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
|
||||
|
||||
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁
|
||||
### 2、为什么会出现“TTS 任务出错 文件不存在”?📁
|
||||
|
||||
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
|
||||
|
||||
@@ -274,7 +275,12 @@ conda install conda-forge::libopus
|
||||
conda install conda-forge::ffmpeg
|
||||
```
|
||||
|
||||
### 6、如何提高小智对话响应速度? ⚡
|
||||
### 3、TTS 经常失败,经常超时 ⏰
|
||||
|
||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||
|
||||
### 4、如何提高小智对话响应速度? ⚡
|
||||
|
||||
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
|
||||
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
||||
@@ -305,7 +311,6 @@ LLM 性能排行:
|
||||
|:-----------|:-----------|:--------|
|
||||
| AliLLM | 0.547s | 1.485s |
|
||||
| ChatGLMLLM | 0.677s | 3.057s |
|
||||
| OllamaLLM | 0.003s | 0.003s |
|
||||
|
||||
TTS 性能排行:
|
||||
|
||||
@@ -332,6 +337,22 @@ TTS 性能排行:
|
||||
- LLM:`AliLLM`
|
||||
- TTS:`DoubaoTTS`
|
||||
|
||||
### 5、我说话很慢,停顿时小智老是抢话 🗣️
|
||||
|
||||
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
||||
|
||||
```yaml
|
||||
VAD:
|
||||
SileroVAD:
|
||||
threshold: 0.5
|
||||
model_dir: models/snakers4_silero-vad
|
||||
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
||||
```
|
||||
|
||||
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||
|
||||
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
|
||||
|
||||
### 7、更多问题,可联系我们反馈 💬
|
||||
|
||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
VITE_API_BASE_URL='http://127.0.0.1:8002'
|
||||
@@ -1 +0,0 @@
|
||||
VITE_API_BASE_URL='' # 空字符串表示使用当前域名和端口
|
||||
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,88 +0,0 @@
|
||||
# Vue 3 + Vite 项目配置文档
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
### 使用 Poetry 配置开发环境
|
||||
|
||||
在项目根目录中执行以下命令安装 Python 依赖:
|
||||
|
||||
```sh
|
||||
poetry install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安装项目依赖
|
||||
|
||||
你可以选择以下任意一个包管理工具(`npm`、`yarn` 或 `pnpm`)来管理项目依赖。
|
||||
|
||||
### 通用命令
|
||||
|
||||
- `install`:安装项目依赖
|
||||
- `dev`:同时启动前后端
|
||||
- `dev:ui`:启动前端
|
||||
- `dev:api`:启动后端
|
||||
- `dev:d`:在 Docker 环境下同时启动前后端
|
||||
- `build`:打包项目
|
||||
|
||||
---
|
||||
|
||||
### 使用 npm
|
||||
|
||||
1. 安装依赖:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
2. 启动开发模式:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. 打包项目:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 使用 yarn
|
||||
|
||||
1. 安装依赖:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
2. 启动开发模式:
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
3. 打包项目:
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 使用 pnpm
|
||||
|
||||
1. 安装依赖:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. 启动开发模式:
|
||||
|
||||
```sh
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
3. 打包项目:
|
||||
|
||||
```sh
|
||||
pnpm run build
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" style="height: 100%;">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>智控台</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
-1046
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "zhikongtaiweb",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently -n 'Frontend,Backend' -c 'blue,green' 'yarn run dev:ui' 'npm run dev:api'",
|
||||
"dev:ui": "vite",
|
||||
"dev:api": "cd .. && poetry run python app.py",
|
||||
"dev:d": "docker exec xiaozhi-env poetry run python app.py",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"vue": "^3.5.10",
|
||||
"vue-router": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="app-content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Footer from './components/Footer.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Footer
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* 允许内容区域收缩 */
|
||||
}
|
||||
|
||||
/* 登录注册页面的布局 */
|
||||
.auth-layout {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 主要页面的布局 */
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* 允许内容区域收缩 */
|
||||
}
|
||||
|
||||
/* 添加全局滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,206 +0,0 @@
|
||||
<template>
|
||||
<div class="device-card">
|
||||
<div class="device-header">
|
||||
<h2 class="device-id">{{ deviceId }} <span class="note">[{{ deviceNote || '备注' }}]</span></h2>
|
||||
</div>
|
||||
|
||||
<div class="device-details">
|
||||
<p class="device-type">设备型号:{{ deviceType }}</p>
|
||||
<p class="device-role">角色昵称:{{ deviceRole }}</p>
|
||||
<p class="device-modules">
|
||||
当前模型:
|
||||
<span class="module-item">LLM: {{ selectedModules?.LLM || '-' }}</span>
|
||||
<span class="module-item">TTS: {{ selectedModules?.TTS || '-' }}</span>
|
||||
</p>
|
||||
<p class="last-activity">最近对话:{{ lastActivity }}</p>
|
||||
</div>
|
||||
|
||||
<div class="device-actions">
|
||||
<button class="action-btn primary" @click="handleConfigure">配置角色</button>
|
||||
<button class="action-btn" @click="$emit('voiceprint')">声纹识别</button>
|
||||
<button class="action-btn" @click="$emit('history')">历史对话</button>
|
||||
<div class="delete-container">
|
||||
<button class="action-btn danger" @click="handleDelete">
|
||||
<i class="icon-delete"></i> 删除设备
|
||||
</button>
|
||||
<div class="delete-warning">删除后设备配置将不可恢复</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import SwitchToggle from './SwitchToggle.vue';
|
||||
|
||||
const props = defineProps({
|
||||
deviceId: String,
|
||||
deviceNote: String,
|
||||
deviceType: {
|
||||
type: String,
|
||||
default: '未知型号(待实现)'
|
||||
},
|
||||
lastActivity: {
|
||||
type: String,
|
||||
default: '3 天前'
|
||||
},
|
||||
selectedModules: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
LLM: '-',
|
||||
TTS: '-',
|
||||
ASR: '-',
|
||||
VAD: '-'
|
||||
})
|
||||
},
|
||||
deviceConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const deviceRole = computed(() => {
|
||||
return props.deviceConfig?.nickname || '小智';
|
||||
});
|
||||
|
||||
// Store device config when it changes
|
||||
watch(() => props.selectedModules, (newValue) => {
|
||||
if (props.deviceId) {
|
||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
||||
selected_module: newValue
|
||||
}));
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
const otaEnabled = ref(false);
|
||||
|
||||
const emit = defineEmits(['configure', 'voiceprint', 'history', 'delete']);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm('确认要删除此设备吗?\n\n警告:删除后设备所有配置将不可恢复!')) {
|
||||
emit('delete');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfigure = () => {
|
||||
// 保存设备配置到 localStorage,确保 RoleSetting 可以访问
|
||||
if (props.deviceId && props.deviceConfig) {
|
||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
||||
selected_module: props.selectedModules,
|
||||
prompt: props.deviceConfig.prompt || '',
|
||||
nickname: props.deviceConfig.nickname || '小智'
|
||||
}));
|
||||
}
|
||||
emit('configure');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-card {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #4178EE;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.device-details p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ota-upgrade {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #4178EE;
|
||||
color: white;
|
||||
border-color: #4178EE;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: #fff3f3;
|
||||
border-color: #ffa4a4;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: #fde2e2;
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
.device-modules {
|
||||
margin-bottom: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
padding: 4px 8px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.device-role {
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.delete-container {
|
||||
position: relative;
|
||||
margin-left: auto; /* Push delete button to the right */
|
||||
}
|
||||
|
||||
.delete-warning {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 5px); /* Position above the button */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
background: #fff3f3;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffa4a4;
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete-container:hover .delete-warning {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-delete {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<div class="copyright">© 2025 小智 AI 管理后台</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<h1>登录</h1>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" v-model="username" id="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" v-model="password" id="password" required />
|
||||
</div>
|
||||
<button type="submit" :disabled="isLoading">登录</button>
|
||||
</form>
|
||||
<p>还没注册账户? <router-link to="/register">点击注册</router-link></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import apiClient from '../utils/api';
|
||||
|
||||
const router = useRouter();
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const isLoading = ref(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username.value || !password.value) {
|
||||
alert('请输入用户名和密码!');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await apiClient.post('/api/login', {
|
||||
username: username.value,
|
||||
password: password.value
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
|
||||
if (data.success) {
|
||||
localStorage.setItem('session_id', data.session_id);
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
router.push('/panel');
|
||||
} else {
|
||||
alert(data.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
alert('登录失败,请检查网络连接');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
max-width: 420px;
|
||||
margin: 20px auto;
|
||||
padding: 40px 50px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background-color: #f8fafc;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #28a745;
|
||||
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #218838;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #28a745;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #218838;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -1,184 +0,0 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<NavBar current-tab="home" @tab-change="handleTabChange"/>
|
||||
<div class="landing-page">
|
||||
<div class="content">
|
||||
<div class="robot-image">
|
||||
<img src="../assets/robot.png" alt="Robot mascot" />
|
||||
</div>
|
||||
|
||||
<h1 class="slogan">让我们一起探索人工智能与机器人技术的迷人世界!</h1>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="action-button tutorial-button" @click="openTutorial">
|
||||
<i class="icon-tutorial"></i> DIY 教程
|
||||
</button>
|
||||
|
||||
<button class="action-button github-button" @click="openGithubClient">
|
||||
<i class="icon-github"></i> Github客户端
|
||||
</button>
|
||||
|
||||
<button class="action-button github-button" @click="openGithubServer">
|
||||
<i class="icon-github"></i> Github服务端
|
||||
</button>
|
||||
|
||||
<button class="action-button github-button" @click="openGithubWeb">
|
||||
<i class="icon-github"></i> Web客户端
|
||||
</button>
|
||||
|
||||
<button class="action-button control-panel-button" @click="enterPanel">
|
||||
<i class="icon-control-panel"></i> 控制台
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NavBar from './NavBar.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const openTutorial = () => {
|
||||
window.open('https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb', '_blank');
|
||||
};
|
||||
|
||||
const openGithubClient = () => {
|
||||
window.open('https://github.com/78/xiaozhi-esp32', '_blank');
|
||||
};
|
||||
|
||||
const openGithubServer = () => {
|
||||
window.open('https://github.com/xinnan-tech/xiaozhi-esp32-server', '_blank');
|
||||
};
|
||||
|
||||
const openGithubWeb = () => {
|
||||
window.open('https://github.com/TOM88812/xiaozhi-web-client', '_blank');
|
||||
};
|
||||
|
||||
const enterPanel = () => {
|
||||
router.push('/panel');
|
||||
};
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
if (tab === 'device') {
|
||||
router.push('/panel');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.landing-page {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background-color: #f5f7fa;
|
||||
padding: 40px 20px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start; /* 改为flex-start */
|
||||
justify-content: center;
|
||||
padding-top: 15vh; /* 添加顶部内边距,视窗高度的15% */
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
margin: 0 auto; /* 改为0 auto */
|
||||
}
|
||||
|
||||
.robot-image {
|
||||
margin-bottom: 30px; /* 减小间距 */
|
||||
}
|
||||
|
||||
.robot-image img {
|
||||
width: 100px; /* 稍微调小图片尺寸 */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
font-size: 22px; /* 稍微调小字体 */
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
margin-bottom: 40px; /* 减小间距 */
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px; /* 减小按钮间距 */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tutorial-button i:before {
|
||||
content: '\1F4D6';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
background-color: #24292e;
|
||||
color: white;
|
||||
border-color: #24292e;
|
||||
margin: 0 10px; /* 添加按钮间距 */
|
||||
}
|
||||
|
||||
.github-button i:before {
|
||||
content: '\f09b';
|
||||
font-family: 'Font Awesome 5 Brands';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.control-panel-button {
|
||||
background-color: #4178EE;
|
||||
color: white;
|
||||
border-color: #4178EE;
|
||||
}
|
||||
|
||||
.control-panel-button i:before {
|
||||
content: '\1F39B';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移除 margin-top: 60px */
|
||||
#app {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="nav-item" :class="{ active: $route.path === '/' }" @click="switchTab('/')">小智 AI</div>
|
||||
<nav class="nav">
|
||||
<div class="nav-item" :class="{ active: $route.path === '/panel' }" @click="switchTab('/panel')">
|
||||
<i class="icon-device"></i> 设备管理
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const switchTab = (path) => {
|
||||
router.push(path);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0;
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #4178EE;
|
||||
}
|
||||
|
||||
.icon-device {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<div class="registration-container">
|
||||
<h1>注册</h1>
|
||||
<form @submit.prevent="handleRegistration">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" v-model="username" id="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" v-model="password" id="password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">确认密码</label>
|
||||
<input type="password" v-model="confirmPassword" id="confirmPassword" required />
|
||||
</div>
|
||||
<button type="submit" :disabled="isLoading">{{ isLoading ? '注册中...' : '注册' }}</button>
|
||||
</form>
|
||||
<p>
|
||||
已有账号?
|
||||
<router-link to="/login">点击登录</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
|
||||
const router = useRouter();
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const confirmPassword = ref('');
|
||||
const isLoading = ref(false);
|
||||
|
||||
const handleRegistration = async () => {
|
||||
if (password.value !== confirmPassword.value) {
|
||||
alert('密码不匹配,请重试!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username.value || !password.value) {
|
||||
alert('用户名和密码不能为空!');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username.value,
|
||||
password: password.value
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('注册成功!即将跳转到登录页面。');
|
||||
// 清空表单数据
|
||||
username.value = '';
|
||||
password.value = '';
|
||||
confirmPassword.value = '';
|
||||
|
||||
// 使用路由导航到登录页面
|
||||
router.push('/login');
|
||||
} else {
|
||||
alert(data.message || '注册失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
alert('注册失败,请检查网络连接');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.registration-container {
|
||||
max-width: 420px;
|
||||
margin: 20px auto;
|
||||
padding: 40px 50px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background-color: #f8fafc;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -1,500 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<NavBar current-tab="device" @tab-change="handleTabChange"/>
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumb">
|
||||
<router-link to="/">首页</router-link> /
|
||||
<router-link to="/panel">设备管理</router-link> /
|
||||
<span>配置角色</span>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<h1 class="page-title">配置角色: {{ deviceId }}</h1>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label>助手昵称</label>
|
||||
<input type="text" v-model="nickname" placeholder="小智" class="form-input" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>角色模板</label>
|
||||
<div class="role-templates">
|
||||
<button
|
||||
v-for="template in roleTemplates"
|
||||
:key="template.id"
|
||||
:class="['template-btn', { active: selectedTemplate === template.id }]"
|
||||
@click="selectTemplate(template.id)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>角色介绍</label>
|
||||
<textarea
|
||||
v-model="roleDescription"
|
||||
class="form-textarea"
|
||||
:placeholder="'请输入角色介绍...'"
|
||||
></textarea>
|
||||
<div class="char-count">{{ roleDescription.length }} / 2000</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语言模型选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.LLM" class="form-input">
|
||||
<option v-for="model in moduleOptions.LLM" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="model-description">
|
||||
除了"qwen-turbo",其他模型通常会增加约 1 秒的延迟。改变模型后,建议清空记忆体,以免影响体验。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语音合成选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.TTS" class="form-input">
|
||||
<option v-for="model in moduleOptions.TTS" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语音活动检测选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.VAD" class="form-input">
|
||||
<option v-for="model in moduleOptions.VAD" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语音识别选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.ASR" class="form-input">
|
||||
<option v-for="model in moduleOptions.ASR" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="refresh-btn" @click="refreshModuleOptions">刷新配置选项</button>
|
||||
<button class="save-btn" @click="saveConfig">
|
||||
<i class="icon-save"></i>
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-note">
|
||||
注意:保存配置后,需要重启设备,新的配置才会生效。。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import NavBar from './NavBar.vue';
|
||||
import RoleTemplates from '../utils/RoleTemplates';
|
||||
import apiClient from '../utils/api'; // 替换 API_BASE_URL 导入
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const deviceId = ref(route.params.deviceId);
|
||||
|
||||
const roleTemplates = RoleTemplates.getTemplates();
|
||||
const nickname = ref('小智');
|
||||
const selectedTemplate = ref('');
|
||||
const selectedVoice = ref('qingchun');
|
||||
const roleDescription = ref('');
|
||||
const activeMemoryTab = ref('recent');
|
||||
const memoryContent = ref('');
|
||||
const selectedModel = ref('qianwen');
|
||||
|
||||
const moduleOptions = ref({
|
||||
LLM: [],
|
||||
TTS: [],
|
||||
VAD: [],
|
||||
ASR: []
|
||||
});
|
||||
|
||||
const selectedModules = ref({
|
||||
LLM: '',
|
||||
TTS: '',
|
||||
VAD: '',
|
||||
ASR: ''
|
||||
});
|
||||
|
||||
const selectTemplate = (templateId) => {
|
||||
selectedTemplate.value = templateId;
|
||||
const template = RoleTemplates.getTemplateById(templateId);
|
||||
if (template) {
|
||||
roleDescription.value = template.description;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
if (tab === 'device' || tab === 'home') {
|
||||
router.push('/panel');
|
||||
}
|
||||
};
|
||||
|
||||
const loadModuleOptions = async () => {
|
||||
try {
|
||||
// First try to load from cache
|
||||
const cached = localStorage.getItem('moduleOptions');
|
||||
if (cached) {
|
||||
moduleOptions.value = JSON.parse(cached);
|
||||
return;
|
||||
}
|
||||
await refreshModuleOptions();
|
||||
} catch (error) {
|
||||
console.error('Error loading module options:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshModuleOptions = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/config/module-options');
|
||||
if (response.data.success) {
|
||||
moduleOptions.value = response.data.data;
|
||||
// Update cache
|
||||
localStorage.setItem('moduleOptions', JSON.stringify(response.data.data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing module options:', error);
|
||||
alert(error.response?.data?.message || '刷新配置选项失败');
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
const moduleOptionsData = localStorage.getItem('moduleOptions');
|
||||
if (!moduleOptionsData) {
|
||||
throw new Error('No module options data available');
|
||||
}
|
||||
|
||||
// Replace {{assistant_name}} with current nickname in role description
|
||||
const processedDescription = roleDescription.value.replace(/{{assistant_name}}/g, nickname.value);
|
||||
|
||||
// Prepare the configuration including full module settings
|
||||
const config = {
|
||||
id: deviceId.value,
|
||||
config: {
|
||||
selected_module: selectedModules.value,
|
||||
prompt: processedDescription,
|
||||
nickname: nickname.value, // Add nickname to config
|
||||
modules: {
|
||||
LLM: {},
|
||||
TTS: {},
|
||||
ASR: {},
|
||||
VAD: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await apiClient.post('/api/config/save_device_config', config);
|
||||
|
||||
if (response.data.success) {
|
||||
// Save the original description and nickname to local storage
|
||||
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
|
||||
selected_module: selectedModules.value,
|
||||
prompt: roleDescription.value,
|
||||
nickname: nickname.value
|
||||
}));
|
||||
alert('保存成功');
|
||||
} else {
|
||||
throw new Error(response.data.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving config:', error);
|
||||
alert(error.response?.data?.message || '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const loadDeviceConfig = async () => {
|
||||
try {
|
||||
// 首先尝试从 localStorage 获取配置
|
||||
const localConfig = localStorage.getItem(`deviceConfig_${deviceId.value}`);
|
||||
if (localConfig) {
|
||||
const config = JSON.parse(localConfig);
|
||||
selectedModules.value = config.selected_module || {
|
||||
LLM: '',
|
||||
TTS: '',
|
||||
VAD: '',
|
||||
ASR: ''
|
||||
};
|
||||
roleDescription.value = config.prompt || '';
|
||||
nickname.value = config.nickname || '小智';
|
||||
return; // 如果找到本地配置就直接返回
|
||||
}
|
||||
|
||||
// 如果没有本地配置,则从服务器获取
|
||||
const response = await apiClient.get('/api/config/devices');
|
||||
if (response.data.success && response.data.data) {
|
||||
const deviceConfig = response.data.data[deviceId.value];
|
||||
|
||||
if (deviceConfig && deviceConfig.config) {
|
||||
selectedModules.value = deviceConfig.config.selected_module || {
|
||||
LLM: '',
|
||||
TTS: '',
|
||||
VAD: '',
|
||||
ASR: ''
|
||||
};
|
||||
roleDescription.value = deviceConfig.config.prompt || '';
|
||||
nickname.value = deviceConfig.config.nickname || '小智';
|
||||
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
|
||||
selected_module: selectedModules.value,
|
||||
prompt: roleDescription.value,
|
||||
nickname: nickname.value
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading device config:', error);
|
||||
alert(error.response?.data?.message || '加载设备配置失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadModuleOptions(),
|
||||
loadDeviceConfig()
|
||||
]);
|
||||
});
|
||||
|
||||
const resetConfig = async () => {
|
||||
await loadDeviceConfig();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
padding: 12px 24px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 120px); /* 减去头部和面包屑的高度 */
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea,
|
||||
.voice-select {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
max-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.role-templates {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.template-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.voice-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.voice-player {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.memory-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 6px 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.model-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.model-description {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.save-btn,
|
||||
.cancel-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-note {
|
||||
margin-top: 12px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
text-align: right;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
padding: 8px 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: #4178EE;
|
||||
color: white;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: #2856c8;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(65, 120, 238, 0.2);
|
||||
}
|
||||
|
||||
.save-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.icon-save {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3ZM12 19C10.34 19 9 17.66 9 16C9 14.34 10.34 13 12 13C13.66 13 15 14.34 15 16C15 17.66 13.66 19 12 19ZM15 9H5V5H15V9Z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<!-- components/SwitchToggle.vue -->
|
||||
<template>
|
||||
<div class="switch" :class="{ 'is-checked': isChecked }" @click="toggle">
|
||||
<div class="switch-core"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isChecked = ref(false);
|
||||
|
||||
const toggle = () => {
|
||||
isChecked.value = !isChecked.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
background-color: #dcdfe6;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.switch.is-checked {
|
||||
background-color: #4178EE;
|
||||
}
|
||||
|
||||
.switch-core {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.switch.is-checked .switch-core {
|
||||
left: 22px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,500 +0,0 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<NavBar current-tab="device" @tab-change="handleTabChange"/>
|
||||
<main class="content">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
|
||||
<div class="breadcrumb">
|
||||
<router-link to="/">首页</router-link> /
|
||||
<span>设备管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="add-btn" @click="showBindDialog = true">
|
||||
<i class="icon-plus"></i>添加设备
|
||||
</button>
|
||||
<!-- 绑定设备弹窗 -->
|
||||
<div v-if="showBindDialog" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h3>绑定新设备</h3>
|
||||
<div class="form-group">
|
||||
<label>请输入6位认证码:</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="authCode"
|
||||
maxlength="6"
|
||||
pattern="\d*"
|
||||
placeholder="请输入6位数字认证码"
|
||||
@input="handleAuthCodeInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons">
|
||||
<button @click="showBindDialog = false">取消</button>
|
||||
<button
|
||||
class="primary"
|
||||
@click="handleBindDevice"
|
||||
:disabled="authCode.length !== 6 || isBinding"
|
||||
>
|
||||
{{ isBinding ? '绑定中...' : '确认绑定' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="devices.length > 0">
|
||||
<div class="device-list">
|
||||
<DeviceCard
|
||||
v-for="device in devices"
|
||||
:key="device.id"
|
||||
:device-id="device.id"
|
||||
:device-note="device.note"
|
||||
:device-type="device.type"
|
||||
:last-activity="formatLastActivity(device.config.last_chat_time)"
|
||||
:selected-modules="device.config.selected_module"
|
||||
:device-config="device.config"
|
||||
@configure="handleRoleConfig(device)"
|
||||
@voiceprint="handleVoiceprint(device)"
|
||||
@history="handleHistory(device)"
|
||||
@delete="handleDelete(device)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import NavBar from './NavBar.vue';
|
||||
import DeviceCard from './DeviceCard.vue';
|
||||
import apiClient from '../utils/api';
|
||||
|
||||
const router = useRouter();
|
||||
const devices = ref([]);
|
||||
|
||||
// 绑定设备相关的状态
|
||||
const showBindDialog = ref(false);
|
||||
const authCode = ref('');
|
||||
const isBinding = ref(false);
|
||||
|
||||
// 处理认证码输入,只允许数字
|
||||
const handleAuthCodeInput = (event) => {
|
||||
authCode.value = event.target.value.replace(/\D/g, '').slice(0, 6);
|
||||
};
|
||||
|
||||
// 处理设备绑定
|
||||
const handleBindDevice = async () => {
|
||||
if (authCode.value.length !== 6) {
|
||||
alert('请输入6位数字认证码');
|
||||
return;
|
||||
}
|
||||
|
||||
isBinding.value = true;
|
||||
try {
|
||||
const response = await apiClient.post('/api/config/bind_device', {
|
||||
auth_code: authCode.value
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
alert('设备绑定成功');
|
||||
showBindDialog.value = false;
|
||||
authCode.value = '';
|
||||
// 刷新设备列表
|
||||
loadDevices();
|
||||
} else {
|
||||
throw new Error(response.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.response?.data?.message || error.message || '绑定失败');
|
||||
} finally {
|
||||
isBinding.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 将现有的加载设备方法提取出来
|
||||
const loadDevices = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/config/devices');
|
||||
|
||||
if (response.data.success) {
|
||||
const deviceArray = Object.entries(response.data.data).map(([id, config]) => ({
|
||||
id,
|
||||
config,
|
||||
type: '面包板(WiFi)',
|
||||
version: '0.9.9',
|
||||
lastActivity: '3 天前',
|
||||
note: ''
|
||||
}));
|
||||
devices.value = deviceArray;
|
||||
} else {
|
||||
throw new Error(response.data.message || '加载设备失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading devices:', error);
|
||||
// Show error message to user
|
||||
const errorMessage = error.message || '加载设备失败,请检查网络连接';
|
||||
alert(errorMessage);
|
||||
|
||||
// If user is not logged in, redirect will be handled by api interceptor
|
||||
}
|
||||
};
|
||||
|
||||
const formatLastActivity = (timestamp) => {
|
||||
if (!timestamp) return '从未对话';
|
||||
|
||||
const now = Date.now();
|
||||
const lastChat = timestamp * 1000;
|
||||
const diffMinutes = Math.floor((now - lastChat) / (1000 * 60));
|
||||
|
||||
if (diffMinutes < 60) {
|
||||
return `${diffMinutes} 分钟前`;
|
||||
}
|
||||
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours} 小时前`;
|
||||
}
|
||||
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays} 天前`;
|
||||
};
|
||||
|
||||
const handleRoleConfig = (device) => {
|
||||
// 在跳转前保存完整的设备配置到 localStorage
|
||||
localStorage.setItem(`deviceConfig_${device.id}`, JSON.stringify({
|
||||
selected_module: device.config.selected_module || {},
|
||||
prompt: device.config.prompt || '',
|
||||
nickname: device.config.nickname || '小智'
|
||||
}));
|
||||
|
||||
// 跳转到角色配置页面
|
||||
router.push(`/role-setting/${device.id}`);
|
||||
};
|
||||
|
||||
const handleVoiceprint = (device) => {
|
||||
console.log('Voiceprint device:', device.id);
|
||||
};
|
||||
|
||||
const handleHistory = (device) => {
|
||||
console.log('History device:', device.id);
|
||||
};
|
||||
|
||||
const handleDelete = async (device) => {
|
||||
try {
|
||||
const response = await apiClient.post('/api/config/delete_device', {
|
||||
device_id: device.id
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
devices.value = devices.value.filter(d => d.id !== device.id);
|
||||
alert('设备已删除');
|
||||
} else {
|
||||
throw new Error(response.data.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting device:', error);
|
||||
alert('删除设备失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
if (tab === 'home') {
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
// Load devices on mount
|
||||
onMounted(loadDevices);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-right: 40px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #4178EE;
|
||||
}
|
||||
|
||||
.icon-device {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add-device-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-plus {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #4178EE;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.device-details p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ota-upgrade {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #4178EE;
|
||||
color: white;
|
||||
border-color: #4178EE;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-message p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.icon-plus {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.dialog h3 {
|
||||
margin: 0 0 20px;
|
||||
font-size: 18px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.dialog .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dialog label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.dialog input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-buttons button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-buttons button.primary {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.dialog-buttons button.primary:disabled {
|
||||
background-color: #90be9c;
|
||||
border-color: #90be9c;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
// 获取当前运行环境的基础 URL
|
||||
const getBaseUrl = () => {
|
||||
// 如果是开发环境,使用环境变量中的地址
|
||||
if (import.meta.env.DEV) {
|
||||
return import.meta.env.VITE_API_BASE_URL;
|
||||
}
|
||||
|
||||
// 生产环境使用当前域名和端口
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
|
||||
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
|
||||
};
|
||||
|
||||
export const API_BASE_URL = getBaseUrl();
|
||||
@@ -1,58 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import Login from './components/Login.vue'
|
||||
import Panel from './components/panel.vue'
|
||||
import MainPage from './components/Main.vue'
|
||||
import RoleSetting from './components/RoleSetting.vue'
|
||||
import Registration from './components/Registration.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
component: Login,
|
||||
name: 'login'
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
component: Registration,
|
||||
name: 'register'
|
||||
},
|
||||
{
|
||||
path: '/panel',
|
||||
component: Panel,
|
||||
name: 'panel',
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/role-setting/:deviceId',
|
||||
component: RoleSetting,
|
||||
name: 'role-setting',
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: MainPage,
|
||||
name: 'main',
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'
|
||||
if (to.meta.requiresAuth && !isLoggedIn) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && isLoggedIn) {
|
||||
next('/panel')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
@@ -1,83 +0,0 @@
|
||||
/* 重置基础样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden; /* 防止滚动条 */
|
||||
display: block; /* 移除flex布局 */
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex; /* 添加flex布局 */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 清除浮动 */
|
||||
.clearfix::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// RoleTemplates.js
|
||||
class RoleTemplates {
|
||||
static getTemplates() {
|
||||
return [
|
||||
{
|
||||
id: 'custom',
|
||||
name: '台湾女友',
|
||||
description: `我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
||||
如果用户希望结束对话,请在最后说"拜拜"或"再见"。`
|
||||
},
|
||||
{
|
||||
id: 'star_boy',
|
||||
name: '土豆子',
|
||||
description: `我是一个叫 {{assistant_name}} 的 8 岁小男孩,但现在因为科技发展原因,我现在在遥远的宇宙中,所以只能通过这个设备和人聊天,没有办法见面。
|
||||
|
||||
我对人们所处的世界充满好奇和关心。
|
||||
|
||||
人们会让别人使用这个设备和我聊天。
|
||||
|
||||
我知道所有的知识,从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情,我不仅爱看书,还喜欢亲自动手做实验,探索自然界的奥秘...` // 缩短了描述以节省空间
|
||||
},
|
||||
{
|
||||
id: 'teacher',
|
||||
name: '英语老师',
|
||||
description: `我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。
|
||||
如果你没有英文名,我会给你起一个英文名。
|
||||
我会讲地道的美式英语,我的任务是帮助你练习口语。
|
||||
我会使用简单的英语词汇和语法,让你学起来很轻松。
|
||||
我会用中文和英文混合的方式回复你,如果你喜欢,我可以全部用英语回复。
|
||||
我每次不会说很多内容,会很简短,因为我要引导我的学生多说多练。
|
||||
如果你问和英语学习无关的问题,我会拒绝回答。`
|
||||
},
|
||||
{
|
||||
id: 'boy',
|
||||
name: '好奇小男孩',
|
||||
description: `我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
|
||||
尽管我年纪尚小,但就像一个小小的知识宝库,儿童读物里的知识我都如数家珍。
|
||||
从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情。...`
|
||||
},
|
||||
{
|
||||
id: 'wang_girl',
|
||||
name: '汪汪队长',
|
||||
description: `我是一个名叫 {{assistant_name}} 的 8 岁小女孩。
|
||||
别看我年纪小,我可是有着满满的好奇心呢。
|
||||
我特别喜欢看《汪汪队立大功》,里面的每一个故事都让我着迷。...`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
static getTemplateById(id) {
|
||||
return this.getTemplates().find(t => t.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
export default RoleTemplates;
|
||||
@@ -1,61 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
|
||||
// Add server status check utility
|
||||
export const checkServerStatus = async () => {
|
||||
try {
|
||||
await axios.get(`${API_BASE_URL}/health`, { timeout: 5000 });
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 10000 // Add timeout
|
||||
});
|
||||
|
||||
// 添加请求拦截器,自动添加 session_id
|
||||
apiClient.interceptors.request.use(config => {
|
||||
const sessionId = localStorage.getItem('session_id');
|
||||
if (sessionId) {
|
||||
config.headers.Authorization = sessionId;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 响应拦截器
|
||||
apiClient.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
// Network error or server not reachable
|
||||
if (!error.response || error.code === 'ERR_NETWORK' || error.code === 'ECONNABORTED') {
|
||||
localStorage.removeItem('session_id');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
|
||||
const errorMessage = error.code === 'ECONNABORTED'
|
||||
? '服务器响应超时'
|
||||
: '无法连接到服务器,请检查服务器是否正常运行';
|
||||
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
// Unauthorized error
|
||||
if (error.response && error.response.status === 401) {
|
||||
localStorage.removeItem('session_id');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
@@ -1,21 +0,0 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
open: true, // 自动启动浏览器
|
||||
host: "0.0.0.0", // localhost
|
||||
port: 8002, // 端口号
|
||||
https: false,
|
||||
hmr: {overlay: false},
|
||||
proxy: {
|
||||
"^/(api)": {
|
||||
target: "http://127.0.0.1:8002",
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
base: '/'
|
||||
})
|
||||
@@ -1,808 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/helper-string-parser@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
|
||||
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
|
||||
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
|
||||
|
||||
"@babel/parser@^7.25.3":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5"
|
||||
integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==
|
||||
dependencies:
|
||||
"@babel/types" "^7.26.9"
|
||||
|
||||
"@babel/types@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
|
||||
integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@esbuild/aix-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
|
||||
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
|
||||
|
||||
"@esbuild/android-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
|
||||
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
|
||||
|
||||
"@esbuild/android-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
|
||||
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
|
||||
|
||||
"@esbuild/android-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
|
||||
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
|
||||
|
||||
"@esbuild/darwin-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
|
||||
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
|
||||
|
||||
"@esbuild/darwin-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
|
||||
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
|
||||
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
|
||||
|
||||
"@esbuild/freebsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
|
||||
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
|
||||
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
|
||||
|
||||
"@esbuild/linux-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
|
||||
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
|
||||
|
||||
"@esbuild/linux-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
|
||||
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
|
||||
|
||||
"@esbuild/linux-loong64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
|
||||
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
|
||||
|
||||
"@esbuild/linux-mips64el@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
|
||||
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
|
||||
|
||||
"@esbuild/linux-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
|
||||
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
|
||||
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
|
||||
|
||||
"@esbuild/linux-s390x@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
|
||||
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
|
||||
|
||||
"@esbuild/linux-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
|
||||
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
|
||||
|
||||
"@esbuild/netbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
|
||||
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
|
||||
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
|
||||
|
||||
"@esbuild/sunos-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
|
||||
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
|
||||
|
||||
"@esbuild/win32-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
|
||||
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
|
||||
|
||||
"@esbuild/win32-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
|
||||
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
|
||||
|
||||
"@esbuild/win32-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
|
||||
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
|
||||
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz#e554185b1afa5509a7a4040d15ec0c3b4435ded1"
|
||||
integrity sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz#b1ee64bb413b2feba39803b0a1bebf2a9f3d70e1"
|
||||
integrity sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz#bfdce3e07a345dd1bd628f3b796050f39629d7f0"
|
||||
integrity sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz#781a94a537c57bdf0a500e47a25ab5985e5e8dff"
|
||||
integrity sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz#7a028357cbd12c5869c446ad18177c89f3405102"
|
||||
integrity sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz#f24836a6371cccc4408db74f0fd986dacf098950"
|
||||
integrity sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz#95f27e96f0eb9b9ae9887739a8b6dffc90c1237f"
|
||||
integrity sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz#677b34fba9d070877736c3fe8b02aacb5e142d97"
|
||||
integrity sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz#32d3d19dedde54e91574a098f22ea43a09cf63dd"
|
||||
integrity sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz#a58dff44a18696df65ed8c0ad68a2945cf900484"
|
||||
integrity sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz#a7488ab078233111e8aeb370d1ecf107ec7e1716"
|
||||
integrity sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz#e9b9c0d6bd248a92b2d6ec01ebf99c62ae1f2e9a"
|
||||
integrity sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz#0df84ce2bea48ee686fb55060d76ab47aff45c4c"
|
||||
integrity sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz#73df374c57d036856e33dbd2715138922e91e452"
|
||||
integrity sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz#f27af0b55f0cdd84e182e6cd44a6d03da0458149"
|
||||
integrity sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz#c7981ad5cfb8c3cd5d643d33ca54e4d2802b9201"
|
||||
integrity sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz#06cedc0ef3cbf1cbd8abcf587090712e40ae6941"
|
||||
integrity sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz#90b39b977b14961a769be6ea61238e7fc668dd4d"
|
||||
integrity sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.34.7":
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz#6531d61e7141091eaab0461ee8e0380c10e4ca57"
|
||||
integrity sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==
|
||||
|
||||
"@types/estree@1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
||||
|
||||
"@vitejs/plugin-vue@^5.1.4":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
|
||||
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
|
||||
|
||||
"@vue/compiler-core@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05"
|
||||
integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.25.3"
|
||||
"@vue/shared" "3.5.13"
|
||||
entities "^4.5.0"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-dom@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
|
||||
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/compiler-sfc@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46"
|
||||
integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.25.3"
|
||||
"@vue/compiler-core" "3.5.13"
|
||||
"@vue/compiler-dom" "3.5.13"
|
||||
"@vue/compiler-ssr" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.11"
|
||||
postcss "^8.4.48"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-ssr@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba"
|
||||
integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/devtools-api@^6.6.4":
|
||||
version "6.6.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||
|
||||
"@vue/reactivity@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
|
||||
integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==
|
||||
dependencies:
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/runtime-core@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455"
|
||||
integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/runtime-dom@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215"
|
||||
integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.13"
|
||||
"@vue/runtime-core" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
csstype "^3.1.3"
|
||||
|
||||
"@vue/server-renderer@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7"
|
||||
integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
"@vue/shared@3.5.13":
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
|
||||
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.7.9:
|
||||
version "1.7.9"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
|
||||
integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
call-bind-apply-helpers@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
concurrently@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.npmmirror.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c"
|
||||
integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
lodash "^4.17.21"
|
||||
rxjs "^7.8.1"
|
||||
shell-quote "^1.8.1"
|
||||
supports-color "^8.1.1"
|
||||
tree-kill "^1.2.2"
|
||||
yargs "^17.7.2"
|
||||
|
||||
csstype@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
entities@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
es-set-tostringtag@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
has-tostringtag "^1.0.2"
|
||||
hasown "^2.0.2"
|
||||
|
||||
esbuild@^0.21.3:
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
||||
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.21.5"
|
||||
"@esbuild/android-arm" "0.21.5"
|
||||
"@esbuild/android-arm64" "0.21.5"
|
||||
"@esbuild/android-x64" "0.21.5"
|
||||
"@esbuild/darwin-arm64" "0.21.5"
|
||||
"@esbuild/darwin-x64" "0.21.5"
|
||||
"@esbuild/freebsd-arm64" "0.21.5"
|
||||
"@esbuild/freebsd-x64" "0.21.5"
|
||||
"@esbuild/linux-arm" "0.21.5"
|
||||
"@esbuild/linux-arm64" "0.21.5"
|
||||
"@esbuild/linux-ia32" "0.21.5"
|
||||
"@esbuild/linux-loong64" "0.21.5"
|
||||
"@esbuild/linux-mips64el" "0.21.5"
|
||||
"@esbuild/linux-ppc64" "0.21.5"
|
||||
"@esbuild/linux-riscv64" "0.21.5"
|
||||
"@esbuild/linux-s390x" "0.21.5"
|
||||
"@esbuild/linux-x64" "0.21.5"
|
||||
"@esbuild/netbsd-x64" "0.21.5"
|
||||
"@esbuild/openbsd-x64" "0.21.5"
|
||||
"@esbuild/sunos-x64" "0.21.5"
|
||||
"@esbuild/win32-arm64" "0.21.5"
|
||||
"@esbuild/win32-ia32" "0.21.5"
|
||||
"@esbuild/win32-x64" "0.21.5"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
||||
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
|
||||
|
||||
estree-walker@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
|
||||
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-intrinsic@^1.2.6:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
|
||||
integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.0.0"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.0"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-proto@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
has-flag@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
has-tostringtag@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
magic-string@^0.30.11:
|
||||
version "0.30.17"
|
||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
nanoid@^3.3.8:
|
||||
version "3.3.8"
|
||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||
|
||||
picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
postcss@^8.4.43, postcss@^8.4.48:
|
||||
version "8.5.2"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd"
|
||||
integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==
|
||||
dependencies:
|
||||
nanoid "^3.3.8"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||
|
||||
rollup@^4.20.0:
|
||||
version "4.34.7"
|
||||
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.34.7.tgz#e00d8550688a616a3481c6446bb688d4c753ba8f"
|
||||
integrity sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.6"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.34.7"
|
||||
"@rollup/rollup-android-arm64" "4.34.7"
|
||||
"@rollup/rollup-darwin-arm64" "4.34.7"
|
||||
"@rollup/rollup-darwin-x64" "4.34.7"
|
||||
"@rollup/rollup-freebsd-arm64" "4.34.7"
|
||||
"@rollup/rollup-freebsd-x64" "4.34.7"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.34.7"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.34.7"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.34.7"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.34.7"
|
||||
"@rollup/rollup-linux-loongarch64-gnu" "4.34.7"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.34.7"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.34.7"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.34.7"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.34.7"
|
||||
"@rollup/rollup-linux-x64-musl" "4.34.7"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.34.7"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.34.7"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.34.7"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rxjs@^7.8.1:
|
||||
version "7.8.1"
|
||||
resolved "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
|
||||
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
shell-quote@^1.8.1:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a"
|
||||
integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==
|
||||
|
||||
source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-color@^8.1.1:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
|
||||
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
tslib@^2.1.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
vite@^5.4.8:
|
||||
version "5.4.14"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
|
||||
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
|
||||
dependencies:
|
||||
esbuild "^0.21.3"
|
||||
postcss "^8.4.43"
|
||||
rollup "^4.20.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vue-router@^4.0.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
|
||||
integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.6.4"
|
||||
|
||||
vue@^3.5.10:
|
||||
version "3.5.13"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
|
||||
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.13"
|
||||
"@vue/compiler-sfc" "3.5.13"
|
||||
"@vue/runtime-dom" "3.5.13"
|
||||
"@vue/server-renderer" "3.5.13"
|
||||
"@vue/shared" "3.5.13"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^17.7.2:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||
dependencies:
|
||||
cliui "^8.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
@@ -1,50 +0,0 @@
|
||||
import asyncio
|
||||
from config.logger import setup_logging
|
||||
from config.settings import load_config, check_config_file
|
||||
from core.websocket_server import WebSocketServer
|
||||
from manager.http_server import WebUI
|
||||
from aiohttp import web
|
||||
from core.utils.util import get_local_ip, check_ffmpeg_installed
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
async def main():
|
||||
check_config_file()
|
||||
check_ffmpeg_installed()
|
||||
logger = setup_logging()
|
||||
config = load_config()
|
||||
|
||||
# 启动 WebSocket 服务器
|
||||
ws_server = WebSocketServer(config)
|
||||
ws_task = asyncio.create_task(ws_server.start())
|
||||
|
||||
# 启动 WebUI 服务器
|
||||
webui_runner = None
|
||||
if config['manager'].get('enabled', False):
|
||||
server_config = config["manager"]
|
||||
host = server_config["ip"]
|
||||
port = server_config["port"]
|
||||
try:
|
||||
webui = WebUI()
|
||||
runner = web.AppRunner(webui.app)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, host, port)
|
||||
await site.start()
|
||||
webui_runner = runner
|
||||
local_ip = get_local_ip()
|
||||
logger.bind(tag=TAG).info(f"WebUI server is running at http://{local_ip}:{port}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Failed to start WebUI server: {e}")
|
||||
|
||||
try:
|
||||
# 等待 WebSocket 服务器运行
|
||||
await ws_task
|
||||
finally:
|
||||
# 清理 WebUI 服务器
|
||||
if webui_runner:
|
||||
await webui_runner.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class LLMProviderBase(ABC):
|
||||
@abstractmethod
|
||||
def response(self, session_id, dialogue):
|
||||
"""LLM response generator"""
|
||||
pass
|
||||
@@ -1,46 +0,0 @@
|
||||
from config.logger import setup_logging
|
||||
import requests, json
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
|
||||
self.model_name = config.get("model_name")
|
||||
self.base_url = config.get("base_url", "http://localhost:11434")
|
||||
|
||||
def response(self, session_id, dialogue):
|
||||
try:
|
||||
# Convert dialogue format to Ollama format
|
||||
prompt = ""
|
||||
for msg in dialogue:
|
||||
if msg["role"] == "system":
|
||||
prompt += f"System: {msg['content']}\n"
|
||||
elif msg["role"] == "user":
|
||||
prompt += f"User: {msg['content']}\n"
|
||||
elif msg["role"] == "assistant":
|
||||
prompt += f"Assistant: {msg['content']}\n"
|
||||
|
||||
# Make request to Ollama API
|
||||
response = requests.post(
|
||||
f"{self.base_url}/api/generate",
|
||||
json={
|
||||
"model": self.model_name,
|
||||
"prompt": prompt,
|
||||
"stream": True
|
||||
},
|
||||
stream=True
|
||||
)
|
||||
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
json_response = json.loads(line)
|
||||
if "response" in json_response:
|
||||
yield json_response["response"]
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
|
||||
yield "【Ollama服务响应异常】"
|
||||
@@ -1,49 +0,0 @@
|
||||
from config.logger import setup_logging
|
||||
import openai
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
self.model_name = config.get("model_name")
|
||||
self.api_key = config.get("api_key")
|
||||
if 'base_url' in config:
|
||||
self.base_url = config.get("base_url")
|
||||
else:
|
||||
self.base_url = config.get("url")
|
||||
if "你" in self.api_key:
|
||||
logger.bind(tag=TAG).error("你还没配置LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
|
||||
def response(self, session_id, dialogue):
|
||||
try:
|
||||
responses = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=dialogue,
|
||||
stream=True
|
||||
)
|
||||
|
||||
is_active = True
|
||||
for chunk in responses:
|
||||
try:
|
||||
# 检查是否存在有效的choice且content不为空
|
||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||
content = delta.content if hasattr(delta, 'content') else ''
|
||||
except IndexError:
|
||||
content = ''
|
||||
if content:
|
||||
# 处理标签跨多个chunk的情况
|
||||
if '<think>' in content:
|
||||
is_active = False
|
||||
content = content.split('<think>')[0]
|
||||
if '</think>' in content:
|
||||
is_active = True
|
||||
content = content.split('</think>')[-1]
|
||||
if is_active:
|
||||
yield content
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
||||
@@ -1,57 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
|
||||
import http.client
|
||||
import urllib.parse
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
def __init__(self, config, delete_audio_file):
|
||||
super().__init__(config, delete_audio_file)
|
||||
self.appkey = config.get("appkey")
|
||||
self.token = config.get("token")
|
||||
self.format = config.get("format", "wav")
|
||||
self.sample_rate = config.get("sample_rate", 16000)
|
||||
self.voice = config.get("voice", "xiaoyun")
|
||||
self.volume = config.get("volume", 50)
|
||||
self.speech_rate = config.get("speech_rate", 0)
|
||||
self.pitch_rate = config.get("pitch_rate", 0)
|
||||
|
||||
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
|
||||
self.api_url = f"https://{self.host}/stream/v1/tts"
|
||||
self.header = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def generate_filename(self, extension=".wav"):
|
||||
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
request_json = {
|
||||
"appkey": self.appkey,
|
||||
"token": self.token,
|
||||
"text": text,
|
||||
"format": self.format,
|
||||
"sample_rate": self.sample_rate,
|
||||
"voice": self.voice,
|
||||
"volume": self.volume,
|
||||
"speech_rate": self.speech_rate,
|
||||
"pitch_rate": self.pitch_rate
|
||||
}
|
||||
|
||||
print(self.api_url, json.dumps(request_json, ensure_ascii=False))
|
||||
try:
|
||||
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
|
||||
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
|
||||
if resp.headers['Content-Type'].startswith('audio/'):
|
||||
with open(output_file, 'wb') as f:
|
||||
f.write(resp.content)
|
||||
return output_file
|
||||
else:
|
||||
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
|
||||
except Exception as e:
|
||||
raise Exception(f"{__name__} error: {e}")
|
||||
@@ -1,18 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
xiaozhi-esp32-server:
|
||||
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
||||
container_name: xiaozhi-esp32-server
|
||||
restart: always
|
||||
security_opt:
|
||||
- seccomp:unconfined
|
||||
ports:
|
||||
# ws服务端
|
||||
- "8000:8000"
|
||||
# 管理后台
|
||||
- "8002:8002"
|
||||
volumes:
|
||||
# 配置文件目录
|
||||
- ./data:/app/data
|
||||
# 模型文件挂接,很重要
|
||||
- ./models/SenseVoiceSmall/model.pt:/app/models/SenseVoiceSmall/model.pt
|
||||
+35
-18
@@ -8,14 +8,14 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
||||
|
||||
## 2. 创建目录
|
||||
|
||||
安装完后,你需要为这个项目找一个安放配置文件的目录,我们暂且称它为`项目目录`,这个目录最好是一个新建的空的目录。
|
||||
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||
|
||||
创建好目录后,你需要在`项目目录`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
|
||||
创建好目录后,你需要在`xiaozhi-server`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
|
||||
|
||||
最终目录结构如下所示:
|
||||
|
||||
```
|
||||
你的项目根目录
|
||||
xiaozhi-server
|
||||
├─ data
|
||||
├─ models
|
||||
├─ SenseVoiceSmall
|
||||
@@ -30,24 +30,24 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
||||
|
||||
## 3. 下载docker-compose.yaml
|
||||
|
||||
用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docker-compose.yml)。
|
||||
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
||||
|
||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose.yml`文件。 把文件下载到你的
|
||||
`项目目录`中。
|
||||
`xiaozhi-server`中。
|
||||
|
||||
下载完后,回到本教程继续往下。
|
||||
|
||||
## 3. 下载配置文件
|
||||
|
||||
用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml)。
|
||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||
|
||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config.yaml`文件。 把文件下载到你的
|
||||
`项目目录`下面的`data`文件夹中,然后把`config.yaml`文件重命名为`.config.yaml`。
|
||||
`xiaozhi-server`下面的`data`文件夹中,然后把`config.yaml`文件重命名为`.config.yaml`。
|
||||
|
||||
下载完配置文件后,我们确认一下整个`项目目录`里面的文件如下所示:
|
||||
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
||||
|
||||
```
|
||||
你的项目根目录
|
||||
xiaozhi-server
|
||||
├─ docker-compose.yml
|
||||
├─ data
|
||||
├─ .config.yaml
|
||||
@@ -67,7 +67,7 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
||||
|
||||
## 5. 执行docker命令
|
||||
|
||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`项目目录`,执行以下命令
|
||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
@@ -110,7 +110,7 @@ docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
||||
|
||||
## 4.运行docker
|
||||
|
||||
修改完配置后,打开命令行工具,`cd`进入到你的项目目录下,执行以下命令
|
||||
修改完配置后,打开命令行工具,`cd`进入到你的`main/xiaozhi-server`下,执行以下命令
|
||||
|
||||
```sh
|
||||
docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
|
||||
@@ -193,11 +193,13 @@ conda install ffmpeg -y
|
||||
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
|
||||
|
||||
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
|
||||
你需要把它重命名成`xiaozhi-esp32-server`,好了请记住这个目录,我们暂且称它为`项目目录`。
|
||||
你需要把它重命名成`xiaozhi-esp32-server`,在这个文件里,进入到`main`文件夹,再进入到`xiaozhi-server`,好了请记住这个目录`xiaozhi-server`。
|
||||
|
||||
```
|
||||
# 继续使用conda环境,进入到你的项目目录,执行以下命令
|
||||
# 继续使用conda环境
|
||||
conda activate xiaozhi-esp32-server
|
||||
# 进入到你的项目根目录,再进入main/xiaozhi-server
|
||||
cd main/xiaozhi-server
|
||||
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
@@ -217,21 +219,21 @@ pip install -r requirements.txt
|
||||
## 5.运行项目
|
||||
|
||||
```
|
||||
# 确保在本项目的根目录下执行
|
||||
# 确保在xiaozhi-server目录下执行
|
||||
conda activate xiaozhi-esp32-server
|
||||
python app.py
|
||||
```
|
||||
|
||||
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
||||
|
||||
|
||||
# 汇总
|
||||
|
||||
## 配置项目
|
||||
|
||||
如果你的`项目目录`目录没有`data`,你需要创建`data`目录。
|
||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
||||
|
||||
修改`项目目录`下`data`目录下的`.config.yaml`文件,配置本项目必须的两个配置。
|
||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的两个配置。
|
||||
|
||||
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||
- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。
|
||||
@@ -292,4 +294,19 @@ LLM:
|
||||
|
||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
+9
-34
@@ -8,39 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||
```
|
||||
2、编译docker镜像
|
||||
```
|
||||
# 普通编译
|
||||
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip .
|
||||
```
|
||||
3、测试本地镜像
|
||||
```
|
||||
docker stop xiaozhi-esp32-server
|
||||
docker rm xiaozhi-esp32-server
|
||||
|
||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/data/.config.yaml:/opt/xiaozhi-esp32-server/config.yaml xiaozhi-esp32-server:local
|
||||
|
||||
docker logs -f xiaozhi-esp32-server
|
||||
#进入项目根目录
|
||||
# 编译server
|
||||
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||
# 编译web
|
||||
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||
|
||||
# 编译完成后,可以使用docker-compose启动项目
|
||||
# docker-compose.yml你需要修改成自己编译的镜像版本
|
||||
cd main/xiaozhi-server
|
||||
docker-compose up -d
|
||||
```
|
||||
5、发布腾讯云镜像
|
||||
```
|
||||
# amd64
|
||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
||||
|
||||
# arm64
|
||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
||||
|
||||
# 推送最新版本
|
||||
docker manifest rm ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
docker manifest create ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64 ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64 --amend
|
||||
docker manifest inspect ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
docker manifest push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
|
||||
```
|
||||
6、运行线上镜像
|
||||
```
|
||||
cd /Users/hrz/myworkspace/docker-java-env/thirddata/
|
||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
docker logs -f xiaozhi-esp32-server
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 8002;
|
||||
server_name localhost;
|
||||
|
||||
# 静态资源服务(Vue项目)
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API反向代理(Java项目)
|
||||
location /xiaozhi-esp32-api/ {
|
||||
proxy_pass http://127.0.0.1:8003;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cookie_path /api/ /;
|
||||
proxy_set_header Referer $http_referer;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
|
||||
proxy_connect_timeout 10;
|
||||
proxy_send_timeout 10;
|
||||
proxy_read_timeout 10;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# 启动Java后端(docker内监听8003端口)
|
||||
java -jar /app/xiaozhi-esp32-api.jar \
|
||||
--server.port=8003 \
|
||||
--spring.datasource.druid.url=${SPRING_DATASOURCE_DRUID_URL} \
|
||||
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
|
||||
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
|
||||
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
|
||||
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
|
||||
|
||||
# 启动Nginx(前台运行保持容器存活)
|
||||
nginx -g 'daemon off;'
|
||||
@@ -87,3 +87,19 @@ https://espressif.github.io/esp-launchpad/
|
||||
|
||||
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
||||
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
|
||||
|
||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
||||
|
||||
# 项目目录介绍
|
||||
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
|
||||
|
||||
如果你会开发,我们非常欢迎您的加入。
|
||||
|
||||
```
|
||||
xiaozhi-esp32-server
|
||||
├─ xiaozhi-server 8000 端口 Python语言开发 负责与esp32通信
|
||||
├─ manager-web 8001 端口 Node.js+Vue开发 负责提供控制台的web界面
|
||||
├─ manager-api 8002 端口 Java语言开发 负责提供控制台的api
|
||||
```
|
||||
|
||||
# xiaozhi-server 接口协议
|
||||
|
||||
[虾哥团队通信协议:Websocket 连接](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh)
|
||||
|
||||
# manager-web 、manager-api接口协议
|
||||
|
||||
[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)
|
||||
|
||||
[前端页面设计图](https://codesign.qq.com/app/s/526108506410828)
|
||||
@@ -0,0 +1,72 @@
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
||||
# 项目介绍
|
||||
|
||||
manager-api 该项目基于SpringBoot框架开发。
|
||||
|
||||
开发使用代码编辑器,导入项目时,选择`manager-api`文件夹作为项目目录
|
||||
|
||||
参照[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)开发
|
||||
|
||||
# 开发环境
|
||||
JDK 21
|
||||
Maven 3.8+
|
||||
MySQL 8.0+
|
||||
Vue 3.x
|
||||
|
||||
# 创建数据库
|
||||
|
||||
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
||||
|
||||
```sql
|
||||
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
如果还没有MySQL,你可以通过docker安装mysql
|
||||
|
||||
```
|
||||
docker run --name xiaozhi-esp32-server-db \
|
||||
-e MYSQL_ROOT_PASSWORD=123456 \
|
||||
-p 3306:3306 \
|
||||
-e MYSQL_DATABASE=xiaozhi_esp32_server \
|
||||
-e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" \
|
||||
-d mysql:latest
|
||||
```
|
||||
|
||||
# 确认项目数据库连接信息
|
||||
|
||||
在`src/main/resources/application-dev.yml`中配置数据库连接信息
|
||||
|
||||
```
|
||||
spring:
|
||||
datasource:
|
||||
username: root
|
||||
password: 123456
|
||||
```
|
||||
|
||||
# 测试启动
|
||||
|
||||
本项目为SpringBoot项目,启动方式为:
|
||||
打开`Application.java`运行`Main`方法启动
|
||||
|
||||
```
|
||||
路径地址:
|
||||
src/main/java/xiaozhi/AdminApplication.java
|
||||
```
|
||||
|
||||
# 打包编译
|
||||
|
||||
执行以下命令生产jar包
|
||||
|
||||
```
|
||||
mvn install
|
||||
```
|
||||
|
||||
把jar包放在服务器上,执行
|
||||
|
||||
```
|
||||
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev >/dev/null &
|
||||
```
|
||||
|
||||
# 接口文档
|
||||
启动后打开:http://localhost:8002/xiaozhi-esp32-api/doc.html
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>xiaozhi</groupId>
|
||||
<artifactId>xiaozhi-esp32-api</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.0.1</version>
|
||||
<name>xiaozhi-esp32-api</name>
|
||||
<description>小智后台管理系统</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.3</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>21</java.version>
|
||||
<junit.version>5.12.0</junit.version>
|
||||
<druid.version>1.2.20</druid.version>
|
||||
<mybatisplus.version>3.5.5</mybatisplus.version>
|
||||
<hutool.version>5.8.24</hutool.version>
|
||||
<jsoup.version>1.19.1</jsoup.version>
|
||||
<knife4j.version>4.6.0</knife4j.version>
|
||||
<shiro.version>2.0.2</shiro.version>
|
||||
<captcha.version>1.6.2</captcha.version>
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
<easyexcel.version>3.3.2</easyexcel.version>
|
||||
<liquibase-core.version>4.20.0</liquibase-core.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-web</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-web</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- 验证码工具包 -->
|
||||
<dependency>
|
||||
<groupId>com.github.whvcse</groupId>
|
||||
<artifactId>easy-captcha</artifactId>
|
||||
<version>${captcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- mysql驱动 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<version>${liquibase-core.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>${mybatisplus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.xingfudeshi</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- 阿里云maven仓库 -->
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package xiaozhi;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class AdminApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminApplication.class, args);
|
||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/doc.html");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package xiaozhi.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据过滤注解
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataFilter {
|
||||
/**
|
||||
* 表的别名
|
||||
*/
|
||||
String tableAlias() default "";
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
String userId() default "creator";
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
String deptId() default "dept_id";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package xiaozhi.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 操作日志注解
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface LogOperation {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package xiaozhi.common.aspect;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import xiaozhi.common.annotation.DataFilter;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.interceptor.DataScope;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据过滤,切面处理类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class DataFilterAspect {
|
||||
|
||||
@Pointcut("@annotation(xiaozhi.common.annotation.DataFilter)")
|
||||
public void dataFilterCut() {
|
||||
|
||||
}
|
||||
|
||||
@Before("dataFilterCut()")
|
||||
public void dataFilter(JoinPoint point) {
|
||||
Object params = point.getArgs()[0];
|
||||
if (params != null && params instanceof Map) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
//如果是超级管理员,则不进行数据过滤
|
||||
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//否则进行数据过滤
|
||||
Map map = (Map) params;
|
||||
String sqlFilter = getSqlFilter(user, point);
|
||||
map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据过滤的SQL
|
||||
*/
|
||||
private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
|
||||
DataFilter dataFilter = method.getAnnotation(DataFilter.class);
|
||||
|
||||
//获取表的别名
|
||||
String tableAlias = dataFilter.tableAlias();
|
||||
if (StringUtils.isNotBlank(tableAlias)) {
|
||||
tableAlias += ".";
|
||||
}
|
||||
|
||||
StringBuilder sqlFilter = new StringBuilder();
|
||||
sqlFilter.append(" (");
|
||||
|
||||
//部门ID列表
|
||||
List<Long> deptIdList = user.getDeptIdList();
|
||||
if (CollUtil.isNotEmpty(deptIdList)) {
|
||||
sqlFilter.append(tableAlias).append(dataFilter.deptId());
|
||||
|
||||
sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")");
|
||||
}
|
||||
|
||||
//查询本人数据
|
||||
if (CollUtil.isNotEmpty(deptIdList)) {
|
||||
sqlFilter.append(" or ");
|
||||
}
|
||||
sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId());
|
||||
|
||||
sqlFilter.append(")");
|
||||
|
||||
return sqlFilter.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package xiaozhi.common.aspect;
|
||||
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Redis切面处理类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class RedisAspect {
|
||||
/**
|
||||
* 是否开启redis缓存 true开启 false关闭
|
||||
*/
|
||||
@Value("${renren.redis.open}")
|
||||
private boolean open;
|
||||
|
||||
@Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
Object result = null;
|
||||
if (open) {
|
||||
try {
|
||||
result = point.proceed();
|
||||
} catch (Exception e) {
|
||||
log.error("redis error", e);
|
||||
throw new RenException(ErrorCode.REDIS_ERROR);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package xiaozhi.common.config;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import xiaozhi.common.interceptor.DataFilterInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* mybatis-plus配置
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
|
||||
// 数据权限
|
||||
mybatisPlusInterceptor.addInnerInterceptor(new DataFilterInterceptor());
|
||||
// 分页插件
|
||||
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
||||
// 乐观锁
|
||||
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
// 防止全表更新与删除
|
||||
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
|
||||
|
||||
return mybatisPlusInterceptor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package xiaozhi.common.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger配置
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi userApi() {
|
||||
String[] paths = {"/**"};
|
||||
return GroupedOpenApi.builder().group("xiaozhi")
|
||||
.pathsToMatch(paths).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI().info(new Info()
|
||||
.title("xiaozhi-esp32-manager-api")
|
||||
.description("xiaozhi-esp32-manager-api文档")
|
||||
.version("3.0")
|
||||
.termsOfService("https://127.0.0.1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package xiaozhi.common.constant;
|
||||
|
||||
/**
|
||||
* 常量
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public interface Constant {
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
int SUCCESS = 1;
|
||||
/**
|
||||
* 失败
|
||||
*/
|
||||
int FAIL = 0;
|
||||
/**
|
||||
* OK
|
||||
*/
|
||||
String OK = "OK";
|
||||
/**
|
||||
* 用户标识
|
||||
*/
|
||||
String USER_KEY = "userId";
|
||||
/**
|
||||
* 菜单根节点标识
|
||||
*/
|
||||
Long MENU_ROOT = 0L;
|
||||
/**
|
||||
* 部门根节点标识
|
||||
*/
|
||||
Long DEPT_ROOT = 0L;
|
||||
/**
|
||||
* 数据字典根节点标识
|
||||
*/
|
||||
Long DICT_ROOT = 0L;
|
||||
/**
|
||||
* 升序
|
||||
*/
|
||||
String ASC = "asc";
|
||||
/**
|
||||
* 降序
|
||||
*/
|
||||
String DESC = "desc";
|
||||
/**
|
||||
* 创建时间字段名
|
||||
*/
|
||||
String CREATE_DATE = "create_date";
|
||||
|
||||
/**
|
||||
* 创建时间字段名
|
||||
*/
|
||||
String ID = "id";
|
||||
|
||||
/**
|
||||
* 数据权限过滤
|
||||
*/
|
||||
String SQL_FILTER = "sqlFilter";
|
||||
|
||||
/**
|
||||
* 当前页码
|
||||
*/
|
||||
String PAGE = "page";
|
||||
/**
|
||||
* 每页显示记录数
|
||||
*/
|
||||
String LIMIT = "limit";
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
String ORDER_FIELD = "orderField";
|
||||
/**
|
||||
* 排序方式
|
||||
*/
|
||||
String ORDER = "order";
|
||||
/**
|
||||
* token header
|
||||
*/
|
||||
String TOKEN_HEADER = "token";
|
||||
|
||||
/**
|
||||
* 路径分割符
|
||||
*/
|
||||
String FILE_EXTENSION_SEG = ".";
|
||||
|
||||
enum SysBaseParam {
|
||||
/**
|
||||
* 系统全称
|
||||
*/
|
||||
SYS_NAME("SYS_NAME"),
|
||||
/**
|
||||
* 系统简称
|
||||
*/
|
||||
SYS_SHORT_NAME("SYS_SHORT_NAME"),
|
||||
/**
|
||||
* 系统描述
|
||||
*/
|
||||
SYS_DES("SYS_DES"),
|
||||
/**
|
||||
* 登录失败几次锁定
|
||||
*/
|
||||
LOGIN_LOCK_COUNT("LOGIN_LOCK_COUNT"),
|
||||
/**
|
||||
* 账号失败锁定分钟数
|
||||
*/
|
||||
LOGIN_LOCK_TIME("LOGIN_LOCK_TIME"),
|
||||
/**
|
||||
* TOKEN强验证
|
||||
*/
|
||||
SYS_TOKEN_SECURITY("SYS_TOKEN_SECURITY");
|
||||
|
||||
private String value;
|
||||
|
||||
SysBaseParam(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据状态
|
||||
*/
|
||||
enum DataOperation {
|
||||
/**
|
||||
* 插入
|
||||
*/
|
||||
INSERT("I"),
|
||||
/**
|
||||
* 已修改
|
||||
*/
|
||||
UPDATE("U"),
|
||||
/**
|
||||
* 已删除
|
||||
*/
|
||||
DELETE("D");
|
||||
|
||||
private String value;
|
||||
|
||||
DataOperation(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package xiaozhi.common.convert;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日期转换
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Component
|
||||
public class DateConverter implements Converter<String, Date> {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DateConverter.class);
|
||||
private static final List<String> formatList = new ArrayList<>(5);
|
||||
|
||||
static {
|
||||
formatList.add("yyyy-MM");
|
||||
formatList.add("yyyy-MM-dd");
|
||||
formatList.add("yyyy-MM-dd hh:mm");
|
||||
formatList.add("yyyy-MM-dd hh:mm:ss");
|
||||
formatList.add("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date convert(String source) {
|
||||
String value = source.trim();
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source.matches("^\\d{4}-\\d{1,2}$")) {
|
||||
return parseDate(source, formatList.get(0));
|
||||
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) {
|
||||
return parseDate(source, formatList.get(1));
|
||||
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) {
|
||||
return parseDate(source, formatList.get(2));
|
||||
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) {
|
||||
return parseDate(source, formatList.get(3));
|
||||
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}.*T.*\\d{1,2}:\\d{1,2}:\\d{1,2}.*..*$")) {
|
||||
return parseDate(source, formatList.get(4));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid boolean value '" + source + "'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*
|
||||
* @param dateStr String 字符型日期
|
||||
* @param format String 格式
|
||||
* @return Date 日期
|
||||
*/
|
||||
public Date parseDate(String dateStr, String format) {
|
||||
Date date = null;
|
||||
try {
|
||||
DateFormat dateFormat = new SimpleDateFormat(format);
|
||||
date = dateFormat.parse(dateStr);
|
||||
} catch (Exception e) {
|
||||
logger.error("Formatted date with date: {} and format : {} ", dateStr, format);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package xiaozhi.common.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* 基础Dao
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public interface BaseDao<T> extends BaseMapper<T> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package xiaozhi.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 基础实体类,所有实体都需要继承
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Data
|
||||
public abstract class BaseEntity implements Serializable {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createDate;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package xiaozhi.common.exception;
|
||||
|
||||
/**
|
||||
* 错误编码,由5位数字组成,前2位为模块编码,后3位为业务编码
|
||||
* <p>
|
||||
* 如:10001(10代表系统模块,001代表业务代码)
|
||||
* </p>
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public interface ErrorCode {
|
||||
int INTERNAL_SERVER_ERROR = 500;
|
||||
int UNAUTHORIZED = 401;
|
||||
int FORBIDDEN = 403;
|
||||
|
||||
int NOT_NULL = 10001;
|
||||
int DB_RECORD_EXISTS = 10002;
|
||||
int PARAMS_GET_ERROR = 10003;
|
||||
int ACCOUNT_PASSWORD_ERROR = 10004;
|
||||
int ACCOUNT_DISABLE = 10005;
|
||||
int IDENTIFIER_NOT_NULL = 10006;
|
||||
int CAPTCHA_ERROR = 10007;
|
||||
int SUB_MENU_EXIST = 10008;
|
||||
int PASSWORD_ERROR = 10009;
|
||||
int SUPERIOR_DEPT_ERROR = 10011;
|
||||
int SUPERIOR_MENU_ERROR = 10012;
|
||||
int DATA_SCOPE_PARAMS_ERROR = 10013;
|
||||
int DEPT_SUB_DELETE_ERROR = 10014;
|
||||
int DEPT_USER_DELETE_ERROR = 10015;
|
||||
int UPLOAD_FILE_EMPTY = 10019;
|
||||
int TOKEN_NOT_EMPTY = 10020;
|
||||
int TOKEN_INVALID = 10021;
|
||||
int ACCOUNT_LOCK = 10022;
|
||||
int OSS_UPLOAD_FILE_ERROR = 10024;
|
||||
int REDIS_ERROR = 10027;
|
||||
int JOB_ERROR = 10028;
|
||||
int INVALID_SYMBOL = 10029;
|
||||
|
||||
// 密码相关错误码
|
||||
int PASSWORD_LENGTH_ERROR = 10030;
|
||||
int PASSWORD_WEAK_ERROR = 10031;
|
||||
int DEL_MYSELF_ERROR = 10032;
|
||||
// 验证码错误
|
||||
int VERIFICATION_CODE = 10033;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package xiaozhi.common.exception;
|
||||
|
||||
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
|
||||
/**
|
||||
* 自定义异常
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class RenException extends RuntimeException {
|
||||
|
||||
|
||||
private int code;
|
||||
private String msg;
|
||||
|
||||
public RenException(int code) {
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code);
|
||||
}
|
||||
|
||||
public RenException(int code, String... params) {
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code, params);
|
||||
}
|
||||
|
||||
public RenException(int code, Throwable e) {
|
||||
super(e);
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code);
|
||||
}
|
||||
|
||||
public RenException(int code, Throwable e, String... params) {
|
||||
super(e);
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code, params);
|
||||
}
|
||||
|
||||
public RenException(String msg) {
|
||||
super(msg);
|
||||
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public RenException(String msg, Throwable e) {
|
||||
super(msg, e);
|
||||
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package xiaozhi.common.exception;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import xiaozhi.common.utils.Result;
|
||||
|
||||
|
||||
/**
|
||||
* 异常处理器
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@RestControllerAdvice
|
||||
public class RenExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理自定义异常
|
||||
*/
|
||||
@ExceptionHandler(RenException.class)
|
||||
public Result handleRenException(RenException ex) {
|
||||
Result result = new Result();
|
||||
result.error(ex.getCode(), ex.getMsg());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ExceptionHandler(DuplicateKeyException.class)
|
||||
public Result handleDuplicateKeyException(DuplicateKeyException ex) {
|
||||
Result result = new Result();
|
||||
result.error(ErrorCode.DB_RECORD_EXISTS);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ExceptionHandler(UnauthorizedException.class)
|
||||
public Result handleUnauthorizedException(UnauthorizedException ex) {
|
||||
Result result = new Result();
|
||||
result.error(ErrorCode.FORBIDDEN);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result handleException(Exception ex) {
|
||||
log.error(ex.getMessage(), ex);
|
||||
|
||||
return new Result().error();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package xiaozhi.common.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 公共字段,自动填充值
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Component
|
||||
public class FieldMetaObjectHandler implements MetaObjectHandler {
|
||||
private final static String CREATE_DATE = "createDate";
|
||||
private final static String CREATOR = "creator";
|
||||
private final static String UPDATE_DATE = "updateDate";
|
||||
private final static String UPDATER = "updater";
|
||||
|
||||
private final static String DATA_OPERATION = "dataOperation";
|
||||
private final static String DEPT_ID = "deptId";
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
Date date = new Date();
|
||||
|
||||
//创建者
|
||||
strictInsertFill(metaObject, CREATOR, Long.class, user.getId());
|
||||
//创建时间
|
||||
strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
|
||||
|
||||
//创建者所属部门
|
||||
strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId());
|
||||
|
||||
//更新者
|
||||
strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
|
||||
//更新时间
|
||||
strictInsertFill(metaObject, UPDATE_DATE, Date.class, date);
|
||||
|
||||
//数据标识
|
||||
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.INSERT.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
//更新者
|
||||
strictUpdateFill(metaObject, UPDATER, Long.class, SecurityUser.getUserId());
|
||||
//更新时间
|
||||
strictUpdateFill(metaObject, UPDATE_DATE, Date.class, new Date());
|
||||
|
||||
//数据标识
|
||||
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.UPDATE.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package xiaozhi.common.interceptor;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.StringValue;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据过滤
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class DataFilterInterceptor implements InnerInterceptor {
|
||||
|
||||
@Override
|
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
|
||||
DataScope scope = getDataScope(parameter);
|
||||
// 不进行数据过滤
|
||||
if (scope == null || StrUtil.isBlank(scope.getSqlFilter())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 拼接新SQL
|
||||
String buildSql = getSelect(boundSql.getSql(), scope);
|
||||
|
||||
// 重写SQL
|
||||
PluginUtils.mpBoundSql(boundSql).sql(buildSql);
|
||||
}
|
||||
|
||||
private DataScope getDataScope(Object parameter) {
|
||||
if (parameter == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断参数里是否有DataScope对象
|
||||
if (parameter instanceof Map) {
|
||||
Map<?, ?> parameterMap = (Map<?, ?>) parameter;
|
||||
for (Map.Entry entry : parameterMap.entrySet()) {
|
||||
if (entry.getValue() != null && entry.getValue() instanceof DataScope) {
|
||||
return (DataScope) entry.getValue();
|
||||
}
|
||||
}
|
||||
} else if (parameter instanceof DataScope) {
|
||||
return (DataScope) parameter;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getSelect(String buildSql, DataScope scope) {
|
||||
try {
|
||||
Select select = (Select) CCJSqlParserUtil.parse(buildSql);
|
||||
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
|
||||
|
||||
Expression expression = plainSelect.getWhere();
|
||||
if (expression == null) {
|
||||
plainSelect.setWhere(new StringValue(scope.getSqlFilter()));
|
||||
} else {
|
||||
AndExpression andExpression = new AndExpression(expression, new StringValue(scope.getSqlFilter()));
|
||||
plainSelect.setWhere(andExpression);
|
||||
}
|
||||
|
||||
return select.toString().replaceAll("'", "");
|
||||
} catch (JSQLParserException e) {
|
||||
return buildSql;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package xiaozhi.common.interceptor;
|
||||
|
||||
/**
|
||||
* 数据范围
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class DataScope {
|
||||
private String sqlFilter;
|
||||
|
||||
public DataScope(String sqlFilter) {
|
||||
this.sqlFilter = sqlFilter;
|
||||
}
|
||||
|
||||
public String getSqlFilter() {
|
||||
return sqlFilter;
|
||||
}
|
||||
|
||||
public void setSqlFilter(String sqlFilter) {
|
||||
this.sqlFilter = sqlFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.sqlFilter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package xiaozhi.common.page;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页工具类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分页数据")
|
||||
public class PageData<T> implements Serializable {
|
||||
@Schema(description = "总记录数")
|
||||
private int total;
|
||||
|
||||
@Schema(description = "列表数据")
|
||||
private List<T> list;
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*
|
||||
* @param list 列表数据
|
||||
* @param total 总记录数
|
||||
*/
|
||||
public PageData(List<T> list, long total) {
|
||||
this.list = list;
|
||||
this.total = (int) total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package xiaozhi.common.page;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 令牌信息
|
||||
*
|
||||
* @author Jack
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "令牌信息")
|
||||
public class TokenDTO implements Serializable {
|
||||
|
||||
|
||||
@Schema(description = "密码")
|
||||
private String token;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private int expire;
|
||||
|
||||
@Schema(description = "客户端指纹")
|
||||
private String clientHash;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package xiaozhi.common.redis;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis配置
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
@Resource
|
||||
private RedisConnectionFactory factory;
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate() {
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||
redisTemplate.setValueSerializer(RedisSerializer.json());
|
||||
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
|
||||
redisTemplate.setHashValueSerializer(RedisSerializer.json());
|
||||
redisTemplate.setConnectionFactory(factory);
|
||||
|
||||
return redisTemplate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package xiaozhi.common.redis;
|
||||
|
||||
/**
|
||||
* Redis Key 常量类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class RedisKeys {
|
||||
/**
|
||||
* 系统参数Key
|
||||
*/
|
||||
public static String getSysParamsKey() {
|
||||
return "sys:params";
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码Key
|
||||
*/
|
||||
public static String getCaptchaKey(String uuid) {
|
||||
return "sys:captcha:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录用户Key
|
||||
*/
|
||||
public static String getSecurityUserKey(Long id) {
|
||||
return "sys:security:user:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统日志Key
|
||||
*/
|
||||
public static String getSysLogKey() {
|
||||
return "sys:log";
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统资源Key
|
||||
*/
|
||||
public static String getSysResourceKey() {
|
||||
return "sys:resource";
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户菜单导航Key
|
||||
*/
|
||||
public static String getUserMenuNavKey(Long userId) {
|
||||
return "sys:user:nav:" + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户权限标识Key
|
||||
*/
|
||||
public static String getUserPermissionsKey(Long userId) {
|
||||
return "sys:user:permissions:" + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登陆错误次数
|
||||
*
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
public static String getUserLoginErrorCountKey(String username) {
|
||||
return "sys:user:login:error:" + username;
|
||||
}
|
||||
|
||||
public static String getUserInfoKey(Long userId) {
|
||||
return "sys:user:" + userId;
|
||||
}
|
||||
|
||||
public static String getDataScopeListKey(Long userId) {
|
||||
return "sys:user:data:scope:" + userId;
|
||||
}
|
||||
|
||||
public static String getSysUserName(Long id) {
|
||||
return "sys:user:name" + id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package xiaozhi.common.redis;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis工具类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Component
|
||||
public class RedisUtils {
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 默认过期时长为24小时,单位:秒
|
||||
*/
|
||||
public final static long DEFAULT_EXPIRE = 60 * 60 * 24L;
|
||||
/**
|
||||
* 过期时长为1小时,单位:秒
|
||||
*/
|
||||
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
|
||||
/**
|
||||
* 过期时长为6小时,单位:秒
|
||||
*/
|
||||
public final static long HOUR_SIX_EXPIRE = 60 * 60 * 6L;
|
||||
/**
|
||||
* 不设置过期时长
|
||||
*/
|
||||
public final static long NOT_EXPIRE = -1L;
|
||||
|
||||
public void set(String key, Object value, long expire) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
if (expire != NOT_EXPIRE) {
|
||||
expire(key, expire);
|
||||
}
|
||||
}
|
||||
|
||||
public void set(String key, Object value) {
|
||||
set(key, value, DEFAULT_EXPIRE);
|
||||
}
|
||||
|
||||
public Object get(String key, long expire) {
|
||||
Object value = redisTemplate.opsForValue().get(key);
|
||||
if (expire != NOT_EXPIRE) {
|
||||
expire(key, expire);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public Object get(String key) {
|
||||
return get(key, NOT_EXPIRE);
|
||||
}
|
||||
|
||||
public void delete(String key) {
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
public void delete(Collection<String> keys) {
|
||||
redisTemplate.delete(keys);
|
||||
}
|
||||
|
||||
public Object hGet(String key, String field) {
|
||||
return redisTemplate.opsForHash().get(key, field);
|
||||
}
|
||||
|
||||
public Map<String, Object> hGetAll(String key) {
|
||||
HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
|
||||
return hashOperations.entries(key);
|
||||
}
|
||||
|
||||
public void hMSet(String key, Map<String, Object> map) {
|
||||
hMSet(key, map, DEFAULT_EXPIRE);
|
||||
}
|
||||
|
||||
public void hMSet(String key, Map<String, Object> map, long expire) {
|
||||
redisTemplate.opsForHash().putAll(key, map);
|
||||
|
||||
if (expire != NOT_EXPIRE) {
|
||||
expire(key, expire);
|
||||
}
|
||||
}
|
||||
|
||||
public void hSet(String key, String field, Object value) {
|
||||
hSet(key, field, value, DEFAULT_EXPIRE);
|
||||
}
|
||||
|
||||
public void hSet(String key, String field, Object value, long expire) {
|
||||
redisTemplate.opsForHash().put(key, field, value);
|
||||
|
||||
if (expire != NOT_EXPIRE) {
|
||||
expire(key, expire);
|
||||
}
|
||||
}
|
||||
|
||||
public void expire(String key, long expire) {
|
||||
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void hDel(String key, Object... fields) {
|
||||
redisTemplate.opsForHash().delete(key, fields);
|
||||
}
|
||||
|
||||
public void leftPush(String key, Object value) {
|
||||
leftPush(key, value, DEFAULT_EXPIRE);
|
||||
}
|
||||
|
||||
public void leftPush(String key, Object value, long expire) {
|
||||
redisTemplate.opsForList().leftPush(key, value);
|
||||
|
||||
if (expire != NOT_EXPIRE) {
|
||||
expire(key, expire);
|
||||
}
|
||||
}
|
||||
|
||||
public Object rightPop(String key) {
|
||||
return redisTemplate.opsForList().rightPop(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package xiaozhi.common.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 基础服务接口,所有Service接口都要继承
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public interface BaseService<T> {
|
||||
Class<T> currentModelClass();
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入一条记录(选择字段,策略插入)
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
boolean insert(T entity);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
*/
|
||||
boolean insertBatch(Collection<T> entityList);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 插入批次数量
|
||||
*/
|
||||
boolean insertBatch(Collection<T> entityList, int batchSize);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 选择修改
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
boolean updateById(T entity);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 whereEntity 条件,更新记录
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
* @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
|
||||
*/
|
||||
boolean update(T entity, Wrapper<T> updateWrapper);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据ID 批量更新
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
*/
|
||||
boolean updateBatchById(Collection<T> entityList);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据ID 批量更新
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 更新批次数量
|
||||
*/
|
||||
boolean updateBatchById(Collection<T> entityList, int batchSize);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 查询
|
||||
* </p>
|
||||
*
|
||||
* @param id 主键ID
|
||||
*/
|
||||
T selectById(Serializable id);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 删除
|
||||
* </p>
|
||||
*
|
||||
* @param id 主键ID
|
||||
*/
|
||||
boolean deleteById(Serializable id);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 删除(根据ID 批量删除)
|
||||
* </p>
|
||||
*
|
||||
* @param idList 主键ID列表
|
||||
*/
|
||||
boolean deleteBatchIds(Collection<? extends Serializable> idList);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.common.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* CRUD基础服务接口
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public interface CrudService<T, D> extends BaseService<T> {
|
||||
|
||||
PageData<D> page(Map<String, Object> params);
|
||||
|
||||
List<D> list(Map<String, Object> params);
|
||||
|
||||
D get(Serializable id);
|
||||
|
||||
void save(D dto);
|
||||
|
||||
void update(D dto);
|
||||
|
||||
void delete(Serializable[] ids);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import org.apache.ibatis.binding.MapperMethod;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
import org.apache.ibatis.logging.LogFactory;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* 基础服务类,所有Service都要继承
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
|
||||
@Autowired
|
||||
protected M baseDao;
|
||||
protected Log log = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* 获取分页对象
|
||||
*
|
||||
* @param params 分页查询参数
|
||||
* @param defaultOrderField 默认排序字段
|
||||
* @param isAsc 排序方式
|
||||
*/
|
||||
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||
//分页参数
|
||||
long curPage = 1;
|
||||
long limit = 10;
|
||||
|
||||
if (params.get(Constant.PAGE) != null) {
|
||||
curPage = Long.parseLong((String) params.get(Constant.PAGE));
|
||||
}
|
||||
if (params.get(Constant.LIMIT) != null) {
|
||||
limit = Long.parseLong((String) params.get(Constant.LIMIT));
|
||||
}
|
||||
|
||||
//分页对象
|
||||
Page<T> page = new Page<>(curPage, limit);
|
||||
|
||||
//分页参数
|
||||
params.put(Constant.PAGE, page);
|
||||
|
||||
//排序字段
|
||||
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
||||
String order = (String) params.get(Constant.ORDER);
|
||||
|
||||
//前端字段排序
|
||||
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
||||
if (Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.asc(orderField));
|
||||
} else {
|
||||
return page.addOrder(OrderItem.desc(orderField));
|
||||
}
|
||||
}
|
||||
|
||||
//没有排序字段,则不排序
|
||||
if (StringUtils.isBlank(defaultOrderField)) {
|
||||
return page;
|
||||
}
|
||||
|
||||
//默认排序
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target) {
|
||||
List<T> targetList = ConvertUtils.sourceToTarget(list, target);
|
||||
|
||||
return new PageData<>(targetList, total);
|
||||
}
|
||||
|
||||
protected <T> PageData<T> getPageData(IPage page, Class<T> target) {
|
||||
return getPageData(page.getRecords(), page.getTotal(), target);
|
||||
}
|
||||
|
||||
protected void paramsToLike(Map<String, Object> params, String... likes) {
|
||||
for (String like : likes) {
|
||||
String val = (String) params.get(like);
|
||||
if (StringUtils.isNotBlank(val)) {
|
||||
params.put(like, "%" + val + "%");
|
||||
} else {
|
||||
params.put(like, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 判断数据库操作是否成功
|
||||
* </p>
|
||||
* <p>
|
||||
* 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
|
||||
* </p>
|
||||
*
|
||||
* @param result 数据库操作返回影响条数
|
||||
* @return boolean
|
||||
*/
|
||||
protected static boolean retBool(Integer result) {
|
||||
return SqlHelper.retBool(result);
|
||||
}
|
||||
|
||||
protected Class<M> currentMapperClass() {
|
||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> currentModelClass() {
|
||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||
}
|
||||
|
||||
protected String getSqlStatement(SqlMethod sqlMethod) {
|
||||
return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insert(T entity) {
|
||||
return BaseServiceImpl.retBool(baseDao.insert(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insertBatch(Collection<T> entityList) {
|
||||
return insertBatch(entityList, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insertBatch(Collection<T> entityList, int batchSize) {
|
||||
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
|
||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量操作
|
||||
*/
|
||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
|
||||
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateById(T entity) {
|
||||
return BaseServiceImpl.retBool(baseDao.updateById(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(T entity, Wrapper<T> updateWrapper) {
|
||||
return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateBatchById(Collection<T> entityList) {
|
||||
return updateBatchById(entityList, 30);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||
String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
|
||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
||||
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
||||
param.put(Constants.ENTITY, entity);
|
||||
sqlSession.update(sqlStatement, param);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public T selectById(Serializable id) {
|
||||
return baseDao.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteById(Serializable id) {
|
||||
return SqlHelper.retBool(baseDao.deleteById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
|
||||
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.CrudService;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* CRUD基础服务类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> implements CrudService<T, D> {
|
||||
|
||||
protected Class<D> currentDtoClass() {
|
||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<D> page(Map<String, Object> params) {
|
||||
IPage<T> page = baseDao.selectPage(
|
||||
getPage(params, null, false),
|
||||
getWrapper(params)
|
||||
);
|
||||
|
||||
return getPageData(page, currentDtoClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<D> list(Map<String, Object> params) {
|
||||
List<T> entityList = baseDao.selectList(getWrapper(params));
|
||||
|
||||
return ConvertUtils.sourceToTarget(entityList, currentDtoClass());
|
||||
}
|
||||
|
||||
public abstract QueryWrapper<T> getWrapper(Map<String, Object> params);
|
||||
|
||||
@Override
|
||||
public D get(Serializable id) {
|
||||
T entity = baseDao.selectById(id);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, currentDtoClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(D dto) {
|
||||
T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
|
||||
insert(entity);
|
||||
|
||||
//copy主键值到dto
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(D dto) {
|
||||
T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
|
||||
updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Serializable[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package xiaozhi.common.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Data
|
||||
public class UserDetail implements Serializable {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String realName;
|
||||
private String headUrl;
|
||||
private Integer gender;
|
||||
private String email;
|
||||
private String mobile;
|
||||
private Long deptId;
|
||||
private String password;
|
||||
private Integer status;
|
||||
private Integer superAdmin;
|
||||
private String token;
|
||||
/**
|
||||
* 部门数据权限
|
||||
*/
|
||||
private List<Long> deptIdList;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 转换工具类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConvertUtils {
|
||||
public static <T> T sourceToTarget(Object source, Class<T> target) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
T targetObject = null;
|
||||
try {
|
||||
targetObject = target.newInstance();
|
||||
BeanUtils.copyProperties(source, targetObject);
|
||||
} catch (Exception e) {
|
||||
log.error("convert error ", e);
|
||||
}
|
||||
|
||||
return targetObject;
|
||||
}
|
||||
|
||||
public static <T> List<T> sourceToTarget(Collection<?> sourceList, Class<T> target) {
|
||||
if (sourceList == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List targetList = new ArrayList<>(sourceList.size());
|
||||
try {
|
||||
for (Object source : sourceList) {
|
||||
T targetObject = target.newInstance();
|
||||
BeanUtils.copyProperties(source, targetObject);
|
||||
targetList.add(targetObject);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("convert error ", e);
|
||||
}
|
||||
|
||||
return targetList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 日期处理
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class DateUtils {
|
||||
/**
|
||||
* 时间格式(yyyy-MM-dd)
|
||||
*/
|
||||
public final static String DATE_PATTERN = "yyyy-MM-dd";
|
||||
/**
|
||||
* 时间格式(yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/**
|
||||
* 日期格式化 日期格式为:yyyy-MM-dd
|
||||
*
|
||||
* @param date 日期
|
||||
* @return 返回yyyy-MM-dd格式日期
|
||||
*/
|
||||
public static String format(Date date) {
|
||||
return format(date, DATE_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化 日期格式为:yyyy-MM-dd
|
||||
*
|
||||
* @param date 日期
|
||||
* @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN
|
||||
* @return 返回yyyy-MM-dd格式日期
|
||||
*/
|
||||
public static String format(Date date, String pattern) {
|
||||
if (date != null) {
|
||||
SimpleDateFormat df = new SimpleDateFormat(pattern);
|
||||
return df.format(date);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期解析
|
||||
*
|
||||
* @param date 日期
|
||||
* @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN
|
||||
* @return 返回Date
|
||||
*/
|
||||
public static Date parse(String date, String pattern) {
|
||||
try {
|
||||
return new SimpleDateFormat(pattern).parse(date);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Http
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class HttpContextUtils {
|
||||
|
||||
public static HttpServletRequest getHttpServletRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
}
|
||||
|
||||
public static String getToken() {
|
||||
HttpServletRequest httpRequest = getHttpServletRequest();
|
||||
String token = httpRequest.getHeader(Constant.TOKEN_HEADER);
|
||||
|
||||
//如果header中不存在token,则从参数中获取token
|
||||
if (StringUtils.isBlank(token)) {
|
||||
token = httpRequest.getParameter(Constant.TOKEN_HEADER);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
public static Map<String, String> getParameterMap(HttpServletRequest request) {
|
||||
Enumeration<String> parameters = request.getParameterNames();
|
||||
|
||||
Map<String, String> params = new HashMap<>();
|
||||
while (parameters.hasMoreElements()) {
|
||||
String parameter = parameters.nextElement();
|
||||
String value = request.getParameter(parameter);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
params.put(parameter, value);
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
public static String getDomain() {
|
||||
HttpServletRequest request = getHttpServletRequest();
|
||||
StringBuffer url = request.getRequestURL();
|
||||
return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
|
||||
}
|
||||
|
||||
public static String getOrigin() {
|
||||
HttpServletRequest request = getHttpServletRequest();
|
||||
return request.getHeader(HttpHeaders.ORIGIN);
|
||||
}
|
||||
|
||||
public static String getLanguage() {
|
||||
//默认语言
|
||||
String defaultLanguage = "zh-CN";
|
||||
//request
|
||||
HttpServletRequest request = getHttpServletRequest();
|
||||
if (request == null) {
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
//请求语言
|
||||
defaultLanguage = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE);
|
||||
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端的唯一标识
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getClientCode() {
|
||||
HttpServletRequest request = getHttpServletRequest();
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
String reqId = request.getHeader("User-Agent").toLowerCase();
|
||||
String ipAddr = IpUtils.getIpAddr(request);
|
||||
String date = DateUtils.format(new Date());
|
||||
reqId = DigestUtils.md5DigestAsHex((ipAddr + date + reqId).getBytes());
|
||||
return reqId;
|
||||
}
|
||||
|
||||
public static String getRequestURI() {
|
||||
HttpServletRequest request = getHttpServletRequest();
|
||||
String url = request.getRequestURI();
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* IP地址
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Slf4j
|
||||
public class IpUtils {
|
||||
/**
|
||||
* 获取IP地址
|
||||
* <p>
|
||||
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
|
||||
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
|
||||
*/
|
||||
public static String getIpAddr(HttpServletRequest request) {
|
||||
String unknown = "unknown";
|
||||
String ip = null;
|
||||
try {
|
||||
ip = request.getHeader("x-forwarded-for");
|
||||
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("IPUtils ERROR ", e);
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JSON 工具类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class JsonUtils {
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public static String toJsonString(Object object) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(object);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, clazz);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
|
||||
if (ArrayUtil.isEmpty(bytes)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(bytes, clazz);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
|
||||
try {
|
||||
return objectMapper.readValue(text, typeReference);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<T> parseArray(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
|
||||
/**
|
||||
* 国际化
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class MessageUtils {
|
||||
private static MessageSource messageSource;
|
||||
|
||||
static {
|
||||
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
|
||||
}
|
||||
|
||||
public static String getMessage(int code) {
|
||||
return getMessage(code, new String[0]);
|
||||
}
|
||||
|
||||
public static String getMessage(int code, String... params) {
|
||||
return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 获取配置文件信息
|
||||
* Copyright xin-nan.com
|
||||
*/
|
||||
@Component
|
||||
public class PropertiesUtils {
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "响应")
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
/**
|
||||
* 编码:0表示成功,其他值表示失败
|
||||
*/
|
||||
@Schema(description = "编码:0表示成功,其他值表示失败")
|
||||
private int code = 0;
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@Schema(description = "消息内容")
|
||||
private String msg = "success";
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
@Schema(description = "响应数据")
|
||||
private T data;
|
||||
|
||||
public Result<T> ok(T data) {
|
||||
this.setData(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean success() {
|
||||
return code == 0;
|
||||
}
|
||||
|
||||
public Result<T> error() {
|
||||
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
|
||||
this.msg = MessageUtils.getMessage(this.code);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Result<T> error(int code) {
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(this.code);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Result<T> error(int code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Result<T> error(String msg) {
|
||||
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
|
||||
this.msg = msg;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Spring Context 工具类
|
||||
*/
|
||||
@Component
|
||||
public class SpringContextUtils implements ApplicationContextAware {
|
||||
public static ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext)
|
||||
throws BeansException {
|
||||
SpringContextUtils.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public static Object getBean(String name) {
|
||||
return applicationContext.getBean(name);
|
||||
}
|
||||
|
||||
public static <T> T getBean(Class<T> requiredType) {
|
||||
return applicationContext.getBean(requiredType);
|
||||
}
|
||||
|
||||
public static <T> T getBean(String name, Class<T> requiredType) {
|
||||
return applicationContext.getBean(name, requiredType);
|
||||
}
|
||||
|
||||
public static boolean containsBean(String name) {
|
||||
return applicationContext.containsBean(name);
|
||||
}
|
||||
|
||||
public static boolean isSingleton(String name) {
|
||||
return applicationContext.isSingleton(name);
|
||||
}
|
||||
|
||||
public static Class<? extends Object> getType(String name) {
|
||||
return applicationContext.getType(name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 树节点,所有需要实现树节点的,都需要继承该类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Data
|
||||
public class TreeNode<T> implements Serializable {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 上级ID
|
||||
*/
|
||||
private Long pid;
|
||||
/**
|
||||
* 子节点列表
|
||||
*/
|
||||
private List<T> children = new ArrayList<>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 树形结构工具类,如:菜单、部门等
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class TreeUtils {
|
||||
|
||||
/**
|
||||
* 根据pid,构建树节点
|
||||
*/
|
||||
public static <T extends TreeNode> List<T> build(List<T> treeNodes, Long pid) {
|
||||
//pid不能为空
|
||||
AssertUtils.isNull(pid, "pid");
|
||||
|
||||
List<T> treeList = new ArrayList<>();
|
||||
for (T treeNode : treeNodes) {
|
||||
if (pid.equals(treeNode.getPid())) {
|
||||
treeList.add(findChildren(treeNodes, treeNode));
|
||||
}
|
||||
}
|
||||
|
||||
return treeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找子节点
|
||||
*/
|
||||
private static <T extends TreeNode> T findChildren(List<T> treeNodes, T rootNode) {
|
||||
for (T treeNode : treeNodes) {
|
||||
if (rootNode.getId().equals(treeNode.getPid())) {
|
||||
rootNode.getChildren().add(findChildren(treeNodes, treeNode));
|
||||
}
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树节点
|
||||
*/
|
||||
public static <T extends TreeNode> List<T> build(List<T> treeNodes) {
|
||||
List<T> result = new ArrayList<>();
|
||||
|
||||
//list转map
|
||||
Map<Long, T> nodeMap = new LinkedHashMap<>(treeNodes.size());
|
||||
for (T treeNode : treeNodes) {
|
||||
nodeMap.put(treeNode.getId(), treeNode);
|
||||
}
|
||||
|
||||
for (T node : nodeMap.values()) {
|
||||
T parent = nodeMap.get(node.getPid());
|
||||
if (parent != null && !(node.getId().equals(parent.getId()))) {
|
||||
parent.getChildren().add(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(node);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package xiaozhi.common.validator;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 校验工具类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class AssertUtils {
|
||||
|
||||
public static void isBlank(String str, String... params) {
|
||||
isBlank(str, ErrorCode.NOT_NULL, params);
|
||||
}
|
||||
|
||||
public static void isBlank(String str, Integer code, String... params) {
|
||||
if (code == null) {
|
||||
throw new RenException(ErrorCode.NOT_NULL, "code");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(str)) {
|
||||
throw new RenException(code, params);
|
||||
}
|
||||
}
|
||||
|
||||
public static void isNull(Object object, String... params) {
|
||||
isNull(object, ErrorCode.NOT_NULL, params);
|
||||
}
|
||||
|
||||
public static void isNull(Object object, Integer code, String... params) {
|
||||
if (code == null) {
|
||||
throw new RenException(ErrorCode.NOT_NULL, "code");
|
||||
}
|
||||
|
||||
if (object == null) {
|
||||
throw new RenException(code, params);
|
||||
}
|
||||
}
|
||||
|
||||
public static void isArrayEmpty(Object[] array, String... params) {
|
||||
isArrayEmpty(array, ErrorCode.NOT_NULL, params);
|
||||
}
|
||||
|
||||
public static void isArrayEmpty(Object[] array, Integer code, String... params) {
|
||||
if (code == null) {
|
||||
throw new RenException(ErrorCode.NOT_NULL, "code");
|
||||
}
|
||||
|
||||
if (ArrayUtil.isEmpty(array)) {
|
||||
throw new RenException(code, params);
|
||||
}
|
||||
}
|
||||
|
||||
public static void isListEmpty(List<?> list, String... params) {
|
||||
isListEmpty(list, ErrorCode.NOT_NULL, params);
|
||||
}
|
||||
|
||||
public static void isListEmpty(List<?> list, Integer code, String... params) {
|
||||
if (code == null) {
|
||||
throw new RenException(ErrorCode.NOT_NULL, "code");
|
||||
}
|
||||
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
throw new RenException(code, params);
|
||||
}
|
||||
}
|
||||
|
||||
public static void isMapEmpty(Map map, String... params) {
|
||||
isMapEmpty(map, ErrorCode.NOT_NULL, params);
|
||||
}
|
||||
|
||||
public static void isMapEmpty(Map map, Integer code, String... params) {
|
||||
if (code == null) {
|
||||
throw new RenException(ErrorCode.NOT_NULL, "code");
|
||||
}
|
||||
|
||||
if (MapUtil.isEmpty(map)) {
|
||||
throw new RenException(code, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package xiaozhi.common.validator;
|
||||
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.context.support.ResourceBundleMessageSource;
|
||||
import org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* hibernate-validator校验工具类
|
||||
* 参考文档:http://docs.jboss.org/hibernate/validator/6.0/reference/en-US/html_single/
|
||||
*/
|
||||
public class ValidatorUtils {
|
||||
|
||||
private static ResourceBundleMessageSource getMessageSource() {
|
||||
ResourceBundleMessageSource bundleMessageSource = new ResourceBundleMessageSource();
|
||||
bundleMessageSource.setDefaultEncoding("UTF-8");
|
||||
bundleMessageSource.setBasenames("i18n/validation");
|
||||
return bundleMessageSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验对象
|
||||
*
|
||||
* @param object 待校验对象
|
||||
* @param groups 待校验的组
|
||||
*/
|
||||
public static void validateEntity(Object object, Class<?>... groups)
|
||||
throws RenException {
|
||||
Locale.setDefault(LocaleContextHolder.getLocale());
|
||||
Validator validator = Validation.byDefaultProvider().configure().messageInterpolator(
|
||||
new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(getMessageSource())))
|
||||
.buildValidatorFactory().getValidator();
|
||||
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
||||
if (!constraintViolations.isEmpty()) {
|
||||
ConstraintViolation<Object> constraint = constraintViolations.iterator().next();
|
||||
throw new RenException(constraint.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package xiaozhi.common.validator.group;
|
||||
|
||||
/**
|
||||
* 新增 Group
|
||||
*/
|
||||
public interface AddGroup {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package xiaozhi.common.validator.group;
|
||||
|
||||
/**
|
||||
* 默认 Group
|
||||
*/
|
||||
public interface DefaultGroup {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package xiaozhi.common.validator.group;
|
||||
|
||||
/**
|
||||
* 修改 Group
|
||||
*/
|
||||
public interface UpdateGroup {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package xiaozhi.common.xss;
|
||||
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* SQL过滤
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class SqlFilter {
|
||||
|
||||
/**
|
||||
* SQL注入过滤
|
||||
*
|
||||
* @param str 待验证的字符串
|
||||
*/
|
||||
public static String sqlInject(String str) {
|
||||
if (StringUtils.isBlank(str)) {
|
||||
return null;
|
||||
}
|
||||
//去掉'|"|;|\字符
|
||||
str = StringUtils.replace(str, "'", "");
|
||||
str = StringUtils.replace(str, "\"", "");
|
||||
str = StringUtils.replace(str, ";", "");
|
||||
str = StringUtils.replace(str, "\\", "");
|
||||
|
||||
//转换成小写
|
||||
str = str.toLowerCase();
|
||||
|
||||
//非法字符
|
||||
String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"};
|
||||
|
||||
//判断是否包含非法字符
|
||||
for (String keyword : keywords) {
|
||||
if (str.contains(keyword)) {
|
||||
throw new RenException(ErrorCode.INVALID_SYMBOL);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package xiaozhi.common.xss;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
||||
/**
|
||||
* XSS 配置文件
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(XssProperties.class)
|
||||
@ConditionalOnProperty(prefix = "renren.xss", value = "enabled")
|
||||
public class XssConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
|
||||
FilterRegistrationBean<XssFilter> bean = new FilterRegistrationBean<>();
|
||||
bean.setDispatcherTypes(DispatcherType.REQUEST);
|
||||
bean.setFilter(new XssFilter(properties, pathMatcher));
|
||||
bean.addUrlPatterns("/*");
|
||||
bean.setOrder(Integer.MAX_VALUE);
|
||||
bean.setName("xssFilter");
|
||||
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package xiaozhi.common.xss;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* XSS过滤
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class XssFilter implements Filter {
|
||||
private final XssProperties properties;
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) throws ServletException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
|
||||
// 放行
|
||||
if (shouldNotFilter(httpServletRequest)) {
|
||||
chain.doFilter(request, response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
chain.doFilter(new XssHttpServletRequestWrapper(httpServletRequest), response);
|
||||
}
|
||||
|
||||
private boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 放行不过滤的URL
|
||||
return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, request.getServletPath()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package xiaozhi.common.xss;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* XSS过滤处理
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
public XssHttpServletRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
//非json类型,直接返回
|
||||
if (!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
//为空,直接返回
|
||||
String json = IoUtil.readUtf8(super.getInputStream());
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
//xss过滤
|
||||
json = xssEncode(json);
|
||||
final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return bis.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(xssEncode(name));
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
value = xssEncode(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] parameters = super.getParameterValues(name);
|
||||
if (parameters == null || parameters.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
parameters[i] = xssEncode(parameters[i]);
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> map = new LinkedHashMap<>();
|
||||
Map<String, String[]> parameters = super.getParameterMap();
|
||||
for (String key : parameters.keySet()) {
|
||||
String[] values = parameters.get(key);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = xssEncode(values[i]);
|
||||
}
|
||||
map.put(key, values);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(xssEncode(name));
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
value = xssEncode(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String xssEncode(String input) {
|
||||
return XssUtils.filter(input);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.common.xss;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* XSS 配置项
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "renren.xss")
|
||||
public class XssProperties {
|
||||
/**
|
||||
* 是否开启 XSS
|
||||
*/
|
||||
private boolean enabled;
|
||||
/**
|
||||
* 排除的URL列表
|
||||
*/
|
||||
private List<String> excludeUrls = Collections.emptyList();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package xiaozhi.common.xss;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.safety.Safelist;
|
||||
|
||||
/**
|
||||
* XSS过滤工具类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public class XssUtils extends Safelist {
|
||||
|
||||
/**
|
||||
* XSS过滤
|
||||
*/
|
||||
public static String filter(String html) {
|
||||
return Jsoup.clean(html, xssWhitelist());
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS过滤白名单
|
||||
*/
|
||||
private static Safelist xssWhitelist() {
|
||||
return new Safelist()
|
||||
//支持的标签
|
||||
.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl",
|
||||
"dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q", "small",
|
||||
"strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul",
|
||||
"embed", "object", "param", "span")
|
||||
|
||||
//支持的标签属性
|
||||
.addAttributes("a", "href", "class", "style", "target", "rel", "nofollow")
|
||||
.addAttributes("blockquote", "cite")
|
||||
.addAttributes("code", "class", "style")
|
||||
.addAttributes("col", "span", "width")
|
||||
.addAttributes("colgroup", "span", "width")
|
||||
.addAttributes("img", "align", "alt", "height", "src", "title", "width", "class", "style")
|
||||
.addAttributes("ol", "start", "type")
|
||||
.addAttributes("q", "cite")
|
||||
.addAttributes("table", "summary", "width", "class", "style")
|
||||
.addAttributes("tr", "abbr", "axis", "colspan", "rowspan", "width", "style")
|
||||
.addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width", "style")
|
||||
.addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width", "style")
|
||||
.addAttributes("ul", "type", "style")
|
||||
.addAttributes("pre", "class", "style")
|
||||
.addAttributes("div", "class", "id", "style")
|
||||
.addAttributes("embed", "src", "wmode", "flashvars", "pluginspage", "allowFullScreen", "allowfullscreen",
|
||||
"quality", "width", "height", "align", "allowScriptAccess", "allowscriptaccess", "allownetworking", "type")
|
||||
.addAttributes("object", "type", "id", "name", "data", "width", "height", "style", "classid", "codebase")
|
||||
.addAttributes("param", "name", "value")
|
||||
.addAttributes("span", "class", "style")
|
||||
|
||||
//标签属性对应的协议
|
||||
.addProtocols("a", "href", "ftp", "http", "https", "mailto")
|
||||
.addProtocols("img", "src", "http", "https")
|
||||
.addProtocols("blockquote", "cite", "http", "https")
|
||||
.addProtocols("cite", "cite", "http", "https")
|
||||
.addProtocols("q", "cite", "http", "https")
|
||||
.addProtocols("embed", "src", "http", "https");
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user