Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847f5c7c90 | ||
|
|
f23b243fc5 | ||
|
|
3768cef62d | ||
|
|
1e1ded4db8 | ||
|
|
13f76f4b35 | ||
|
|
c796e05232 | ||
|
|
f207517d35 | ||
|
|
6f5069567e | ||
|
|
13ce0e9396 | ||
|
|
e0144f7310 | ||
|
|
8ba87c1895 | ||
|
|
414e4b0b36 | ||
|
|
bfa41c3621 | ||
|
|
84ab496308 | ||
|
|
1f27283ab7 | ||
|
|
f91b89f38a | ||
|
|
9bb9861d88 | ||
|
|
403d1c45e5 | ||
|
|
400db0402a | ||
|
|
f0ae178183 | ||
|
|
4641e448d2 | ||
|
|
f0de65b7ce | ||
|
|
0472c5101f | ||
|
|
185cd6f82b | ||
|
|
aa6ad39b83 | ||
|
|
047b57f890 | ||
|
|
a18417ab26 | ||
|
|
20fcf557d5 | ||
|
|
5598ddf93c | ||
|
|
3017460cc7 | ||
|
|
4836a770c4 | ||
|
|
e6c89fad1b | ||
|
|
dba19b1e66 | ||
|
|
4e30bdb7cc | ||
|
|
4ac57cd140 | ||
|
|
c4d52ce47a | ||
|
|
54d0688571 | ||
|
|
66d5fd6ca4 | ||
|
|
25195b6360 | ||
|
|
e02ecf053f | ||
|
|
c86f2ad412 | ||
|
|
82fd97e06a | ||
|
|
614a144f60 | ||
|
|
7d344c71e1 | ||
|
|
6ad6c69660 | ||
|
|
e96379b6c0 | ||
|
|
f7480f3bac | ||
|
|
54d3a5b368 | ||
|
|
7eb4d064ea | ||
|
|
cc66fcddf5 | ||
|
|
aac4c2b42b | ||
|
|
7a17042276 | ||
|
|
42fbfd3c47 | ||
|
|
e273ade0b0 | ||
|
|
bcaa4563ac | ||
|
|
e0c01d4561 | ||
|
|
d6280ea280 | ||
|
|
666b191b6c | ||
|
|
778cb7f4de | ||
|
|
142bbd265d | ||
|
|
f676ec9e7b | ||
|
|
44d379a016 | ||
|
|
2170509d92 | ||
|
|
798ab7d18b | ||
|
|
abd2b4bac0 | ||
|
|
585cbbed23 | ||
|
|
1b40d345eb | ||
|
|
3c0292b56e | ||
|
|
bc21ba7c1e | ||
|
|
c7c3fd7f7e | ||
|
|
547e31eae6 | ||
|
|
6072bcb111 |
6
.gitignore
vendored
@@ -18,9 +18,15 @@
|
||||
**/vendor/
|
||||
.idea
|
||||
.vscode
|
||||
.qoder
|
||||
out
|
||||
|
||||
server/docs/docker-compose
|
||||
server/config.yml
|
||||
server/ip2region.xdb
|
||||
mayfly-go.log
|
||||
|
||||
mayfly-go-linux-amd64/
|
||||
|
||||
.DS_Store
|
||||
__debug_*
|
||||
47
AGENTS.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Mayfly-Go
|
||||
|
||||
你是一位全栈开发工程师,参与 Mayfly-Go 项目的开发。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Go 1.26+, GORM, Gin, 自定义 IOC 依赖注入框架
|
||||
- **前端**: Vue 3 (Composition API) + TypeScript 6.x + Vite 8.x + Element Plus + Tailwind CSS 4.x + Pinia
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
cd server && go run main.go
|
||||
cd server && go test ./...
|
||||
|
||||
# 前端
|
||||
cd frontend && pnpm dev
|
||||
cd frontend && pnpm build
|
||||
cd frontend && pnpm lint
|
||||
```
|
||||
|
||||
## 全局边界
|
||||
|
||||
- ✅ **Always**: 后端遵循 Clean Architecture 分层(api → application → domain → infra)
|
||||
- ✅ **Always**: 所有错误必须处理,禁止 `result, _ := doSomething()`
|
||||
- ✅ **Always**: 前端所有展示文本使用 i18n(`$t()` / `t()`),禁止硬编码
|
||||
- ⚠️ **Ask first**: 修改 pkg/ 或 common/ 下的公共接口
|
||||
- 🚫 **Never**: 在 application/domain/infra 层使用 `biz.ErrIsNil`,必须返回 error
|
||||
- 🚫 **Never**: 前端直接调用 axios,必须通过 API 封装
|
||||
|
||||
## 详细规范
|
||||
|
||||
- @./docs/server/architecture.md — 分层架构与目录规范
|
||||
- @./docs/server/api.md — API 层规范
|
||||
- @./docs/server/application.md — Application 层规范
|
||||
- @./docs/server/domain.md — Domain 层规范
|
||||
- @./docs/server/infrastructure.md — Infrastructure 层规范
|
||||
- @./docs/server/concurrent.md — 并发与 Panic 处理
|
||||
- @./docs/server/security.md — 安全与权限
|
||||
- @./docs/server/quality.md — 代码质量与 Git 提交
|
||||
- @./docs/server/i18n.md — 后端国际化规范
|
||||
- @./docs/frontend/overview.md — 前端综合示例与技术栈
|
||||
- @./docs/frontend/component.md — 组件开发规范
|
||||
- @./docs/frontend/api.md — API 定义与调用
|
||||
- @./docs/frontend/i18n.md — 国际化规范
|
||||
- @./docs/frontend/style.md — 样式与 UI 规范
|
||||
@@ -1,16 +1,16 @@
|
||||
# 构建前端资源
|
||||
FROM m.daocloud.io/docker.io/node:18-bookworm-slim AS fe-builder
|
||||
FROM m.daocloud.io/docker.io/node:22-bookworm-slim AS fe-builder
|
||||
|
||||
WORKDIR /mayfly
|
||||
|
||||
COPY frontend .
|
||||
|
||||
RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||
yarn install && \
|
||||
yarn build
|
||||
RUN npm config set registry 'https://registry.npmmirror.com' && \
|
||||
npm install && \
|
||||
npm run build
|
||||
|
||||
# 构建后端资源
|
||||
FROM m.daocloud.io/docker.io/golang:1.23 AS be-builder
|
||||
FROM m.daocloud.io/docker.io/golang:1.26 AS be-builder
|
||||
|
||||
ENV GOPROXY https://goproxy.cn
|
||||
WORKDIR /mayfly
|
||||
|
||||
52
README.md
@@ -15,11 +15,14 @@
|
||||
<img src="https://img.shields.io/github/stars/dromara/mayfly-go.svg?style=social" alt="github star"/>
|
||||
<img src="https://img.shields.io/github/forks/dromara/mayfly-go.svg?style=social" alt="github fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/dromara/mayfly-go" target="_blank">
|
||||
<img src="https://gitcode.com/dromara/mayfly-go/star/badge.svg" alt="github star"/>
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/mayflygo/mayfly-go/tags" target="_blank">
|
||||
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
|
||||
</a>
|
||||
<a href="https://github.com/golang/go" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
|
||||
<img src="https://img.shields.io/badge/Golang-1.24%2B-yellow.svg" alt="golang"/>
|
||||
</a>
|
||||
<a href="https://cn.vuejs.org" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
||||
@@ -28,7 +31,7 @@
|
||||
|
||||
## 前言
|
||||
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)。数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作、数据同步、数据迁移。redis(单机 哨兵 集群)。mongo 等集工单流程审批于一体的统一管理操作平台。**
|
||||
Web 版 **统一管理操作平台**,集成了对 Linux 系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite、ClickHouse 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis(单机、哨兵、集群模式)、MongoDB、Elasticsearch、Kafka、Milvus 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
|
||||
|
||||
## 开发语言与主要框架
|
||||
|
||||
@@ -48,42 +51,32 @@ http://go.mayfly.run
|
||||
|
||||
#### 首页
|
||||
|
||||

|
||||

|
||||
|
||||
#### 机器操作
|
||||
#### 资源管理
|
||||
|
||||
##### 状态查看
|
||||

|
||||
|
||||

|
||||
#### 资源操作
|
||||
|
||||
##### ssh 终端
|
||||

|
||||
|
||||

|
||||
|
||||
##### 文件操作
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
#### 数据库操作
|
||||

|
||||
|
||||
##### sql 编辑器
|
||||

|
||||
|
||||

|
||||
|
||||
##### 在线增删改查数据
|
||||
|
||||

|
||||
|
||||
#### Redis 操作
|
||||
|
||||

|
||||
|
||||
#### Mongo 操作
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 工单流程审批
|
||||
|
||||

|
||||
@@ -106,4 +99,11 @@ http://go.mayfly.run
|
||||
|
||||
## 💌 支持作者
|
||||
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 或 <a target="_blank" href="https://gitcode.com/dromara/mayfly-go">Gitcode</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
|
||||
> 喝杯咖啡 ☕️ 或者来杯奶茶 🧋,让作者更有精神,写出更棒的代码!
|
||||
|
||||
<img class="no-margin" src="https://foruda.gitee.com/images/1744113367791412282/36a3c23b_1240250.png" alt="微信打赏" width="200" height="200">
|
||||
|
||||
> **特别感谢:**
|
||||
> 赞助金额达 199 元以上,加微信(wx-error),可受邀进入付费交流群,享受更快、更优先的技术支持与交流服务!
|
||||
|
||||
36
README_EN.md
@@ -28,7 +28,7 @@
|
||||
|
||||
## Preface
|
||||
|
||||
Browser-based management platform. **linux(Terminal [terminal playback, command filtering], file, script, process, cronjob), database (mysql, postgres, oracle, sqlserver, Dameng, gauss, sqlite) data operation, data synchronization, data migration, redis(standlone, sentinel, cluster), mongo and other unified management and operation platforms that integrate work order process approval.**
|
||||
Web-based **Unified Management and Operation Platform**, integrating comprehensive operation support for Linux systems (including terminal management [terminal playback, command filtering], file management, script execution, process monitoring, and cronjob settings). It also provides data operation, data synchronization, and data migration for multiple databases (such as MySQL, PostgreSQL, Oracle, SQL Server, Dameng, Gauss, SQLite, ClickHouse, etc.). Additionally, it supports Redis operations (standalone, sentinel, and cluster modes) and MongoDB, Elasticsearch, Kafka, Milvus management, combined with work order process approval functionality to offer enterprises an all-in-one solution for operations and management.
|
||||
|
||||
## Development languages and major frameworks
|
||||
|
||||
@@ -46,40 +46,30 @@ account/password:test/test123.
|
||||
|
||||

|
||||
|
||||
#### Machine Operation
|
||||
#### Resource Manage
|
||||
|
||||
##### Status
|
||||

|
||||
|
||||

|
||||
#### Resource Operation
|
||||
|
||||
##### SSH Terminal
|
||||

|
||||
|
||||

|
||||
|
||||
##### File Operation
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
#### Database Operation
|
||||

|
||||
|
||||
##### SQL Editor
|
||||

|
||||
|
||||

|
||||
|
||||
##### Add, delete, update and check data online
|
||||
|
||||

|
||||
|
||||
#### Redis Operation
|
||||
|
||||

|
||||
|
||||
#### Mongo Operation
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### Work order process approval
|
||||
|
||||

|
||||
|
||||
450
build_release.sh
@@ -1,200 +1,312 @@
|
||||
#bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
#==============================================
|
||||
# Mayfly-Go Release Build Tool
|
||||
# 前后端打包编译至指定目录,快速制作发行版
|
||||
#==============================================
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
#----------------------------------------------
|
||||
# 前后端打包编译至指定目录,即快速制作发行版
|
||||
# 全局配置
|
||||
#----------------------------------------------
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SERVER_DIR="${PROJECT_ROOT}/server"
|
||||
FRONTEND_DIR="${PROJECT_ROOT}/frontend"
|
||||
BINARY_NAME="mayfly-go"
|
||||
|
||||
project_path=`pwd`
|
||||
# 构建后的二进制执行文件名
|
||||
exec_file_name="mayfly-go"
|
||||
# web项目目录
|
||||
web_folder="${project_path}/frontend"
|
||||
# server目录
|
||||
server_folder="${project_path}/server"
|
||||
# 构建目标配置:名称|操作系统|架构
|
||||
BUILD_TARGETS=(
|
||||
"linux-amd64|linux|amd64"
|
||||
"linux-arm64|linux|arm64"
|
||||
"windows|windows|amd64"
|
||||
"mac|darwin|amd64"
|
||||
)
|
||||
|
||||
function echo_red() {
|
||||
echo -e "\033[1;31m$1\033[0m"
|
||||
}
|
||||
|
||||
function echo_green() {
|
||||
echo -e "\033[1;32m$1\033[0m"
|
||||
}
|
||||
|
||||
function echo_yellow() {
|
||||
#----------------------------------------------
|
||||
# 工具函数
|
||||
#----------------------------------------------
|
||||
print_header() {
|
||||
echo -e "\033[1;33m$1\033[0m"
|
||||
}
|
||||
|
||||
function buildWeb() {
|
||||
cd ${web_folder}
|
||||
copy2Server=$1
|
||||
|
||||
echo_yellow "-------------------Start bundling frontends-------------------"
|
||||
yarn run build
|
||||
if [ "${copy2Server}" == "2" ] ; then
|
||||
echo_green 'Copy the packaged static files to server/static/static'
|
||||
rm -rf ${server_folder}/static/static && mkdir -p ${server_folder}/static/static && cp -r ${web_folder}/dist/* ${server_folder}/static/static
|
||||
fi
|
||||
echo_yellow ">>>>>>>>>>>>>>>>>>>End of packaging frontend<<<<<<<<<<<<<<<<<<<<\n"
|
||||
print_success() {
|
||||
echo -e "\033[1;32m$1\033[0m"
|
||||
}
|
||||
|
||||
function build() {
|
||||
cd ${project_path}
|
||||
print_error() {
|
||||
echo -e "\033[1;31m$1\033[0m" >&2
|
||||
}
|
||||
|
||||
# 打包产物的输出目录
|
||||
toFolder=$1
|
||||
os=$2
|
||||
arch=$3
|
||||
copyDocScript=$4
|
||||
print_info() {
|
||||
echo -e "\033[1;34m$1\033[0m"
|
||||
}
|
||||
|
||||
echo_yellow "-------------------Start a bundle build - ${os}-${arch}-------------------"
|
||||
to_lower() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
cd ${server_folder}
|
||||
echo_green "Package build executables..."
|
||||
#----------------------------------------------
|
||||
# 构建函数
|
||||
#----------------------------------------------
|
||||
build_frontend() {
|
||||
print_header "\n>>> Building frontend..."
|
||||
|
||||
cd "${FRONTEND_DIR}"
|
||||
npm run build
|
||||
|
||||
# 拷贝到 server 静态目录
|
||||
print_success ">>> Copying frontend assets to server/static/static"
|
||||
rm -rf "${SERVER_DIR}/static/static"
|
||||
mkdir -p "${SERVER_DIR}/static/static"
|
||||
cp -r "${FRONTEND_DIR}/dist/"* "${SERVER_DIR}/static/static/"
|
||||
|
||||
cd "${PROJECT_ROOT}"
|
||||
}
|
||||
|
||||
execFileName=${exec_file_name}
|
||||
# 如果是windows系统,可执行文件需要添加.exe结尾
|
||||
if [ "${os}" == "windows" ];then
|
||||
execFileName="${execFileName}.exe"
|
||||
build_backend() {
|
||||
local output_dir="$1"
|
||||
local os_name="$2"
|
||||
local arch="$3"
|
||||
local copy_resources="$4"
|
||||
|
||||
local binary_file="${BINARY_NAME}"
|
||||
local target_name="${os_name}-${arch}"
|
||||
|
||||
print_header "\n>>> Building backend: ${target_name}"
|
||||
|
||||
# Windows 需要 .exe 后缀
|
||||
if [ "${os_name}" = "windows" ]; then
|
||||
binary_file="${BINARY_NAME}.exe"
|
||||
fi
|
||||
|
||||
# 编译
|
||||
cd "${SERVER_DIR}"
|
||||
go mod tidy
|
||||
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
|
||||
|
||||
if [ -d ${toFolder} ] ; then
|
||||
echo_green "The desired folder already exists. Clear the folder"
|
||||
sudo rm -rf ${toFolder}
|
||||
CGO_ENABLED=0 GOOS="${os_name}" GOARCH="${arch}" \
|
||||
go build -trimpath -ldflags="-w" -o "${binary_file}" main.go
|
||||
|
||||
# 准备输出目录
|
||||
local bin_dir="${output_dir}/bin"
|
||||
if [ -d "${output_dir}" ]; then
|
||||
print_info " Output directory exists, cleaning..."
|
||||
rm -rf "${output_dir}"
|
||||
fi
|
||||
echo_green "Create '${toFolder}' Directory"
|
||||
mkdir ${toFolder}
|
||||
|
||||
echo_green "Move binary to '${toFolder}'"
|
||||
mv ${server_folder}/${execFileName} ${toFolder}
|
||||
|
||||
# if [ "${copy2Server}" == "1" ] ; then
|
||||
# echo_green "拷贝前端静态页面至'${toFolder}/static'"
|
||||
# mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
|
||||
# fi
|
||||
|
||||
if [ "${copyDocScript}" == "1" ] ; then
|
||||
echo_green "Copy resources such as scripts [config.yml.example、readme.txt、startup.sh、shutdown.sh]"
|
||||
cp ${server_folder}/config.yml.example ${toFolder}
|
||||
cp ${server_folder}/readme.txt ${toFolder}
|
||||
cp ${server_folder}/readme_en.txt ${toFolder}
|
||||
cp ${server_folder}/resources/script/startup.sh ${toFolder}
|
||||
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
|
||||
mkdir -p "${bin_dir}"
|
||||
|
||||
# 移动二进制文件到 bin 目录
|
||||
mv "${SERVER_DIR}/${binary_file}" "${bin_dir}/"
|
||||
|
||||
# 拷贝资源文件
|
||||
if [ "${copy_resources}" = "1" ]; then
|
||||
print_info " Copying config and scripts..."
|
||||
cp "${SERVER_DIR}/config.yml.example" "${output_dir}/config.yml"
|
||||
cp "${SERVER_DIR}/readme.txt" "${output_dir}/"
|
||||
cp "${SERVER_DIR}/readme_en.txt" "${output_dir}/"
|
||||
cp "${SERVER_DIR}/resources/script/mayfly-go.sh" "${output_dir}/"
|
||||
chmod +x "${output_dir}/mayfly-go.sh"
|
||||
fi
|
||||
|
||||
echo_yellow ">>>>>>>>>>>>>>>>>>> ${os}-${arch} - Bundle build complete <<<<<<<<<<<<<<<<<<<<\n"
|
||||
|
||||
print_success ">>> Build complete: ${target_name}"
|
||||
cd "${PROJECT_ROOT}"
|
||||
}
|
||||
|
||||
function buildLinuxAmd64() {
|
||||
build "$1/mayfly-go-linux-amd64" "linux" "amd64" $2
|
||||
}
|
||||
|
||||
function buildLinuxArm64() {
|
||||
build "$1/mayfly-go-linux-arm64" "linux" "arm64" $2
|
||||
}
|
||||
|
||||
function buildWindows() {
|
||||
build "$1/mayfly-go-windows" "windows" "amd64" $2
|
||||
}
|
||||
|
||||
function buildMac() {
|
||||
build "$1/mayfly-go-mac" "darwin" "amd64" $2
|
||||
}
|
||||
|
||||
function buildDocker() {
|
||||
echo_yellow "-------------------Start building the docker image-------------------"
|
||||
imageVersion=$1
|
||||
imageName="mayfly/mayfly-go:${imageVersion}"
|
||||
docker build --no-cache --platform linux/amd64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
|
||||
echo_green "The docker image is built -> [${imageName}]"
|
||||
echo_yellow "-------------------Finished building the docker image-------------------"
|
||||
}
|
||||
|
||||
function buildxDocker() {
|
||||
echo_yellow "-------------------The docker buildx build image starts-------------------"
|
||||
imageVersion=$1
|
||||
imageName="ccr.ccs.tencentyun.com/mayfly/mayfly-go:${imageVersion}"
|
||||
docker buildx build --no-cache --push --platform linux/amd64,linux/arm64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
|
||||
echo_green "The docker multi-architecture version image is built -> [${imageName}]"
|
||||
echo_yellow "-------------------The docker buildx image is finished-------------------"
|
||||
}
|
||||
|
||||
function runBuild() {
|
||||
read -p "Select build version [0 | Other->Other than docker image 1->linux-amd64 2->linux-arm64 3->windows 4->mac 5->docker 6->docker buildx]: " buildType
|
||||
|
||||
toPath="."
|
||||
imageVersion="latest"
|
||||
copyDocScript="1"
|
||||
|
||||
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
|
||||
# 构建结果的目的路径
|
||||
read -p "Please enter the build product output directory [default current path]: " toPath
|
||||
if [ ! -d ${toPath} ] ; then
|
||||
echo_red "Build product output directory does not exist!"
|
||||
exit;
|
||||
fi
|
||||
if [ "${toPath}" == "" ] ; then
|
||||
toPath="."
|
||||
fi
|
||||
|
||||
read -p "Whether to copy documents & Scripts [0-> No 1-> Yes][Default yes]: " copyDocScript
|
||||
if [ "${copyDocScript}" == "" ] ; then
|
||||
copyDocScript="1"
|
||||
fi
|
||||
|
||||
# 进入目标路径,并赋值全路径
|
||||
cd ${toPath}
|
||||
toPath=`pwd`
|
||||
|
||||
# read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
|
||||
runBuildWeb="2"
|
||||
# 编译web前端
|
||||
buildWeb ${runBuildWeb}
|
||||
build_docker() {
|
||||
local version="$1"
|
||||
local use_buildx="$2"
|
||||
local image_name
|
||||
local build_cmd
|
||||
|
||||
if [ "${use_buildx}" = "1" ]; then
|
||||
image_name="ccr.ccs.tencentyun.com/mayfly/mayfly-go:${version}"
|
||||
build_cmd="docker buildx build --no-cache --push --platform linux/amd64,linux/arm64"
|
||||
print_header "\n>>> Building Docker image (multi-arch): ${image_name}"
|
||||
else
|
||||
image_name="mayfly/mayfly-go:${version}"
|
||||
build_cmd="docker build --no-cache --platform linux/amd64"
|
||||
print_header "\n>>> Building Docker image: ${image_name}"
|
||||
fi
|
||||
|
||||
${build_cmd} --build-arg MAYFLY_GO_VERSION="${version}" -t "${image_name}" "${PROJECT_ROOT}"
|
||||
print_success ">>> Docker image built: ${image_name}"
|
||||
}
|
||||
|
||||
if [[ "${buildType}" == "5" ]] || [[ "${buildType}" == "6" ]] ; then
|
||||
read -p "Please enter the docker image version (default latest) : " imageVersion
|
||||
cleanup_frontend() {
|
||||
print_info "\n>>> Cleaning up temporary frontend assets..."
|
||||
rm -rf "${SERVER_DIR}/static/static/"{assets,config.js,index.html}
|
||||
print_success ">>> Cleanup complete"
|
||||
}
|
||||
|
||||
if [ "${imageVersion}" == "" ] ; then
|
||||
imageVersion="latest"
|
||||
fi
|
||||
compress_package() {
|
||||
local source_dir="$1"
|
||||
local output_dir="$2"
|
||||
local package_name
|
||||
|
||||
package_name="$(basename "${source_dir}")"
|
||||
|
||||
print_header "\n>>> Compressing package: ${package_name}"
|
||||
|
||||
cd "${output_dir}"
|
||||
|
||||
# 统一使用 zip 格式,跨平台兼容性最好
|
||||
zip -r "${package_name}.zip" "${package_name}"/
|
||||
rm -rf "${package_name}"
|
||||
print_success ">>> Compressed: ${package_name}.zip"
|
||||
|
||||
cd "${PROJECT_ROOT}"
|
||||
}
|
||||
|
||||
#----------------------------------------------
|
||||
# 主流程
|
||||
#----------------------------------------------
|
||||
main() {
|
||||
# 显示菜单
|
||||
print_header "========================================"
|
||||
print_header " Mayfly-Go Release Build Tool"
|
||||
print_header "========================================"
|
||||
echo ""
|
||||
echo "Build Options:"
|
||||
echo " [0] All Platforms (linux-amd64, linux-arm64, windows, mac)"
|
||||
echo " [1] Linux AMD64"
|
||||
echo " [2] Linux ARM64"
|
||||
echo " [3] Windows"
|
||||
echo " [4] macOS"
|
||||
echo " [5] Docker Image"
|
||||
echo " [6] Docker Multi-arch (buildx)"
|
||||
echo ""
|
||||
|
||||
read -p "Select build option [0-6] (default: 0): " build_type
|
||||
build_type=${build_type:-0}
|
||||
|
||||
# 验证输入
|
||||
if ! [[ "${build_type}" =~ ^[0-6]$ ]]; then
|
||||
print_error "Error: Invalid option. Please enter a number between 0 and 6."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case ${buildType} in
|
||||
"1")
|
||||
buildLinuxAmd64 ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"2")
|
||||
buildLinuxArm64 ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"3")
|
||||
buildWindows ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"4")
|
||||
buildMac ${toPath} ${copyDocScript}
|
||||
;;
|
||||
|
||||
# 初始化配置
|
||||
local output_dir="."
|
||||
local docker_version="latest"
|
||||
local copy_resources="1"
|
||||
local compress_output="0"
|
||||
local is_docker=0
|
||||
|
||||
# Docker 构建
|
||||
if [[ "${build_type}" == "5" || "${build_type}" == "6" ]]; then
|
||||
is_docker=1
|
||||
echo ""
|
||||
read -p "Enter Docker image version (default: latest): " docker_version
|
||||
docker_version=${docker_version:-latest}
|
||||
else
|
||||
# 二进制构建
|
||||
echo ""
|
||||
read -p "Enter output directory (default: current): " output_dir
|
||||
output_dir=${output_dir:-.}
|
||||
|
||||
# 验证并获取绝对路径
|
||||
if [ "${output_dir}" != "." ] && [ ! -d "${output_dir}" ]; then
|
||||
print_error "Error: Directory '${output_dir}' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
output_dir="$(cd "${output_dir}" && pwd)"
|
||||
|
||||
echo ""
|
||||
read -p "Copy config & scripts? [Y/n] (default: Y): " copy_input
|
||||
if [ "$(to_lower "${copy_input}")" = "n" ]; then
|
||||
copy_resources="0"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
read -p "Compress package? [y/N] (default: N): " compress_input
|
||||
if [ "$(to_lower "${compress_input}")" = "y" ]; then
|
||||
compress_output="1"
|
||||
fi
|
||||
|
||||
# 构建前端
|
||||
echo ""
|
||||
build_frontend
|
||||
fi
|
||||
|
||||
# 显示配置摘要
|
||||
echo ""
|
||||
print_header "Build Configuration:"
|
||||
|
||||
# 获取构建类型名称
|
||||
local type_names=("All Platforms" "Linux AMD64" "Linux ARM64" "Windows" "macOS" "Docker Image" "Docker Multi-arch")
|
||||
echo " Type: ${type_names[${build_type}]}"
|
||||
|
||||
if [ "${is_docker}" = "1" ]; then
|
||||
echo " Version: ${docker_version}"
|
||||
else
|
||||
echo " Output: ${output_dir}"
|
||||
echo " Resources: $([ "${copy_resources}" = "1" ] && echo "Yes" || echo "No")"
|
||||
echo " Compress: $([ "${compress_output}" = "1" ] && echo "Yes" || echo "No")"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 确认构建
|
||||
read -p "Continue? [Y/n] (default: Y): " confirm
|
||||
if [ "$(to_lower "${confirm}")" = "n" ]; then
|
||||
print_info "Build cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 执行构建
|
||||
echo ""
|
||||
print_header "Starting build..."
|
||||
|
||||
case "${build_type}" in
|
||||
"1"|"2"|"3"|"4")
|
||||
# 单个平台构建
|
||||
local target="${BUILD_TARGETS[$((build_type-1))]}"
|
||||
IFS='|' read -r name os arch <<< "${target}"
|
||||
build_backend "${output_dir}/mayfly-go-${name}" "${os}" "${arch}" "${copy_resources}"
|
||||
;;
|
||||
"5")
|
||||
buildDocker ${imageVersion}
|
||||
;;
|
||||
build_docker "${docker_version}" "0"
|
||||
;;
|
||||
"6")
|
||||
buildxDocker ${imageVersion}
|
||||
;;
|
||||
build_docker "${docker_version}" "1"
|
||||
;;
|
||||
*)
|
||||
buildLinuxAmd64 ${toPath} ${copyDocScript}
|
||||
buildLinuxArm64 ${toPath} ${copyDocScript}
|
||||
buildWindows ${toPath} ${copyDocScript}
|
||||
buildMac ${toPath} ${copyDocScript}
|
||||
;;
|
||||
# 构建所有平台
|
||||
print_info "Building all platforms..."
|
||||
for target in "${BUILD_TARGETS[@]}"; do
|
||||
IFS='|' read -r name os arch <<< "${target}"
|
||||
build_backend "${output_dir}/mayfly-go-${name}" "${os}" "${arch}" "${copy_resources}"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
|
||||
echo_green "Delete static assets under ['${server_folder}/static/static']."
|
||||
# 删除静态资源文件,保留一个favicon.ico,否则后端启动会报错
|
||||
rm -rf ${server_folder}/static/static/assets
|
||||
rm -rf ${server_folder}/static/static/config.js
|
||||
rm -rf ${server_folder}/static/static/index.html
|
||||
|
||||
# 清理临时文件
|
||||
if [ "${is_docker}" = "0" ]; then
|
||||
cleanup_frontend
|
||||
fi
|
||||
|
||||
# 压缩输出
|
||||
if [ "${compress_output}" = "1" ] && [ "${is_docker}" = "0" ]; then
|
||||
case "${build_type}" in
|
||||
"1"|"2"|"3"|"4")
|
||||
local target="${BUILD_TARGETS[$((build_type-1))]}"
|
||||
IFS='|' read -r name os arch <<< "${target}"
|
||||
compress_package "${output_dir}/mayfly-go-${name}" "${output_dir}"
|
||||
;;
|
||||
*)
|
||||
print_info "Compressing all packages..."
|
||||
for target in "${BUILD_TARGETS[@]}"; do
|
||||
IFS='|' read -r name os arch <<< "${target}"
|
||||
compress_package "${output_dir}/mayfly-go-${name}" "${output_dir}"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 完成
|
||||
echo ""
|
||||
print_success "========================================"
|
||||
print_success " Build Completed Successfully!"
|
||||
print_success "========================================"
|
||||
}
|
||||
|
||||
runBuild
|
||||
# 执行主函数
|
||||
main
|
||||
|
||||
@@ -14,18 +14,18 @@ services:
|
||||
restart: always
|
||||
|
||||
server:
|
||||
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
|
||||
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: mayfly-go-server
|
||||
ports:
|
||||
- "8888:8888"
|
||||
- "18888:18888"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: mysql:3306
|
||||
volumes:
|
||||
- ./server/config.yml.example:/mayfly/config.yml
|
||||
- ./server/config.yml:/mayfly/config.yml
|
||||
depends_on:
|
||||
- mysql
|
||||
restart: always
|
||||
|
||||
37
docs/frontend/api.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# API 定义与调用规范
|
||||
|
||||
## API 定义
|
||||
|
||||
```typescript
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const accountApi = {
|
||||
list: Api.newGet('/sys/accounts'),
|
||||
save: Api.newPost('/sys/accounts'),
|
||||
update: Api.newPut('/sys/accounts/{id}'),
|
||||
del: Api.newDelete('/sys/accounts/{id}'),
|
||||
changeStatus: Api.newPut('/sys/accounts/change-status/{id}/{status}'),
|
||||
};
|
||||
```
|
||||
|
||||
## 调用模式
|
||||
|
||||
```typescript
|
||||
// 简单请求
|
||||
await accountApi.del.request({ id: row.id });
|
||||
|
||||
// 响应式(用于 loading 状态)
|
||||
const { execute, isFetching } = accountApi.list.useApi();
|
||||
|
||||
// 表格集成
|
||||
<page-table :page-api="accountApi.list" />
|
||||
```
|
||||
|
||||
## 边界
|
||||
|
||||
- ✅ **Always**: API 定义集中放在 `api.ts`
|
||||
- 🚫 **Never**: 直接调用 axios,必须通过 API 封装
|
||||
164
docs/frontend/component.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 组件开发规范
|
||||
|
||||
## 代码组织顺序
|
||||
|
||||
```
|
||||
Imports
|
||||
Props/Emits
|
||||
常量定义 (as const)
|
||||
类型定义
|
||||
响应式数据
|
||||
计算属性
|
||||
监听器
|
||||
工具函数
|
||||
事件处理方法 (on 开头)
|
||||
```
|
||||
|
||||
## 命名规范
|
||||
|
||||
- **事件方法**: 必须以 `on` 开头(`onSubmit`, `onDelete`, `onEdit`)
|
||||
- **变量/函数**: camelCase
|
||||
- **常量**: UPPER_SNAKE_CASE + `as const`
|
||||
- **组件**: PascalCase
|
||||
- **文件**: 组件用 PascalCase,其他用小写
|
||||
|
||||
## Props & Emits
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
data: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
</script>
|
||||
```
|
||||
|
||||
## 双向绑定规范
|
||||
|
||||
### 使用 defineModel (Vue 3.4+)
|
||||
|
||||
**必须使用 `defineModel` 实现双向绑定**,替代旧的 `computed` + `emit('update:xxx')` 模式。
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
// 单个 v-model
|
||||
const modelValue = defineModel<string>('modelValue', {
|
||||
default: '',
|
||||
});
|
||||
|
||||
// 命名 v-model
|
||||
const authCertName = defineModel<string>('authCertName');
|
||||
const machineId = defineModel<number>('machineId');
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 内部字段联动更新
|
||||
|
||||
当组件内部有多个字段,需要联动更新外部的 `modelValue` 时,使用 `watch` 监听:
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
|
||||
const authCertName = defineModel<string>('authCertName');
|
||||
const machineName = defineModel<string>('machineName');
|
||||
const selectNode = defineModel<string>('modelValue', { default: '' });
|
||||
|
||||
// 监听内部字段变化,自动更新 selectNode
|
||||
watch(
|
||||
[authCertName, machineName],
|
||||
() => {
|
||||
selectNode.value = authCertName.value
|
||||
? `${machineName.value} > ${authCertName.value}`
|
||||
: '';
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 规范要点
|
||||
|
||||
- ✅ **Always**: 使用 `defineModel` 替代 `computed` + `emit('update:xxx')`
|
||||
- ✅ **Always**: 为 `defineModel` 提供合适的 `default` 值
|
||||
- 🚫 **Never**: 使用旧的 `computed` getter/setter 模式实现双向绑定
|
||||
|
||||
## 图标使用规范
|
||||
|
||||
### 统一使用 SvgIcon 组件
|
||||
|
||||
**所有图标必须使用 `SvgIcon` 组件**,禁止使用 `<el-icon>` 配合导入图标组件。
|
||||
|
||||
```vue
|
||||
<!-- ✅ 正确:使用 SvgIcon -->
|
||||
<SvgIcon name="Monitor" :size="20" />
|
||||
<SvgIcon name="check" class="text-success" />
|
||||
|
||||
<!-- ❌ 错误:使用 el-icon + 导入 -->
|
||||
<el-icon><Check /></el-icon>
|
||||
```
|
||||
|
||||
**规范要点**:
|
||||
- ✅ 使用 `name` 属性指定图标,`size` 属性控制大小
|
||||
- ✅ 图标名称使用 PascalCase 或 kebab-case
|
||||
- 🚫 禁止使用 `<el-icon>` 和导入 `@element-plus/icons-vue`
|
||||
- 🚫 禁止通过 class 设置图标大小
|
||||
|
||||
### 自定义 SVG 图标
|
||||
|
||||
项目支持在 `assets/icon` 目录下添加自定义 SVG 图标。
|
||||
|
||||
#### 目录结构
|
||||
|
||||
```
|
||||
frontend/src/assets/icon/
|
||||
├── db/ # 数据库图标(mysql.svg, postgres.svg...)
|
||||
├── machine/ # 机器图标
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### 使用方法
|
||||
|
||||
**格式**: `name="icon {目录}/{文件名}"`(不含 .svg)
|
||||
|
||||
```vue
|
||||
<SvgIcon name="icon db/mysql" :size="20" />
|
||||
```
|
||||
|
||||
#### 添加步骤
|
||||
|
||||
1. 将 SVG 文件放到 `frontend/src/assets/icon/` 对应子目录
|
||||
2. 文件名使用小写 + 连字符(如 `mysql.svg`)
|
||||
3. 使用 `name="icon db/mysql"` 引用
|
||||
|
||||
#### 注意事项
|
||||
|
||||
- ✅ SVG 必须有 `viewBox` 属性
|
||||
- ✅ 使用 `size` 属性控制大小
|
||||
- ✅ 图标颜色继承当前元素的 `color`
|
||||
- 🚫 不要在 SVG 中硬编码颜色值
|
||||
- 🚫 文件名不要使用大写或下划线
|
||||
|
||||
## 边界
|
||||
|
||||
- ✅ **Always**: 使用 Composition API + `<script setup>`
|
||||
- ✅ **Always**: 事件方法以 `on` 开头
|
||||
- ✅ **Always**: 移除无用的导入(import)和无用的字段、变量、函数
|
||||
- 🚫 **Never**: 保留未使用的代码或注释掉的代码
|
||||
- 🚫 **Never**: 使用固定高度计算,优先用 Flexbox
|
||||
45
docs/frontend/i18n.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 国际化规范
|
||||
|
||||
## 文件组织
|
||||
|
||||
```
|
||||
src/i18n/
|
||||
├── zh-cn/
|
||||
│ ├── common.ts
|
||||
│ ├── system.ts
|
||||
│ ├── ai.ts
|
||||
│ └── ...
|
||||
└── en/
|
||||
├── common.ts
|
||||
├── system.ts
|
||||
└── ...
|
||||
```
|
||||
|
||||
- 按模块拆分,每个业务模块一个文件
|
||||
- 命名空间以模块名为根 key(如 `system.account.name`)
|
||||
|
||||
## 使用方式
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<h1>{{ $t('system.account.name') }}</h1>
|
||||
<el-button>{{ $t('common.save') }}</el-button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n();
|
||||
const message = t('common.success');
|
||||
</script>
|
||||
```
|
||||
|
||||
## 边界
|
||||
|
||||
- ✅ **Always**: 所有展示文本通过 `$t()` 或 `t()` 获取
|
||||
- ✅ **Always**: 枚举的 label 必须是国际化 key
|
||||
- ✅ **Always**: 新增模块时在 `zh-cn/` 和 `en/` 下同时创建语言文件
|
||||
- 🚫 **Never**: 在组件中直接写中文/英文文本
|
||||
134
docs/frontend/overview.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 前端开发规范
|
||||
|
||||
## 技术栈
|
||||
|
||||
Vue 3 (Composition API) + TypeScript 5.x + Vite 5.x + Element Plus + Tailwind CSS 3.x + Pinia
|
||||
|
||||
## 综合示例:列表页 + 编辑对话框
|
||||
|
||||
### 枚举定义
|
||||
|
||||
```typescript
|
||||
// src/views/system/enums.ts
|
||||
import { EnumValue } from '@/common/Enum';
|
||||
|
||||
export const AccountStatusEnum = {
|
||||
Enable: EnumValue.of(1, 'system.account.statusEnable').tagTypeSuccess(),
|
||||
Disable: EnumValue.of(-1, 'system.account.statusDisable').tagTypeDanger(),
|
||||
};
|
||||
```
|
||||
|
||||
### API 定义
|
||||
|
||||
```typescript
|
||||
// src/views/system/api.ts
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const accountApi = {
|
||||
list: Api.newGet('/sys/accounts'),
|
||||
save: Api.newPost('/sys/accounts'),
|
||||
update: Api.newPut('/sys/accounts/{id}'),
|
||||
del: Api.newDelete('/sys/accounts/{id}'),
|
||||
};
|
||||
```
|
||||
|
||||
### 列表页
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<page-table ref="pageTableRef" :page-api="accountApi.list" :search-items="searchItems" v-model:query-form="query" :columns="columns">
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="'account:add'" type="primary" @click="onAdd">
|
||||
{{ $t('common.create') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #action="{ data }">
|
||||
<el-button link v-auth="'account:edit'" @click="onEdit(data)">
|
||||
{{ $t('common.edit') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
<AccountEdit v-model:visible="editVisible" :data="editData" @success="onEditSuccess" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { accountApi } from '../api';
|
||||
import { AccountStatusEnum } from '../enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { SearchItem, TableColumn } from '@/components/pagetable';
|
||||
import AccountEdit from './AccountEdit.vue';
|
||||
|
||||
const pageTableRef = ref();
|
||||
const editVisible = ref(false);
|
||||
const editData = ref<any>(null);
|
||||
|
||||
const query = ref({ username: '', status: null as number | null, pageNum: 1, pageSize: 10 });
|
||||
|
||||
const searchItems = [SearchItem.input('username', 'common.username'), SearchItem.select('status', 'common.status', AccountStatusEnum)];
|
||||
const columns = [
|
||||
TableColumn.new('username', 'common.username'),
|
||||
TableColumn.new('status', 'common.status').typeTag(AccountStatusEnum),
|
||||
TableColumn.new('action', 'common.operation').isSlot().fixedRight(),
|
||||
];
|
||||
|
||||
const onAdd = () => { editData.value = null; editVisible.value = true; };
|
||||
const onEdit = (row: any) => { editData.value = row; editVisible.value = true; };
|
||||
const onEditSuccess = () => { editVisible.value = false; pageTableRef.value?.search(); };
|
||||
</script>
|
||||
```
|
||||
|
||||
### 编辑对话框
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="dialogTitle" width="500px" @close="onDialogClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item :label="$t('common.username')" prop="username">
|
||||
<el-input v-model="form.username" :disabled="!!form.id" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="onCancel">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { accountApi } from '../api';
|
||||
import { useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
|
||||
const props = defineProps<{ visible?: boolean; data?: any }>();
|
||||
const emit = defineEmits(['update:visible', 'success']);
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({ id: undefined, username: '', name: '', status: 1 });
|
||||
const visible = computed({ get: () => props.visible, set: (val) => emit('update:visible', val) });
|
||||
const { t } = useI18n();
|
||||
const dialogTitle = computed(() => (form.id ? t('system.account.editAccount') : t('system.account.addAccount')));
|
||||
|
||||
watch(() => props.data, (newVal) => { newVal ? Object.assign(form, newVal) : resetForm(); }, { immediate: true });
|
||||
|
||||
const resetForm = () => { form.id = undefined; form.username = ''; form.name = ''; form.status = 1; formRef.value?.clearValidate(); };
|
||||
const onSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
form.id ? await accountApi.update.request(form) : await accountApi.save.request(form);
|
||||
useI18nOperateSuccessMsg();
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} finally { submitting.value = false; }
|
||||
};
|
||||
const onCancel = () => { visible.value = false; };
|
||||
const onDialogClose = () => { resetForm(); };
|
||||
</script>
|
||||
```
|
||||
35
docs/frontend/style.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 样式与 UI 规范
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
优先使用 Tailwind 工具类,支持 `dark:` 前缀:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="flex items-center justify-between p-4 bg-white dark:bg-gray-800">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">Label</span>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 权限控制
|
||||
|
||||
按钮权限使用 `v-auth` 指令:
|
||||
|
||||
```vue
|
||||
<el-button v-auth="'account:add'" type="primary" @click="onAdd">新增</el-button>
|
||||
```
|
||||
|
||||
## 类型安全
|
||||
|
||||
- 避免 `any`,使用可选链 `?.`
|
||||
- 使用 TypeScript 严格模式
|
||||
|
||||
## 边界
|
||||
|
||||
- ✅ **Always**: 优先使用 Tailwind CSS
|
||||
- 🚫 **Never**: 使用固定高度计算,优先用 Flexbox
|
||||
59
docs/server/api.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# API 层规范
|
||||
|
||||
## Handler 标准结构
|
||||
|
||||
```go
|
||||
type Db struct {
|
||||
dbApp application.Db `inject:"T"`
|
||||
tagApp tagapp.TagTree `inject:"T"`
|
||||
}
|
||||
|
||||
// @router /api/dbs [get]
|
||||
func (d *Db) Dbs(rc *req.Ctx) {
|
||||
queryCond := req.BindQuery[entity.DbQuery](rc) // 1. 绑定参数
|
||||
loginAccount := rc.GetLoginAccount() // 2. 获取上下文
|
||||
result, err := d.dbApp.GetPageList(queryCond) // 3. 调用应用层
|
||||
biz.ErrIsNil(err) // 4. 断言错误(仅API层)
|
||||
rc.ResData = result // 5. 返回结果
|
||||
}
|
||||
```
|
||||
|
||||
## 路由配置
|
||||
|
||||
```go
|
||||
func (d *Db) ReqConfs() *req.Confs {
|
||||
return req.NewConfs("/dbs",
|
||||
req.NewGet("", d.Dbs),
|
||||
req.NewPost("", d.Save).Log(req.NewLogSaveI(imsg.LogDbSave)),
|
||||
req.NewDelete(":dbId", d.DeleteDb).Log(req.NewLogSaveI(imsg.LogDbDelete)),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 断言边界
|
||||
|
||||
**✅ API 层可用断言**:
|
||||
|
||||
```go
|
||||
func (d *Db) Save(rc *req.Ctx) {
|
||||
form := req.BindFormAndValid[form.DbForm](rc)
|
||||
biz.IsTrue(form.InstanceId > 0, "实例ID不能为空")
|
||||
biz.ErrIsNil(d.dbApp.SaveDb(rc, &entity.Db{Name: form.Name}))
|
||||
rc.ResData = "保存成功"
|
||||
}
|
||||
```
|
||||
|
||||
**🚫 Application 层禁止断言,必须返回 error**:
|
||||
|
||||
```go
|
||||
func (d *dbAppImpl) SaveDb(ctx context.Context, db *entity.Db) error {
|
||||
if db.Name == "" {
|
||||
return errorx.NewBiz("名称不能为空")
|
||||
}
|
||||
return d.Save(ctx, db)
|
||||
}
|
||||
```
|
||||
51
docs/server/application.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Application 层规范
|
||||
|
||||
## 接口与实现
|
||||
|
||||
```go
|
||||
type Db interface {
|
||||
base.App[*entity.Db]
|
||||
GetPageList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
|
||||
SaveDb(ctx context.Context, entity *entity.Db) error
|
||||
}
|
||||
|
||||
type dbAppImpl struct {
|
||||
base.AppImpl[*entity.Db, repository.Db]
|
||||
dbInstanceApp Instance `inject:"T"`
|
||||
tagApp tagapp.TagTree `inject:"T"`
|
||||
}
|
||||
var _ Db = (*dbAppImpl)(nil)
|
||||
|
||||
func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
|
||||
// 1. 参数校验(返回error)
|
||||
if dbEntity.Name == "" {
|
||||
return errorx.NewBiz("名称不能为空")
|
||||
}
|
||||
// 2. 业务检查
|
||||
oldDb := &entity.Db{Name: dbEntity.Name, InstanceId: dbEntity.InstanceId}
|
||||
if dbEntity.Id == 0 && d.GetByCond(oldDb) == nil {
|
||||
return errorx.NewBizI(ctx, imsg.ErrDbNameExist)
|
||||
}
|
||||
// 3. 持久化
|
||||
return d.Save(ctx, dbEntity)
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
```go
|
||||
// 普通业务错误
|
||||
return errorx.NewBiz("数据库名称已存在")
|
||||
|
||||
// 国际化错误
|
||||
return errorx.NewBizI(ctx, imsg.ErrDbNameExist)
|
||||
```
|
||||
|
||||
## 边界
|
||||
|
||||
- ✅ **Always**: 参数校验后返回 error,禁止 panic
|
||||
- 🚫 **Never**: 在 application 层使用 `biz.ErrIsNil` 或 `biz.IsTrue`
|
||||
59
docs/server/architecture.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Go 分层架构与目录规范
|
||||
|
||||
## 分层目录
|
||||
|
||||
```
|
||||
internal/{module}/
|
||||
├── api/ # HTTP请求处理、参数绑定、响应返回
|
||||
│ ├── form/ # 请求表单结构体
|
||||
│ └── vo/ # 响应视图对象
|
||||
├── application/ # 业务逻辑编排、事务控制
|
||||
│ └── dto/ # 数据传输对象
|
||||
├── domain/ # 核心业务逻辑、实体定义
|
||||
│ ├── entity/ # 领域实体
|
||||
│ └── repository/ # 仓储接口定义
|
||||
├── infra/ # 数据持久化、外部服务调用
|
||||
│ └── persistence/ # 仓储实现
|
||||
├── imsg/ # 国际化消息定义
|
||||
└── init/ # 模块初始化(依赖注册、路由注册)
|
||||
```
|
||||
|
||||
## 命名规范
|
||||
|
||||
- **模块/包名**: 小写无分隔符(`machine`, `dbinstance`)
|
||||
- **文件名**: 小写+下划线(`db.go`, `db_sql_exec.go`)
|
||||
- **结构体/常量**: PascalCase
|
||||
- **接口**: 以 `er` 结尾或名词(`Reader`, `Repository`)
|
||||
- **变量/函数**: camelCase
|
||||
|
||||
## IOC 依赖注入
|
||||
|
||||
```go
|
||||
// 1. 定义接口
|
||||
type Db interface {
|
||||
base.App[*entity.Db]
|
||||
GetPageList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
|
||||
}
|
||||
|
||||
// 2. 实现接口并注入依赖
|
||||
type dbAppImpl struct {
|
||||
base.AppImpl[*entity.Db, repository.Db]
|
||||
dbInstanceApp Instance `inject:"T"` // T=按类型注入
|
||||
tagApp tagapp.TagTree `inject:"T"`
|
||||
}
|
||||
var _ Db = (*dbAppImpl)(nil)
|
||||
|
||||
// 3. 模块初始化时注册
|
||||
func init() {
|
||||
ioc.Register(&dbAppImpl{})
|
||||
}
|
||||
```
|
||||
|
||||
## 边界
|
||||
|
||||
- ✅ **Always**: 依赖单向流动,上层依赖下层接口,禁止反向依赖
|
||||
- 🚫 **Never**: 跨层直接调用具体实现,必须通过接口
|
||||
69
docs/server/concurrent.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 并发与 Panic 处理规范
|
||||
|
||||
## 统一 Panic 捕获(gox.Recover)
|
||||
|
||||
**核心原则**:严禁手动编写 `defer func() { recover() }`,必须使用 `gox.Recover()`
|
||||
|
||||
### 场景1:仅记录日志
|
||||
|
||||
```go
|
||||
func (s *Service) ProcessData(data []byte) {
|
||||
defer gox.Recover()
|
||||
result := parseData(data)
|
||||
saveToDB(result)
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2:Panic 转 Error 返回
|
||||
|
||||
```go
|
||||
func (s *Service) SaveUser(ctx context.Context, user *entity.User) (err error) {
|
||||
defer gox.Recover(func(e error) {
|
||||
err = fmt.Errorf("保存用户失败: %w", e)
|
||||
})
|
||||
if err := validateUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.Insert(ctx, user)
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3:Goroutine 安全启动
|
||||
|
||||
```go
|
||||
// ✅ 推荐
|
||||
gox.Go(func() {
|
||||
sendNotification(userId, message)
|
||||
})
|
||||
|
||||
// 🚫 禁止
|
||||
go func() {
|
||||
sendNotification(userId, message)
|
||||
}()
|
||||
```
|
||||
|
||||
## Context 传递
|
||||
|
||||
所有阻塞操作必须接受 `context.Context`:
|
||||
|
||||
```go
|
||||
func (d *dbAppImpl) SaveDb(ctx context.Context, entity *entity.Db) error {
|
||||
return d.GetRepo().Insert(ctx, entity)
|
||||
}
|
||||
```
|
||||
|
||||
## 错误组使用
|
||||
|
||||
```go
|
||||
eg, ctx := errgroup.WithContext(context.Background())
|
||||
for _, task := range tasks {
|
||||
eg.Go(func() error {
|
||||
return process(ctx, task)
|
||||
})
|
||||
}
|
||||
err := eg.Wait()
|
||||
```
|
||||
44
docs/server/domain.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Domain 层规范
|
||||
|
||||
## 实体定义
|
||||
|
||||
```go
|
||||
package entity
|
||||
|
||||
import "mayfly-go/pkg/model"
|
||||
|
||||
type Db struct {
|
||||
model.Model // 必须嵌入基础模型
|
||||
model.ExtraData // 辅助字段(展示用、非查询条件)
|
||||
|
||||
Code string `json:"code" gorm:"size:32;not null;index:idx_db_code"`
|
||||
Name string `json:"name" gorm:"size:255;not null;"`
|
||||
InstanceId uint64 `json:"instanceId" gorm:"not null;"`
|
||||
}
|
||||
|
||||
type Status int8
|
||||
const (
|
||||
StatusActive Status = 1
|
||||
StatusInactive Status = 0
|
||||
)
|
||||
```
|
||||
|
||||
## ExtraData 使用原则
|
||||
|
||||
- ✅ **使用 ExtraData**: 前端展示字段、关联名称、状态文本、可选扩展信息
|
||||
- 🚫 **必须独立字段**: 查询条件、排序字段、分组统计、索引字段、核心业务字段
|
||||
|
||||
## Repository 接口
|
||||
|
||||
```go
|
||||
package repository
|
||||
|
||||
type Db interface {
|
||||
base.Repo[*entity.Db]
|
||||
GetDbList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
|
||||
}
|
||||
```
|
||||
113
docs/server/i18n.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 后端国际化(i18n)规范
|
||||
|
||||
## 文件组织
|
||||
|
||||
每个业务模块在 `internal/{module}/imsg/` 目录下维护国际化消息:
|
||||
|
||||
```
|
||||
internal/{module}/imsg/
|
||||
├── imsg.go # 消息ID常量定义(MsgId)
|
||||
├── zh_cn.go # 中文语言包
|
||||
└── en.go # 英文语言包
|
||||
```
|
||||
|
||||
### imsg.go — 常量定义
|
||||
|
||||
```go
|
||||
package imsg
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
"mayfly-go/pkg/i18n"
|
||||
)
|
||||
|
||||
func init() {
|
||||
i18n.AppendLangMsg(i18n.Zh_CN, Zh_CN)
|
||||
i18n.AppendLangMsg(i18n.En, En)
|
||||
}
|
||||
|
||||
const (
|
||||
LogDbSave = iota + consts.ImsgNumDb
|
||||
LogDbDelete
|
||||
ErrDbNameExist
|
||||
)
|
||||
```
|
||||
|
||||
### zh_cn.go — 中文语言包
|
||||
|
||||
```go
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var Zh_CN = map[i18n.MsgId]string{
|
||||
LogDbSave: "保存数据库配置",
|
||||
LogDbDelete: "删除数据库配置",
|
||||
ErrDbNameExist: "该实例下数据库名已存在",
|
||||
}
|
||||
```
|
||||
|
||||
### en.go — 英文语言包
|
||||
|
||||
```go
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var En = map[i18n.MsgId]string{
|
||||
LogDbSave: "Save database configuration",
|
||||
LogDbDelete: "Delete database configuration",
|
||||
ErrDbNameExist: "The database name already exists in this instance",
|
||||
}
|
||||
```
|
||||
|
||||
## 消息ID编号规则
|
||||
|
||||
各模块起始编号定义在 `internal/pkg/consts/consts.go`,新增模块需注册唯一起始值:
|
||||
|
||||
```go
|
||||
const (
|
||||
ImsgNumSys = 10000
|
||||
ImsgNumAuth = 20000
|
||||
ImsgNumDb = 60000
|
||||
ImsgNumAi = 140000
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
模块内使用 `iota + consts.ImsgNum{Xxx}` 自增,避免全局冲突。
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 国际化业务错误
|
||||
|
||||
```go
|
||||
return errorx.NewBizI(ctx, imsg.ErrDbNameExist)
|
||||
```
|
||||
|
||||
### 国际化操作日志
|
||||
|
||||
```go
|
||||
req.NewPost("", d.Save).Log(req.NewLogSaveI(imsg.LogDbSave))
|
||||
```
|
||||
|
||||
### 模板变量替换
|
||||
|
||||
```go
|
||||
// 定义:ErrDbNotAccess = "未配置数据库【{{.dbName}}】的操作权限"
|
||||
errorx.NewBizI(ctx, imsg.ErrDbNotAccess, "dbName", dbName)
|
||||
|
||||
// 或直接使用 i18n 包
|
||||
i18n.T(imsg.DataSyncSuccessMsg, "count", 100)
|
||||
i18n.TC(ctx, imsg.DataSyncSuccessMsg, "count", 100)
|
||||
```
|
||||
|
||||
## 边界
|
||||
|
||||
- ✅ **Always**: 新增模块时必须同步创建 `imsg.go`、`zh_cn.go`、`en.go` 三个文件
|
||||
- ✅ **Always**: 操作日志消息以 `Log` 开头,业务错误以 `Err` 开头
|
||||
- 🚫 **Never**: 在 `errorx.NewBiz("硬编码中文")` 中直接使用硬编码文本,必须走国际化
|
||||
45
docs/server/infrastructure.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Infrastructure 层规范
|
||||
|
||||
## Repository 实现
|
||||
|
||||
```go
|
||||
package persistence
|
||||
|
||||
type dbRepoImpl struct {
|
||||
base.RepoImpl[*entity.Db]
|
||||
}
|
||||
|
||||
func newDbRepo() repository.Db {
|
||||
return &dbRepoImpl{}
|
||||
}
|
||||
|
||||
func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error) {
|
||||
pd := model.NewCond().
|
||||
Eq("instance_id", condition.InstanceId).
|
||||
In("code", condition.Codes).
|
||||
Like("name", condition.Name)
|
||||
|
||||
list := []*entity.DbListPO{}
|
||||
return gormx.PageByCond(d.GetModel(), pd, condition.PageParam, list)
|
||||
}
|
||||
```
|
||||
|
||||
## GORMX 常用操作
|
||||
|
||||
```go
|
||||
// 条件构建
|
||||
pd := model.NewCond().Eq("status", 1).In("id", ids).Like("name", keyword)
|
||||
|
||||
// 分页查询
|
||||
result, err := gormx.PageByCond(repo.GetModel(), pd, pageParam, &list)
|
||||
|
||||
// 单条查询
|
||||
err := gormx.GetByCond(repo.GetModel(), pd, &entity)
|
||||
|
||||
// 更新
|
||||
err := gormx.UpdateByCond(repo.GetModel(), values, pd)
|
||||
```
|
||||
62
docs/server/quality.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 代码质量与 Git 规范
|
||||
|
||||
## 函数长度
|
||||
|
||||
- 单个函数不超过 100 行
|
||||
- 复杂逻辑拆分为私有方法
|
||||
|
||||
## Error 处理
|
||||
|
||||
```go
|
||||
// ✅ 完整处理
|
||||
result, err := doSomething()
|
||||
if err != nil {
|
||||
logx.Errorf("操作失败: %v", err)
|
||||
return errorx.NewBiz("操作失败")
|
||||
}
|
||||
|
||||
// 🚫 忽略错误
|
||||
result, _ := doSomething()
|
||||
```
|
||||
|
||||
## 资源释放
|
||||
|
||||
```go
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
```
|
||||
|
||||
## 魔法数字
|
||||
|
||||
```go
|
||||
const MaxRetryCount = 3
|
||||
if retry > MaxRetryCount { ... } // ✅
|
||||
if retry > 3 { ... } // 🚫
|
||||
```
|
||||
|
||||
## Git 提交格式
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
```
|
||||
|
||||
**Type 类型**: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
|
||||
|
||||
**示例**:
|
||||
```
|
||||
feat(db): 添加数据库备份功能
|
||||
|
||||
- 实现定时备份任务
|
||||
- 支持增量备份和全量备份
|
||||
|
||||
Closes #123
|
||||
```
|
||||
26
docs/server/security.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 安全与权限规范
|
||||
|
||||
## 权限控制
|
||||
|
||||
```go
|
||||
// 路由级别
|
||||
req.NewPost(":dbId/exec-sql", d.ExecSql).RequiredPermissionCode("db:sqlscript:run")
|
||||
|
||||
// 代码级别
|
||||
biz.IsTrue(account.HasPermission("db:sqlscript:run"), "无权限执行SQL")
|
||||
```
|
||||
|
||||
## 敏感信息
|
||||
|
||||
- 资源密码使用 AES 加密存储
|
||||
- `aes.key` 和 `jwt.key` 必须使用随机字符串
|
||||
|
||||
## OWASP 安全准则
|
||||
|
||||
- 防范 SQL 注入:使用参数化查询
|
||||
- 防范 XSS:输出转义
|
||||
- 防范 CSRF:配合前端同源策略
|
||||
@@ -5,6 +5,10 @@ VITE_PORT = 8889
|
||||
VITE_OPEN = false
|
||||
|
||||
# public path 配置线上环境路径(打包)
|
||||
VITE_PUBLIC_PATH = ''
|
||||
VITE_PUBLIC_PATH = './'
|
||||
|
||||
VITE_EDITOR=idea
|
||||
VITE_EDITOR=idea
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
@@ -4,8 +4,4 @@ ENV = 'development'
|
||||
VITE_OPEN = true
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
VITE_API_URL = '/api'
|
||||
@@ -4,6 +4,4 @@ ENV = 'production'
|
||||
# 线上环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
VITE_ROUTER_MODE = history
|
||||
@@ -1,76 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.vue'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
// http://eslint.cn/docs/rules/
|
||||
// https://eslint.vuejs.org/rules/
|
||||
// https://typescript-eslint.io/rules/no-unused-vars/
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [2],
|
||||
'vue/custom-event-name-casing': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/one-component-per-file': 'off',
|
||||
'vue/html-closing-bracket-newline': 'off',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/multiline-html-element-content-newline': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-v-model-argument': 'off',
|
||||
'vue/no-arrow-functions-in-watch': 'off',
|
||||
'vue/no-template-key': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/comment-directive': 'off',
|
||||
'vue/no-parsing-error': 'off',
|
||||
'vue/no-deprecated-v-on-native-modifier': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'no-sparse-arrays': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-constant-condition': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'no-restricted-globals': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'generator-star-spacing': 'off',
|
||||
'no-unreachable': 'off',
|
||||
'no-multiple-template-root': 'off',
|
||||
'no-unused-vars': 'error',
|
||||
'no-v-model-argument': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
// 'no-console': 'error',
|
||||
'no-redeclare': 'off',
|
||||
},
|
||||
};
|
||||
106
frontend/eslint.config.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// import js from '@eslint/js';
|
||||
// import tseslint from 'typescript-eslint';
|
||||
// import vuePlugin from 'eslint-plugin-vue';
|
||||
// import vueParser from 'vue-eslint-parser';
|
||||
|
||||
// export default tseslint.config(
|
||||
// {
|
||||
// ignores: [
|
||||
// '*.sh',
|
||||
// 'node_modules',
|
||||
// 'lib',
|
||||
// '*.md',
|
||||
// '*.scss',
|
||||
// '*.woff',
|
||||
// '*.ttf',
|
||||
// '.vscode',
|
||||
// '.idea',
|
||||
// 'dist',
|
||||
// 'mock',
|
||||
// 'public',
|
||||
// 'bin',
|
||||
// 'build',
|
||||
// 'config',
|
||||
// 'index.html',
|
||||
// 'src/assets',
|
||||
// ],
|
||||
// },
|
||||
// js.configs.recommended,
|
||||
// ...tseslint.configs.recommended,
|
||||
// ...vuePlugin.configs['flat/recommended'],
|
||||
// {
|
||||
// files: ['**/*.{js,ts,tsx,vue}'],
|
||||
// languageOptions: {
|
||||
// ecmaVersion: 2021,
|
||||
// sourceType: 'module',
|
||||
// parser: vueParser,
|
||||
// parserOptions: {
|
||||
// parser: tseslint.parser,
|
||||
// },
|
||||
// globals: {
|
||||
// browser: true,
|
||||
// es2021: true,
|
||||
// node: true,
|
||||
// console: true,
|
||||
// window: true,
|
||||
// document: true,
|
||||
// setTimeout: true,
|
||||
// },
|
||||
// },
|
||||
// plugins: {
|
||||
// vue: vuePlugin,
|
||||
// },
|
||||
// rules: {
|
||||
// '@typescript-eslint/ban-ts-ignore': 'off',
|
||||
// '@typescript-eslint/explicit-function-return-type': 'off',
|
||||
// '@typescript-eslint/no-explicit-any': 'off',
|
||||
// '@typescript-eslint/no-var-requires': 'off',
|
||||
// '@typescript-eslint/no-empty-function': 'off',
|
||||
// '@typescript-eslint/no-use-before-define': 'off',
|
||||
// '@typescript-eslint/ban-ts-comment': 'off',
|
||||
// '@typescript-eslint/ban-types': 'off',
|
||||
// '@typescript-eslint/no-non-null-assertion': 'off',
|
||||
// '@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
// '@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
// '@typescript-eslint/no-unused-vars': 'off',
|
||||
|
||||
// // Vue rules
|
||||
// 'vue/html-indent': ['error', 4],
|
||||
// 'vue/script-indent': ['error', 4],
|
||||
// 'vue/custom-event-name-casing': 'off',
|
||||
// 'vue/attributes-order': 'off',
|
||||
// 'vue/one-component-per-file': 'off',
|
||||
// 'vue/html-closing-bracket-newline': 'off',
|
||||
// 'vue/max-attributes-per-line': 'off',
|
||||
// 'vue/multiline-html-element-content-newline': 'off',
|
||||
// 'vue/singleline-html-element-content-newline': 'off',
|
||||
// 'vue/attribute-hyphenation': 'off',
|
||||
// 'vue/html-self-closing': 'off',
|
||||
// 'vue/no-multiple-template-root': 'off',
|
||||
// 'vue/require-default-prop': 'off',
|
||||
// 'vue/no-v-model-argument': 'off',
|
||||
// 'vue/no-arrow-functions-in-watch': 'off',
|
||||
// 'vue/no-template-key': 'off',
|
||||
// 'vue/no-v-html': 'off',
|
||||
// 'vue/no-unused-vars': 'off',
|
||||
// 'vue/comment-directive': 'off',
|
||||
// 'vue/no-parsing-error': 'off',
|
||||
// 'vue/no-deprecated-v-on-native-modifier': 'off',
|
||||
// 'vue/multi-word-component-names': 'off',
|
||||
|
||||
// // JavaScript rules
|
||||
// 'no-useless-escape': 'off',
|
||||
// 'no-sparse-arrays': 'off',
|
||||
// 'no-prototype-builtins': 'off',
|
||||
// 'no-constant-condition': 'off',
|
||||
// 'no-use-before-define': 'off',
|
||||
// 'no-restricted-globals': 'off',
|
||||
// 'no-restricted-syntax': 'off',
|
||||
// 'generator-star-spacing': 'off',
|
||||
// 'no-unreachable': 'off',
|
||||
// 'no-unused-vars': 'off',
|
||||
// 'no-case-declarations': 'off',
|
||||
// 'no-redeclare': 'off',
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
@@ -1,7 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh_CN">
|
||||
|
||||
<app-config />
|
||||
|
||||
<head>
|
||||
<base href="/" />
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@@ -14,7 +17,7 @@
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="application/javascript" src="./config.js"></script>
|
||||
<script type="application/javascript" src="/config.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -10,61 +10,69 @@
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"asciinema-player": "^3.9.0",
|
||||
"axios": "^1.6.2",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@logicflow/core": "^2.2.1",
|
||||
"@logicflow/extension": "^2.2.1",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"asciinema-player": "^3.15.1",
|
||||
"axios": "^1.16.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"cropperjs": "^1.6.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.4",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"dayjs": "^1.11.20",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.14.0",
|
||||
"js-base64": "^3.7.8",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"json-bigint": "^1.0.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-sql-languages": "^1.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.3.1",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode.vue": "^3.9.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^3.1.8",
|
||||
"sql-formatter": "^15.4.10",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.0",
|
||||
"vue-router": "^4.5.0",
|
||||
"shiki": "^4.0.2",
|
||||
"shiki-stream": "^0.1.4",
|
||||
"sortablejs": "^1.15.7",
|
||||
"sql-formatter": "^15.7.3",
|
||||
"trzsz": "^1.1.6",
|
||||
"uuid": "^13.0.2",
|
||||
"vue": "3.6.0-beta.11",
|
||||
"vue-element-plus-x": "^2.0.2",
|
||||
"vue-i18n": "^11.4.2",
|
||||
"vue-router": "^5.0.6",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
"xterm-addon-web-links": "^0.9.0"
|
||||
"x-markdown-vue": "0.0.200",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.31.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.84.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.1.0",
|
||||
"@types/node": "^22.19.18",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/sortablejs": "^1.15.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vue/compiler-sfc": "^3.5.34",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"code-inspector-plugin": "^1.5.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-vue": "^10.9.1",
|
||||
"postcss": "^8.5.14",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.99.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.12",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^9.4.3"
|
||||
"vue-eslint-parser": "^10.4.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
// 一行最多多少个字符
|
||||
printWidth: 160,
|
||||
// 指定每个缩进级别的空格数
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 9.4 KiB |
@@ -1,39 +1,39 @@
|
||||
<template>
|
||||
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
|
||||
<div class="h100">
|
||||
<el-watermark
|
||||
:zIndex="10000000"
|
||||
:width="210"
|
||||
v-if="themeConfig.isWatermark"
|
||||
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||
:content="themeConfig.watermarkText"
|
||||
class="h100"
|
||||
>
|
||||
<router-view v-show="themeConfig.lockScreenTime !== 0" />
|
||||
</el-watermark>
|
||||
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
|
||||
<el-config-provider
|
||||
:size="getGlobalComponentSize"
|
||||
:locale="getGlobalI18n"
|
||||
:button="{ autoInsertSpace: false, round: true }"
|
||||
:dialog="{ alignCenter: true, transition: 'dialog-bounce' }"
|
||||
>
|
||||
<el-watermark
|
||||
:zIndex="100000"
|
||||
:width="210"
|
||||
v-if="themeConfig.isWatermark"
|
||||
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||
:content="themeConfig.watermarkText"
|
||||
class="h-full!"
|
||||
>
|
||||
<router-view />
|
||||
</el-watermark>
|
||||
<router-view v-if="!themeConfig.isWatermark" />
|
||||
|
||||
<LockScreen v-if="themeConfig.isLockScreen" />
|
||||
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
|
||||
</div>
|
||||
<Setings />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="app">
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue';
|
||||
import { onMounted, nextTick, watch, computed, defineAsyncComponent } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import LockScreen from '@/layout/lockScreen/index.vue';
|
||||
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import { useIntervalFn } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import EnumValue from './common/Enum';
|
||||
import { I18nEnum } from './common/commonEnum';
|
||||
import { saveThemeConfig } from './common/utils/storage';
|
||||
|
||||
const setingsRef = ref();
|
||||
const Setings = defineAsyncComponent(() => import('@/layout/navBars/breadcrumb/setings.vue'));
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const themeConfigStores = useThemeConfig();
|
||||
@@ -42,19 +42,9 @@ const { themeConfig } = storeToRefs(themeConfigStores);
|
||||
// 定义变量内容
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
// 布局配置弹窗打开
|
||||
const openSetingsDrawer = () => {
|
||||
setingsRef.value.openDrawer();
|
||||
};
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 监听布局配置弹窗点击打开
|
||||
mittBus.on('openSetingsDrawer', () => {
|
||||
openSetingsDrawer();
|
||||
});
|
||||
|
||||
// 初始化系统主题
|
||||
themeConfigStores.initThemeConfig();
|
||||
});
|
||||
@@ -120,11 +110,6 @@ const refreshWatermarkTime = () => {
|
||||
themeConfigStores.setWatermarkNowTime();
|
||||
};
|
||||
|
||||
// 页面销毁时,关闭监听布局配置
|
||||
onUnmounted(() => {
|
||||
mittBus.off('openSetingsDrawer', () => {});
|
||||
});
|
||||
|
||||
// 监听路由的变化,设置网站标题
|
||||
watch(
|
||||
() => route.path,
|
||||
|
||||
1
frontend/src/assets/icon/ai/ai.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1775984689718" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9497" width="48" height="48"><path d="M710.8 98.1H318.5c-120 0-217.6 97.6-217.6 217.6V708c0 120 97.6 217.6 217.6 217.6h392.3c120 0 217.6-97.6 217.6-217.6V315.7c0.1-120-97.6-217.6-217.6-217.6z m-30 57.4L652.6 214c-8.8 18.3-27.7 30.2-48.1 30.2H424.9c-20.4 0-39.2-11.9-48.1-30.2l-28.2-58.6h332.2zM871.1 708c0 88.4-71.9 160.3-160.3 160.3H318.5c-88.4 0-160.3-71.9-160.3-160.3V315.7c0-77.4 55.2-142.2 128.2-157l38.6 80.2c18.3 38.1 57.5 62.7 99.7 62.7h179.6c42.3 0 81.4-24.6 99.7-62.7l38.6-80.2c73.1 14.9 128.2 79.6 128.2 157V708z" p-id="9498"></path><path d="M486.9 408.2c-4.6-9.9-14.1-15.8-24.3-16.4-0.6 0-1.3-0.1-1.9-0.1-0.7 0-1.4 0.1-2.1 0.1-10.1 0.7-19.6 6.5-24.2 16.4l-142.7 306c-6.7 14.4-0.5 31.4 13.9 38.1 3.9 1.8 8 2.7 12.1 2.7 10.8 0 21.1-6.1 26-16.6l34.4-73.8h165.1l34.4 73.8c4.9 10.4 15.2 16.6 26 16.6 4.1 0 8.2-0.9 12.1-2.7 14.4-6.7 20.6-23.8 13.9-38.1l-142.7-306z m-82 199.1l55.8-119.7 55.8 119.7H404.9zM683.1 391.2c-15.8 0-28.7 12.8-28.7 28.7v306.9c0 15.8 12.8 28.7 28.7 28.7s28.7-12.8 28.7-28.7V419.9c0-15.9-12.8-28.7-28.7-28.7z" p-id="9499"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
frontend/src/assets/icon/ai/assistant.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1775984826390" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11872" width="48" height="48"><path d="M762.112 325.632h-225.536v-65.792c49.408-11.264 86.528-55.296 86.528-108.288 0-61.184-49.92-110.848-111.104-110.848s-111.104 49.664-111.104 110.848c0 52.736 37.12 97.024 86.528 108.288v65.792h-225.536c-87.808 0-158.976 71.424-158.976 158.976V706.56c0 87.808 71.424 158.976 158.976 158.976h500.224c87.808 0 158.976-71.424 158.976-158.976v-221.696c0-87.808-71.424-159.232-158.976-159.232z m-312.064-174.08c0-34.048 27.904-61.952 61.952-61.952s61.952 27.904 61.952 61.952-27.904 61.952-61.952 61.952-61.952-27.904-61.952-61.952zM872.192 706.56c0 60.672-49.408 110.08-110.08 110.08H261.888c-60.672 0-110.08-49.408-110.08-110.08v-221.696c0-60.672 49.408-110.08 110.08-110.08h500.224c60.672 0 110.08 49.408 110.08 110.08V706.56zM724.224 934.4H299.776c-13.568 0-24.576 11.008-24.576 24.576s11.008 24.576 24.576 24.576h424.192c13.568 0 24.576-11.008 24.576-24.576s-10.752-24.576-24.32-24.576zM29.696 478.464c-13.568 0-24.576 11.008-24.576 24.576v185.088c0 13.568 11.008 24.576 24.576 24.576s24.576-11.008 24.576-24.576v-185.088c0-13.568-11.008-24.576-24.576-24.576zM994.304 478.464c-13.568 0-24.576 11.008-24.576 24.576v185.088c0 13.568 11.008 24.576 24.576 24.576s24.576-11.008 24.576-24.576v-185.088c0-13.568-11.008-24.576-24.576-24.576z" p-id="11873"></path><path d="M349.184 467.968c-70.4 0-127.488 57.088-127.488 127.488 0 70.4 57.344 127.488 127.488 127.488s127.744-57.088 127.744-127.488c-0.256-70.144-57.344-127.488-127.744-127.488z m0 206.08c-43.264 0-78.592-35.328-78.592-78.592s35.328-78.592 78.592-78.592 78.592 35.328 78.592 78.592-35.328 78.592-78.592 78.592zM674.816 467.968c-70.4 0-127.744 57.088-127.744 127.488 0 70.4 57.344 127.488 127.744 127.488s127.488-57.088 127.488-127.488c0.256-70.144-57.088-127.488-127.488-127.488z m0 206.08c-43.264 0-78.592-35.328-78.592-78.592s35.328-78.592 78.592-78.592 78.592 35.328 78.592 78.592-35.328 78.592-78.592 78.592z" p-id="11874"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
16
frontend/src/assets/icon/db/clickhouse.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 150 150">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #161616;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M27,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
|
||||
<path class="cls-1" d="M49.2,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
|
||||
<path class="cls-1" d="M71.4,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
|
||||
<path class="cls-1" d="M93.6,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
|
||||
<path class="cls-1" d="M115.9,64.6c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v19.9c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2v-19.9Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 962 B |
1
frontend/src/assets/icon/db/db.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1756305127175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22356" width="48" height="48"><path d="M959.718832 123.963683C872.444401 50.185297 704.593576 0.299912 511.850044 0.299912S151.255687 50.185297 63.981255 123.963683C23.193205 158.453578 0 198.04198 0 240.22962v543.840672c0 132.461193 229.132871 239.929708 511.850044 239.929708s511.850044-107.468515 511.850044-239.929708v-543.840672c0-42.18764-23.193205-81.776042-63.981256-116.265937zM87.774285 189.64444c19.794201-21.893586 50.685151-43.087377 89.373816-61.182075 42.287611-19.794201 92.073025-35.489603 147.956653-46.586352C384.087474 70.17944 446.869081 64.281168 511.850044 64.281168s127.76257 5.898272 186.745289 17.594845c55.883628 11.096749 105.669042 26.792151 147.956654 46.586352 38.688665 18.094699 69.579615 39.28849 89.373816 61.182075 15.795372 17.494875 23.793029 34.489896 23.793029 50.48521 0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 273.319926 63.981255 256.324905 63.981255 240.22962s7.997657-33.090306 23.79303-50.58518zM63.981255 356.495558c87.274431 73.778385 255.125256 123.66377 447.868789 123.66377s360.594357-49.885385 447.868788-123.66377v155.254515c0 16.095285-7.997657 33.090306-23.793029 50.48521-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.794201-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 544.740408 63.981255 527.745387 63.981255 511.750073V356.495558z m895.737577 427.574734c0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182076-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182076C71.978912 817.160597 63.981255 800.165576 63.981255 784.070292V627.91604c87.274431 73.778385 255.125256 123.66377 447.868789 123.663771s360.594357-49.885385 447.868788-123.663771v156.154252z" p-id="22357"></path><path d="M167.950796 519.847701m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22358"></path><path d="M167.950796 791.768037m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22359"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
1
frontend/src/assets/icon/db/table.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1756305474315" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24277" width="48" height="48"><path d="M960 0H0v1024h1024V0.146286h-64V0z m-640 960.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m0-256h-256V256.146286H640v192h-256V256.146286h-64v192h-256V256.146286h896v191.853714z" p-id="24278"></path></svg>
|
||||
|
After Width: | Height: | Size: 547 B |
1
frontend/src/assets/icon/docker/docker.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1756107672203" class="icon" viewBox="0 0 1472 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5144" width="48" height="48"><path d="M1449.66628 358.737a233.848 233.848 0 0 0-166.348-35.445 268.717 268.717 0 0 0-108.127-152.273l-31.158-20.026-22.265 30.455a258.736 258.736 0 0 0-22.01 265.39 177.353 177.353 0 0 1-74.28 21.241h-24.953V309.536H830.08228V0H624.44928v154.768H287.27328v154.704H118.68528V468.08H8.44728L3.26528 504.42a493.032 493.032 0 0 0 95.97 353.3c90.149 110.11 234.232 165.964 428.284 165.964a749.848 749.848 0 0 0 585.42-255.025 804.871 804.871 0 0 0 139.86-226.874c187.718-3.391 213.246-134.359 214.27-139.99l4.863-27.447-22.01-15.61z m-766.291-49.84v-92.068h87.717v92.068h-87.717z m-337.176 154.64v-92.068h87.59v92.068h-87.59z m168.588 0v-92.068h87.589v92.068h-87.589z m168.588 0v-92.068h87.717v92.068h-87.717z m170.38-92.068h87.524v92.068h-87.525v-92.068zM683.37428 62.125h87.717v92.003h-87.717V62.125zM514.78728 216.829h87.589v92.068h-87.525v-92.068z m-168.588 0h87.59v92.068h-87.59v-92.068zM177.61228 371.47h87.525v92.068H177.61228v-92.068zM527.19928 938.4a609.348 609.348 0 0 1-235-40.564 399.493 399.493 0 0 0 151.058-66.092 44.018 44.018 0 0 0 7.87-57.582 39.54 39.54 0 0 0-54.575-11.9 375.18 375.18 0 0 1-215.998 62.508 262.639 262.639 0 0 1-19.194-21.433 392.455 392.455 0 0 1-79.591-249.523h943.9a250.035 250.035 0 0 0 155.216-62.06l4.99-4.671a682.157 682.157 0 0 1-658.42 451.636z m699.432-482.412l-25.144-1.215-15.163-21.178a186.566 186.566 0 0 1-21.626-161.358 145.619 145.619 0 0 1 42.483 100.769l-1.663 60.525 54.83-18.682a205.505 205.505 0 0 1 111.07-1.664 170.123 170.123 0 0 1-144.787 42.803zM544.41028 629.31a69.738 69.738 0 1 1-66.412 69.674 68.139 68.139 0 0 1 66.412-69.674z m0 85.413a15.74 15.74 0 1 0-14.971-15.675 15.291 15.291 0 0 0 14.97 15.675z m0 0" p-id="5145"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
frontend/src/assets/icon/es/es-color.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M96.426667 649.173333H712.96a137.173333 137.173333 0 0 0 0-274.346666H96.426667c-12.8 43.52-19.626667 89.514667-19.626667 137.173333s6.826667 93.696 19.626667 137.173333z" fill="#07A5DE" p-id="6101"></path><path d="M563.2 25.6A486.4 486.4 0 0 0 125.354667 299.946667H837.546667c52.096 0 97.450667-29.013333 120.661333-71.808A485.76 485.76 0 0 0 563.2 25.6z" fill="#EFBF19" p-id="6102"></path><path d="M942.421333 816.64a137.258667 137.258667 0 0 0-129.749333-92.586667H125.312A486.4 486.4 0 0 0 563.2 998.4c153.344 0 290.090667-70.954667 379.221333-181.76z" fill="#3EBEB1" p-id="6103"></path><path d="M506.197333 649.173333c12.8-43.52 19.626667-89.514667 19.626667-137.173333s-6.826667-93.696-19.626667-137.173333H96.469333c-12.8 43.52-19.626667 89.514667-19.626666 137.173333s6.826667 93.696 19.626666 137.173333h409.728z" fill="#231F20" p-id="6104"></path><path d="M477.269333 724.053333H125.354667a488.533333 488.533333 0 0 0 175.957333 197.888 488.533333 488.533333 0 0 0 175.957333-197.930666z" fill="#019B8F" p-id="6105"></path><path d="M301.312 102.058667a488.533333 488.533333 0 0 1 175.957333 197.930666H125.354667a488.533333 488.533333 0 0 1 175.957333-197.930666z" fill="#D8A22A" p-id="6106"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/src/assets/icon/es/es.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M465.664 679.168c105.301333 0.597333 172.970667 1.066667 202.922667 1.450667 20.48 0.256 36.181333 0.426667 47.274666 0.469333h3.84c45.824 0 84.096 8.533333 114.901334 25.258667 31.189333 16.938667 54.826667 42.368 70.826666 76.245333l1.152 2.517333a24.106667 24.106667 0 0 1-1.621333 3.413334c-46.336 67.84-101.034667 116.565333-164.096 146.346666-63.146667 29.824-134.613333 40.704-214.485333 32.469334-159.232-16.384-283.477333-106.24-372.352-269.994667a5.973333 5.973333 0 0 1 3.584-8.618667c13.653333-3.968 27.733333-6.528 41.941333-7.594666 91.306667-1.365333 170.538667-1.877333 238.165333-1.962667h27.946667z m44.885333 63.829333l-0.64 1.152c-3.754667 6.485333-9.386667 15.36-16.128 25.6l-2.645333 3.925334-1.578667 2.346666c-21.205333 31.445333-51.072 72.234667-70.784 94.464 64.853333 34.304 133.162667 45.44 227.157334 27.52 95.146667-18.090667 145.450667-52.565333 175.829333-114.090666-5.034667-10.581333-14.592-19.285333-31.488-27.733334-12.8-6.4-32.426667-11.050667-58.752-14.250666l-221.013333 1.066666z m-257.578666-5.546666l1.237333 1.536c21.504 26.112 67.712 72.277333 96.896 95.786666 15.146667-14.08 29.098667-29.397333 41.642667-45.824 13.952-18.261333 24.149333-32.64 35.370666-52.821333l-175.146666 1.322667z m471.296-360.874667c38.229333 5.077333 67.626667 18.944 88.448 41.301333 20.736 22.229333 33.024 52.992 36.565333 92.373334 3.626667 39.722667-5.76 71.808-27.733333 96.426666-20.906667 23.381333-53.461333 40.106667-97.877334 49.706667l-2.645333 0.597333-2.816 0.554667H144.725333a8.021333 8.021333 0 0 1-7.893333-6.485333 1545.173333 1545.173333 0 0 1-0.298667-1.578667c-12.373333-62.378667-18.517333-106.666667-18.517333-132.906667 0-38.570667 5.888-81.962667 17.706667-130.261333l1.066666-4.394667a7.082667 7.082667 0 0 1 6.826667-5.333333h580.650667zM197.546667 442.88l-0.853334 2.688c-7.509333 24.064-12.544 44.330667-12.117333 70.954667 0 30.293333 5.418667 54.272 13.653333 81.664h283.050667l0.341333-2.218667 0.469334-3.2c3.541333-24.448 4.010667-47.701333 4.010666-76.544 0-30.805333-1.066667-51.541333-6.4-75.264l-282.154666 1.92z m493.397333-3.029333l-131.797333 1.024 0.512 2.474666c4.48 22.357333 6.741333 43.861333 6.741333 73.216 0 30.421333-2.432 53.76-7.552 79.189334l134.826667-0.170667 1.962666-0.213333c28.16-2.901333 49.194667-7.210667 62.421334-23.04 11.52-13.866667 17.152-32.469333 17.152-55.765334 0-24.746667-6.272-42.624-19.456-54.826666-13.653333-12.714667-34.474667-19.2-61.994667-21.674667l-2.816-0.213333z m49.877333-342.784c63.104 29.824 117.845333 78.592 164.181334 146.346666l1.536 2.304a23.466667 23.466667 0 0 1-1.066667 3.669334c-16 33.92-39.594667 59.306667-70.784 76.245333-30.805333 16.768-69.12 25.258667-114.986667 25.258667-11.178667 0-28.16 0.213333-50.944 0.469333-45.098667 0.597333-112.597333 1.408-202.965333 1.493333h-14.122667c-70.613333 0-154.453333-0.512-251.733333-1.962666a207.061333 207.061333 0 0 1-42.24-7.594667 5.973333 5.973333 0 0 1-3.626667-8.618667C242.986667 170.922667 367.146667 81.066667 526.378667 64.682667c79.829333-8.277333 151.296 2.56 214.4 32.426666z m-102.101333 28.501333c-85.205333-15.36-143.957333-4.010667-213.717333 27.221333 11.648 13.312 26.410667 33.621333 40.874666 55.04l1.578667 2.389334 2.56 3.754666 3.498667 5.376 2.346666 3.584 1.237334 1.877334c18.688 28.757333 35.157333 56.746667 41.728 70.613333h213.674666l2.474667-0.298667 2.56-0.341333c21.290667-2.986667 38.144-10.794667 55.978667-19.754667 17.408-8.576 30.122667-18.304 40.106666-29.866666-49.493333-63.018667-108.586667-104.106667-194.901333-119.594667zM367.744 186.453333c-12.458667 10.069333-34.304 29.44-56.192 50.048l-1.706667 1.621334-3.328 3.157333-3.498666 3.328-1.877334 1.792-2.048 2.005333c-17.322667 16.64-33.578667 33.109333-44.501333 45.909334l179.797333-1.536-1.109333-1.877334a3067.264 3067.264 0 0 1-12.672-21.418666l-11.776-20.053334-2.474667-4.053333-2.56-4.266667-1.152-2.005333-1.237333-2.005333c-12.458667-20.693333-24.917333-40.405333-33.706667-50.645334z" fill="#2c2c2c" p-id="5739"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -47,6 +47,7 @@ function convertSvgToSymbol(svgString, symbolId) {
|
||||
iconNames.push(`icon ${name}`);
|
||||
svgsymbols += convertSvgToSymbol(allSvgIcons[path].default, name);
|
||||
}
|
||||
|
||||
svgsymbols += '</svg>';
|
||||
|
||||
var t = (t = document.getElementsByTagName('script'))[t.length - 1],
|
||||
|
||||
1
frontend/src/assets/icon/machine/machine.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1756286353957" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19008" width="48" height="48"><path d="M853.333333 554.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-170.666666a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 682.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 896h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 853.333333v-170.666666a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 725.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM853.333333 42.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128V170.666667a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 170.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 384h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 341.333333V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 213.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" p-id="19009"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
frontend/src/assets/icon/milvus/milvus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M254.816 286.4a317.504 317.504 0 0 1 450.144 0c124.512 124.704 124.512 326.688 0 451.392a317.76 317.76 0 0 1-450.144-0.192l-203.136-203.84a30.848 30.848 0 0 1 0-43.584L254.816 286.4z m401.92 59.84a233.28 233.28 0 0 0-330.656 0L176.864 496a22.784 22.784 0 0 0 0 32l149.44 149.92a233.312 233.312 0 0 0 330.688 0 234.88 234.88 0 0 0-0.192-331.648h-0.064zM972.384 492.64l-90.592-91.008a7.552 7.552 0 0 0-12.8 7.104 481.984 481.984 0 0 1 0 211.2c-1.664 7.584 7.488 12.48 12.8 7.136l90.592-90.944a30.912 30.912 0 0 0 0-43.52z" fill="#0077FF"></path><path d="M493.28 354.784c86.784 0 157.216 71.456 157.216 159.52 0 88.16-70.336 159.616-157.216 159.616-86.848 0-157.28-71.456-157.28-159.616 0-88.064 70.4-159.52 157.28-159.52z" fill="#A6D0FF"></path></svg>
|
||||
|
After Width: | Height: | Size: 872 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M475.19999999 84.5568c202.7008 0 362.6496 71.0912 373.50400001 163.6608l0.40959999 4.5568h0.5632v232.2432H795.19999999V364.288c-63.1552 48.3328-175.5648 80.5888-307.5584 82.5088l-12.4416 0.0768c-133.1968 0-247.7312-30.8224-313.93279999-78.08L155.2 364.288v136.7552c0 63.5136 128.6144 126.208 319.99999999 126.208 63.1808 0 119.5264-6.8352 166.656-18.2784-4.9408 23.552-6.4 43.5968-4.4032 60.2112-48.7936 10.6752-103.7056 16.6144-162.2528 16.6144-133.1968 0-247.7312-30.7968-313.93279999-78.08l-6.0672-4.5056v125.824c0 63.5136 128.6144 126.2336 319.99999999 126.2336 74.3168 0 139.1616-9.4464 190.6688-24.7296l15.18080001 55.5008a631.04 631.04 0 0 1-89.6256 19.584 803.8656 803.8656 0 0 1-116.22400001 8.192c-206.7456 0-369.3312-73.984-374.3488-169.1392l-0.128-4.5568V252.7744h0.56320001C107.32799999 158.0032 269.1712 84.5824 475.19999999 84.5824z m335.18080001 637.696c12.3648 0 22.4 10.0608 22.39999999 22.4256l-0.0768 74.112a22.3744 22.3744 0 0 1 8.96-9.3184c15.4112-8.704 27.0336-24.6528 33.408-46.592a22.4 22.4 0 1 1 43.008 12.4928c-9.6 33.024-28.416 58.4704-54.39999999 73.1136a22.4 22.4 0 0 1-30.92480001-9.216v40.7296a22.4 22.4 0 0 1-44.79999999 0V744.704c0-12.3648 10.0608-22.4 22.4256-22.4z m-15.6672-184.7808a22.784 22.784 0 0 1 31.51359999 0.256c9.8816 9.8816 24.6528 26.624 40.06400001 47.36 25.3184 34.048 44.2624 68.4544 53.24799999 101.9136a22.4256 22.4256 0 0 1-43.3664 11.3408c-9.8816-36.6848-35.584-76.3392-65.8432-111.488-39.7824 46.1824-69.76 97.152-69.75999999 138.5984 0 36.992 13.056 67.4048 33.89439999 81.5616l5.632 5.3248a22.4 22.4 0 0 1-30.77119999 31.7696c-33.8432-22.9376-53.5552-67.3792-53.55520001-118.656 0-39.1424 18.1248-81.8944 48.2816-125.8752a461.312 461.312 0 0 1 50.688-62.1056zM475.19999999 143.0016c-187.7504 0.0512-314.7776 60.416-319.53919999 122.7264 4.8128 62.2336 131.7888 122.5984 319.53919999 122.5984s314.7776-60.3648 319.5392-122.6496C789.92639999 203.4176 662.95039999 143.0016 475.19999999 143.0016z" ></path></svg>
|
||||
<svg t="1756389060526" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29147" width="48" height="48"><path d="M465.454545 9.402182c245.697939 0 439.575273 86.171152 452.732122 198.376727l0.496485 5.523394h0.682666v281.506909H853.333333V348.470303c-76.551758 58.585212-212.805818 97.683394-372.79806 100.010667l-15.080728 0.093091c-161.450667 0-300.280242-37.360485-380.524606-94.642425L77.575758 348.470303v165.763879c0 76.986182 155.896242 152.979394 387.878787 152.979394 76.582788 0 144.880485-8.285091 202.007273-22.155637-5.988848 28.547879-7.757576 52.844606-5.337212 72.983273-59.143758 12.939636-125.703758 20.138667-196.670061 20.138667-161.450667 0-300.280242-37.329455-380.524606-94.642424l-7.354181-5.461334v152.51394c0 76.986182 155.896242 153.010424 387.878787 153.010424 90.08097 0 168.680727-11.450182 231.113697-29.975273l18.40097 67.273697a764.89697 764.89697 0 0 1-108.637091 23.738182 974.382545 974.382545 0 0 1-140.877576 9.929697c-250.600727 0-447.674182-89.677576-453.756121-205.017212l-0.155151-5.523394V213.302303h0.682666C19.549091 98.428121 215.722667 9.433212 465.454545 9.433212z m406.279758 772.964848c14.987636 0 27.151515 12.194909 27.151515 27.182546l-0.093091 89.832727a27.120485 27.120485 0 0 1 10.860606-11.29503c18.680242-10.550303 32.768-29.882182 40.494546-56.475152a27.151515 27.151515 0 1 1 52.130909 15.142788c-11.636364 40.029091-34.443636 70.873212-65.939394 88.622546a27.151515 27.151515 0 0 1-37.484606-11.17091v49.369213a27.151515 27.151515 0 0 1-54.30303 0V809.580606c0-14.987636 12.194909-27.151515 27.182545-27.151515z m-18.990545-223.976727a27.61697 27.61697 0 0 1 38.198303 0.310303c11.977697 11.977697 29.882182 32.271515 48.562424 57.406061 30.68897 41.270303 53.651394 82.97503 64.54303 123.531636a27.182545 27.182545 0 0 1-52.565333 13.746424c-11.977697-44.466424-43.132121-92.532364-79.80994-135.136969-48.221091 55.978667-84.557576 117.76-84.557575 167.99806 0 44.838788 15.825455 81.702788 41.084121 98.862546l6.826667 6.454303a27.151515 27.151515 0 0 1-37.298425 38.508606c-41.022061-27.803152-64.915394-81.671758-64.915394-143.825455 0-47.445333 21.969455-99.265939 58.523152-152.576a559.166061 559.166061 0 0 1 61.44-75.279515zM465.454545 80.244364C237.878303 80.306424 83.905939 153.475879 78.134303 229.003636 83.968 304.407273 237.878303 377.607758 465.454545 377.607758S847.003152 304.407273 852.774788 228.941576C846.941091 153.475879 693.030788 80.244364 465.454545 80.244364z" p-id="29148"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.5 KiB |
1
frontend/src/assets/icon/mq/kafka.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M668.8 460.8c64 0 118.4-54.4 118.4-118.4S732.8 224 668.8 224s-118.4 54.4-118.4 118.4c0 12.8 3.2 22.4 6.4 35.2L502.4 416c-22.4-28.8-57.6-48-92.8-57.6v-64c54.4-12.8 92.8-60.8 92.8-115.2 0-64-54.4-118.4-118.4-118.4s-118.4 54.4-118.4 118.4c0 54.4 38.4 102.4 92.8 115.2v67.2c-80 16-134.4 96-118.4 176 12.8 60.8 57.6 105.6 118.4 118.4v70.4c-54.4 12.8-92.8 57.6-92.8 115.2 0 64 54.4 118.4 118.4 118.4s118.4-54.4 118.4-118.4c0-54.4-38.4-102.4-92.8-115.2v-70.4c35.2-6.4 70.4-22.4 92.8-54.4l54.4 41.6c-3.2 9.6-6.4 22.4-6.4 35.2 3.2 64 54.4 115.2 121.6 115.2 64-3.2 115.2-54.4 115.2-121.6-3.2-64-54.4-115.2-118.4-115.2-35.2 0-67.2 16-89.6 41.6l-54.4-41.6c6.4-16 9.6-35.2 9.6-54.4 0-16-3.2-35.2-9.6-48l54.4-38.4c19.2 32 54.4 48 89.6 44.8z m0-176c32 0 54.4 25.6 54.4 54.4s-25.6 54.4-54.4 54.4c-32 0-54.4-25.6-54.4-54.4-3.2-28.8 22.4-54.4 54.4-54.4z m-345.6-105.6c0-32 25.6-54.4 54.4-54.4 32 0 54.4 25.6 54.4 54.4s-25.6 54.4-54.4 54.4c-28.8 3.2-54.4-22.4-54.4-54.4z m115.2 662.4c0 32-22.4 57.6-54.4 57.6s-57.6-22.4-57.6-54.4 22.4-57.6 54.4-57.6 57.6 25.6 57.6 54.4z m-54.4-249.6c-44.8-3.2-76.8-41.6-73.6-86.4 3.2-38.4 35.2-70.4 73.6-73.6 44.8 0 80 35.2 80 80 0 41.6-38.4 76.8-80 80z m284.8 32c28.8 0 54.4 22.4 54.4 54.4s-22.4 54.4-51.2 54.4h-3.2c-32 0-54.4-25.6-54.4-57.6s25.6-54.4 54.4-54.4v3.2z" fill="#0171F1"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/src/assets/icon/mq/mq.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M750.08 657.92c-16.384 0-30.208 2.56-44.032 8.192l-44.032-52.224c33.28-38.4 55.296-90.624 55.296-145.92s-19.456-101.888-52.224-140.288l27.648-24.576c16.384 8.192 33.28 13.824 52.224 13.824 63.488 0 115.712-52.224 115.712-115.712s-52.224-115.712-115.712-115.712c-63.488 0-115.712 52.224-115.712 115.712 0 13.824 2.56 30.208 8.192 41.472l-35.84 30.208c-34.304-19.968-73.216-30.208-112.64-30.208-44.032 0-85.504 13.824-121.344 32.768l-41.472-52.224c0-5.632 2.56-13.824 2.56-19.456 0-40.96-32.768-74.24-73.728-74.24h-0.512c-40.96 0-74.24 32.768-74.24 73.728v0.512c0 40.96 32.768 74.24 73.728 74.24H262.656L306.688 332.8c-27.648 38.4-46.592 85.504-46.592 134.656 0 35.84 8.192 71.68 24.576 101.888l-35.84 30.208c-8.192-5.632-19.456-5.632-30.208-5.632-49.664 0-90.624 41.472-90.624 90.624 0 49.664 41.472 90.624 90.624 90.624s90.624-41.472 90.624-90.624c0-8.192 0-16.384-2.56-24.576l30.208-27.648c41.472 35.84 93.696 57.856 154.112 57.856 33.28 0 63.488-5.632 90.624-19.456l46.592 57.856c-11.264 19.456-19.456 44.032-19.456 68.608 0 77.312 63.488 140.288 143.36 140.288s143.36-63.488 143.36-140.288c0.512-75.776-65.536-139.264-145.408-139.264z m-261.632-11.264c-99.328 0-181.76-79.872-181.76-178.688C306.688 368.64 389.12 289.28 488.448 289.28s181.76 79.872 181.76 178.688c0 99.328-82.432 178.688-181.76 178.688zM430.592 465.408c0.512 18.432-13.824 33.28-32.256 33.792-18.432 0.512-33.28-13.824-33.792-32.256v-1.536c0-18.432 14.848-32.768 33.28-32.768 18.432-0.512 32.768 14.336 32.768 32.768z m88.064 0c0 18.432-14.848 33.28-32.768 33.28-18.432 0-33.28-14.848-33.28-32.768 0-18.432 14.848-32.768 33.28-32.768s32.768 13.824 32.768 32.256z m91.136 0c0 18.432-14.848 33.28-32.768 33.28s-33.28-14.848-33.28-32.768c0-18.432 14.848-32.768 33.28-32.768 17.92-1.024 32.768 13.824 32.768 32.256z" fill="#2B85FB"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M897.8125003 599.75c-0.37500029 8.58750029-11.73750029 18.18749971-35.06250058 30.375-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.95000029-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.3125-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.7375-23.81249971-11.40000029-35.96249971-20.99999971-36.37499942-30.07500029v90.97499971c0 9.07499971 12.52500029 18.71250029 36.37499942 30.11250058 47.7 22.79999971 312.48749971 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.35000029 52.94999971-27.60000029 301.76250029-129.89999971 349.79999942-154.95000029 24.4125-12.7125 35.25000029-22.6125 35.25000029-31.57499971v-89.70000029l-0.18749971-0.07499971z" fill="" ></path><path d="M897.77500001 451.43749971c-0.37500029 8.58750029-11.73750029 18.15000029-35.02500029 30.33750058-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.94999942-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.35000029-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.77500029-23.81249971-11.3625-35.96249971-20.99999971-36.37499942-30.0375v90.97500058c0 9.07499971 12.52500029 18.675 36.37499942 30.07499942 47.7 22.79999971 312.45000029 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.3125 52.94999971-27.60000029 301.76250029-129.9375 349.79999942-154.94999942 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125v-89.70000029l-0.225-0.03750029z" fill="" ></path><path d="M897.77500001 297.61250029c0.45-9.15000029-11.51250029-17.17499971-35.58750029-26.02500029-46.8-17.13750029-294.11250029-115.57500029-341.47499942-132.93749971-47.3625-17.325-66.63750029-16.61249971-122.25000058 3.375C342.7375003 161.93750029 79.41249972 265.24999971 32.5750003 283.55000029c-23.43750029 9.225-34.875 17.73749971-34.50000058 26.81249942V401.37499971c0 9.07499971 12.52500029 18.675 36.37500029 30.07500029 47.7 22.79999971 312.45000029 129.78749971 354.30000029 149.77500029 41.85 19.98749971 71.25000029 20.25 124.23749942-7.35000029 52.94999971-27.60000029 301.76250029-129.9375 349.80000029-154.95000029 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125V297.61250029h-0.30000058zM320.31250001 383.75l208.53749971-32.02499971-63 92.3625-145.49999942-60.33750029z m461.25-83.17500029l-123.33750029 48.75000029-13.3875 5.24999971-123.26249971-48.74999942 136.575-54 123.37499971 48.74999942z m-362.09999971-89.36249942l-20.17500029-37.20000058 62.92500029 24.60000058 59.32499942-19.42500058-16.04999971 38.43750058 60.45000029 22.64999942-77.9625 8.1-17.47500029 42.00000029-28.19999971-46.83750029-90-8.1 67.1625-24.22499942z m-155.3625 52.49999971c61.57500029 0 111.44999971 19.31249971 111.44999971 43.16249971s-49.87500029 43.2-111.44999971 43.2-111.4875-19.38750029-111.4875-43.2c0-23.85 49.91249971-43.2 111.4875-43.2z" fill="" ></path></svg>
|
||||
<svg t="1756388835244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25729" width="48" height="48"><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="25730"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
@@ -1 +1,9 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621859009605" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9709" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M820.203922 812.172549H684.67451v-45.176471h112.439215V279.090196H633.47451l-85.333334 277.082353c-3.011765 10.039216-12.047059 16.062745-22.086274 16.062745-10.039216 0-19.07451-7.027451-21.082353-17.066667l-71.278431-280.094117h-180.705883V762.980392h120.470589v45.176471H229.898039c-12.047059 0-22.086275-10.039216-22.086274-22.086275V252.988235c0-12.047059 10.039216-22.086275 22.086274-22.086274H451.764706c10.039216 0 19.07451 7.027451 22.086274 17.066666l55.215687 218.854902L595.32549 250.980392c3.011765-9.035294 12.047059-16.062745 21.082353-16.062745h202.792157c12.047059 0 22.086275 10.039216 22.086275 22.086275v533.082353c1.003922 12.047059-9.035294 22.086275-21.082353 22.086274z m0 0" fill="#e25813" p-id="9710"></path><path d="M731.858824 425.662745c4.015686-12.047059-2.007843-25.098039-14.054902-29.113725-12.047059-4.015686-25.098039 2.007843-29.113726 14.054902L563.2 766.996078h-73.286275L371.45098 410.603922c-4.015686-12.047059-17.066667-18.070588-28.109804-14.054902-12.047059 4.015686-18.070588 17.066667-14.054901 28.109804l123.482352 371.45098c3.011765 9.035294 12.047059 15.058824 21.082353 15.058823h72.282353l-53.207843 160.627451 46.180392 2.007844 192.752942-548.141177z" fill="#2c2c2c" p-id="9711"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1621859009605" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="9709" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="200" height="200">
|
||||
<defs><style type="text/css"></style></defs>
|
||||
<path d="M820.203922 812.172549H684.67451v-45.176471h112.439215V279.090196H633.47451l-85.333334 277.082353c-3.011765 10.039216-12.047059 16.062745-22.086274 16.062745-10.039216 0-19.07451-7.027451-21.082353-17.066667l-71.278431-280.094117h-180.705883V762.980392h120.470589v45.176471H229.898039c-12.047059 0-22.086275-10.039216-22.086274-22.086275V252.988235c0-12.047059 10.039216-22.086275 22.086274-22.086274H451.764706c10.039216 0 19.07451 7.027451 22.086274 17.066666l55.215687 218.854902L595.32549 250.980392c3.011765-9.035294 12.047059-16.062745 21.082353-16.062745h202.792157c12.047059 0 22.086275 10.039216 22.086275 22.086275v533.082353c1.003922 12.047059-9.035294 22.086275-21.082353 22.086274z m0 0" fill="#e25813" p-id="9710" stroke-width="30" stroke="#e25813"></path>
|
||||
<path d="M731.858824 425.662745c4.015686-12.047059-2.007843-25.098039-14.054902-29.113725-12.047059-4.015686-25.098039 2.007843-29.113726 14.054902L563.2 766.996078h-73.286275L371.45098 410.603922c-4.015686-12.047059-17.066667-18.070588-28.109804-14.054902-12.047059 4.015686-18.070588 17.066667-14.054901 28.109804l123.482352 371.45098c3.011765 9.035294 12.047059 15.058824 21.082353 15.058823h72.282353l-53.207843 160.627451 46.180392 2.007844 192.752942-548.141177z" fill="#2c2c2c" p-id="9711" stroke-width="30" stroke="#2c2c2c"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,10 +1,12 @@
|
||||
import request from './request';
|
||||
import { useApiFetch } from '@/hooks/useRequest';
|
||||
import { RequestOptions, useApiFetch } from '@/hooks/useRequest';
|
||||
|
||||
/**
|
||||
* 可用于各模块定义各自api请求
|
||||
* T 请求返回的数据类型
|
||||
* P 请求参数类型
|
||||
*/
|
||||
class Api {
|
||||
class Api<T = any, P = any> {
|
||||
/**
|
||||
* 请求url
|
||||
*/
|
||||
@@ -45,29 +47,30 @@ class Api {
|
||||
|
||||
/**
|
||||
* 响应式使用该api
|
||||
* @param params 响应式params
|
||||
* @param param 请求参数
|
||||
* @param reqOptions 其他可选值
|
||||
* @returns
|
||||
*/
|
||||
useApi<T>(params: any = null, reqOptions: RequestInit = {}) {
|
||||
return useApiFetch<T>(this, params, reqOptions);
|
||||
useApi(param?: P, reqOptions?: RequestOptions) {
|
||||
return useApiFetch<T, P>(this, param, reqOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch 请求对应的该api
|
||||
* @param {Object} param 请求该api的参数
|
||||
* @param options options
|
||||
*/
|
||||
async request(param: any = null, options: any = {}): Promise<any> {
|
||||
async request(param?: P, options: any = {}): Promise<T> {
|
||||
const { execute, data } = this.useApi(param, options);
|
||||
await execute();
|
||||
return data.value;
|
||||
const res = await execute();
|
||||
return (data.value as T) || (res as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* xhr 请求对应的该api
|
||||
* @param {Object} param 请求该api的参数
|
||||
*/
|
||||
async xhrReq(param: any = null, options: any = {}): Promise<any> {
|
||||
async xhrReq(param: any = null, options: any = {}): Promise<T> {
|
||||
if (this.beforeHandler) {
|
||||
await this.beforeHandler(param);
|
||||
}
|
||||
@@ -81,40 +84,40 @@ class Api {
|
||||
* @param url url
|
||||
* @param method 请求方法(get,post,put,delete...)
|
||||
*/
|
||||
static create(url: string, method: string): Api {
|
||||
return new Api(url, method);
|
||||
static create<T = any, P = any>(url: string, method: string): Api<T> {
|
||||
return new Api<T, P>(url, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建get api
|
||||
* @param url url
|
||||
*/
|
||||
static newGet(url: string): Api {
|
||||
return Api.create(url, 'get');
|
||||
static newGet<T = any, P = any>(url: string): Api<T, P> {
|
||||
return Api.create<T, P>(url, 'get');
|
||||
}
|
||||
|
||||
/**
|
||||
* new post api
|
||||
* @param url url
|
||||
*/
|
||||
static newPost(url: string): Api {
|
||||
return Api.create(url, 'post');
|
||||
static newPost<T = any, P = any>(url: string): Api<T, P> {
|
||||
return Api.create<T, P>(url, 'post');
|
||||
}
|
||||
|
||||
/**
|
||||
* new put api
|
||||
* @param url url
|
||||
*/
|
||||
static newPut(url: string): Api {
|
||||
return Api.create(url, 'put');
|
||||
static newPut<T = any, P = any>(url: string): Api<T, P> {
|
||||
return Api.create<T, P>(url, 'put');
|
||||
}
|
||||
|
||||
/**
|
||||
* new delete api
|
||||
* @param url url
|
||||
*/
|
||||
static newDelete(url: string): Api {
|
||||
return Api.create(url, 'delete');
|
||||
static newDelete<T = any, P = any>(url: string): Api<T, P> {
|
||||
return Api.create<T, P>(url, 'delete');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
class SocketBuilder {
|
||||
websocket: WebSocket;
|
||||
|
||||
constructor(url: string) {
|
||||
if (typeof WebSocket === 'undefined') {
|
||||
throw new Error('不支持websocket');
|
||||
}
|
||||
if (!url) {
|
||||
throw new Error('websocket url不能为空');
|
||||
}
|
||||
this.websocket = new WebSocket(url);
|
||||
}
|
||||
|
||||
static builder(url: string) {
|
||||
return new SocketBuilder(url);
|
||||
}
|
||||
|
||||
open(onopen: any) {
|
||||
this.websocket.onopen = onopen;
|
||||
return this;
|
||||
}
|
||||
|
||||
error(onerror: any) {
|
||||
this.websocket.onerror = onerror;
|
||||
return this;
|
||||
}
|
||||
|
||||
message(onmessage: any) {
|
||||
this.websocket.onmessage = onmessage;
|
||||
return this;
|
||||
}
|
||||
|
||||
close(onclose: any) {
|
||||
this.websocket.onclose = onclose;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return this.websocket;
|
||||
}
|
||||
}
|
||||
|
||||
export default SocketBuilder;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { i18n } from '@/i18n';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 不符合业务断言错误
|
||||
*/
|
||||
class AssertError extends Error {
|
||||
constructor(message: string) {
|
||||
ElMessage.error(message);
|
||||
super(message);
|
||||
// 错误类名
|
||||
this.name = 'AssertError';
|
||||
@@ -15,11 +17,11 @@ class AssertError extends Error {
|
||||
* 断言表达式为true
|
||||
*
|
||||
* @param condition 条件表达式
|
||||
* @param msg 错误消息
|
||||
* @param msgOrI18nKey 错误消息 或者 i18n key
|
||||
*/
|
||||
export function isTrue(condition: boolean, msg: string) {
|
||||
export function isTrue(condition: boolean, msgOrI18nKey: string) {
|
||||
if (!condition) {
|
||||
throw new AssertError(msg);
|
||||
throw new AssertError(i18n.global.t(msgOrI18nKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +32,7 @@ export function isTrue(condition: boolean, msg: string) {
|
||||
* @param msg 错误消息
|
||||
*/
|
||||
export function notBlank(obj: any, msg: string) {
|
||||
if (obj == null || obj == undefined || obj == '') {
|
||||
if (obj == null || obj == undefined || !obj) {
|
||||
throw new AssertError(msg);
|
||||
}
|
||||
if (Array.isArray(obj) && obj.length == 0) {
|
||||
|
||||
@@ -9,12 +9,22 @@ export const I18nEnum = {
|
||||
En: EnumValue.of('en', 'English').setExtra({ icon: 'icon layout/en', el: enLocale }),
|
||||
};
|
||||
|
||||
export const LinkTypeEnum = {
|
||||
Iframes: EnumValue.of(1, 'ifrmaes'),
|
||||
Link: EnumValue.of(2, 'link'),
|
||||
};
|
||||
|
||||
// 资源类型
|
||||
export const ResourceTypeEnum = {
|
||||
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
||||
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||
Machine: EnumValue.of(1, 'tag.machine').setExtra({ icon: 'icon machine/machine', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
||||
Db: EnumValue.of(2, 'tag.db').setExtra({ icon: 'icon db/db', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
||||
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||
Container: EnumValue.of(7, 'tag.container').setExtra({ icon: 'icon docker/docker', iconColor: 'var(--el-color-primary)' }),
|
||||
MqKafka: EnumValue.of(8, 'tag.mq.kafka').setExtra({ icon: 'icon mq/kafka', iconColor: 'var(--el-color-primary)' }),
|
||||
Milvus: EnumValue.of(9, 'tag.milvus').setExtra({ icon: 'icon milvus/milvus', iconColor: 'var(--el-color-primary)' }),
|
||||
};
|
||||
|
||||
// 标签关联的资源类型
|
||||
@@ -24,11 +34,17 @@ export const TagResourceTypeEnum = {
|
||||
|
||||
Machine: ResourceTypeEnum.Machine,
|
||||
DbInstance: ResourceTypeEnum.Db,
|
||||
EsInstance: ResourceTypeEnum.Es,
|
||||
Redis: ResourceTypeEnum.Redis,
|
||||
Mongo: ResourceTypeEnum.Mongo,
|
||||
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
AuthCert: ResourceTypeEnum.AuthCert,
|
||||
Container: ResourceTypeEnum.Container,
|
||||
|
||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
||||
MqKafka: ResourceTypeEnum.MqKafka,
|
||||
|
||||
Milvus: ResourceTypeEnum.Milvus,
|
||||
|
||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
|
||||
};
|
||||
|
||||
// 标签关联的资源类型路径
|
||||
@@ -37,4 +53,33 @@ export const TagResourceTypePath = {
|
||||
|
||||
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
|
||||
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
|
||||
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
|
||||
};
|
||||
|
||||
// 消息子类型
|
||||
export const MsgSubtypeEnum = {
|
||||
UserLogin: EnumValue.of('user.login', 'login.login').setExtra({
|
||||
notifyType: 'primary',
|
||||
}),
|
||||
|
||||
MachineFileUploadSuccess: EnumValue.of('machine.file.upload.success', 'machine.fileUploadSuccess').setExtra({
|
||||
notifyType: 'success',
|
||||
}),
|
||||
MachineFileUploadFail: EnumValue.of('machine.file.upload.fail', 'machine.fileUploadFail').setExtra({
|
||||
notifyType: 'danger',
|
||||
}),
|
||||
|
||||
DbDumpFail: EnumValue.of('db.dump.fail', 'db.dbDumpFail').setExtra({
|
||||
notifyType: 'danger',
|
||||
}),
|
||||
SqlScriptRunSuccess: EnumValue.of('db.sqlscript.run.success', 'db.sqlScriptRunSuccess').setExtra({
|
||||
notifyType: 'success',
|
||||
}),
|
||||
SqlScriptRunFail: EnumValue.of('db.sqlscript.run.fail', 'db.sqlScriptRunFail').setExtra({
|
||||
notifyType: 'danger',
|
||||
}),
|
||||
|
||||
FlowUserTaskTodo: EnumValue.of('flow.usertask.todo', 'flow.todoTask').setExtra({
|
||||
notifyType: 'primary',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
/**
|
||||
* 获取应用配置。
|
||||
* 需要后端将index.html文件中的<app-config />标签替换为script标签,并将配置项挂载到全局变量window.__APP_CONFIG__ 上
|
||||
* @returns 应用配置
|
||||
*/
|
||||
export function getAppConfig() {
|
||||
return (window as any)?.__APP_CONFIG__;
|
||||
}
|
||||
|
||||
export function getBaseApiUrl() {
|
||||
const config = getAppConfig();
|
||||
console.log('app config: ', config);
|
||||
|
||||
if (config) {
|
||||
if (!config.CTX_PATH) {
|
||||
return window.location.host;
|
||||
}
|
||||
return window.location.host + config.CTX_PATH;
|
||||
}
|
||||
|
||||
let path = window.location.pathname;
|
||||
if (path == '/') {
|
||||
return window.location.host;
|
||||
}
|
||||
|
||||
if (path.endsWith('/')) {
|
||||
// 去除最后一个/
|
||||
return window.location.host + path.replace(/\/$/, '');
|
||||
@@ -13,9 +33,6 @@ export function getBaseApiUrl() {
|
||||
const config = {
|
||||
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl || location.protocol + '//' + getBaseApiUrl()}/api`,
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.9.3',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import openApi from './openApi';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
import { notBlank } from './assert';
|
||||
|
||||
/**
|
||||
* AES 加密数据
|
||||
@@ -36,3 +39,36 @@ export function AesDecrypt(word: string, key?: string): string {
|
||||
|
||||
return decrypted.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
|
||||
var encryptor: any = null;
|
||||
|
||||
export async function getRsaPublicKey() {
|
||||
let publicKey = sessionStorage.getItem('RsaPublicKey');
|
||||
if (publicKey) {
|
||||
return publicKey;
|
||||
}
|
||||
publicKey = (await openApi.getPublicKey()) as string;
|
||||
sessionStorage.setItem('RsaPublicKey', publicKey);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公钥加密指定值
|
||||
*
|
||||
* @param value value
|
||||
* @returns 加密后的值
|
||||
*/
|
||||
export async function RsaEncrypt(value: any) {
|
||||
// 不存在则返回空值
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
|
||||
return encryptor.encrypt(value);
|
||||
}
|
||||
encryptor = new JSEncrypt();
|
||||
const publicKey = (await getRsaPublicKey()) as string;
|
||||
notBlank(publicKey, '获取公钥失败');
|
||||
encryptor.setPublicKey(publicKey); //设置公钥
|
||||
return encryptor.encrypt(value);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export const AccountUsernamePattern = {
|
||||
pattern: /^[a-zA-Z0-9_]{5,16}$/g,
|
||||
message: i18n.global.t('system.account.usernamePatternErrMsg'),
|
||||
};
|
||||
|
||||
export const ResourceCodePattern = {
|
||||
pattern: /^[a-zA-Z0-9_\-.:]{1,32}$/g,
|
||||
message: i18n.global.t('system.menu.resourceCodePatternErrMsg'),
|
||||
};
|
||||
@@ -4,9 +4,14 @@ import { getClientId, getToken } from './utils/storage';
|
||||
import { templateResolve } from './utils/string';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import axios from 'axios';
|
||||
import JSONBig from 'json-bigint';
|
||||
import { useApiFetch } from '../hooks/useRequest';
|
||||
import Api from './Api';
|
||||
|
||||
// 配置 JSONBig:将大数(int64/uint64)转为字符串,避免精度丢失
|
||||
// storeAsString: 将大数存储为字符串,而不是 BigNumber 对象
|
||||
const JSONBigString = JSONBig({ storeAsString: true });
|
||||
|
||||
export default {
|
||||
request,
|
||||
xhrReq,
|
||||
@@ -58,6 +63,22 @@ function notifyErrorMsg(msg: string) {
|
||||
const axiosInst = axios.create({
|
||||
baseURL: baseUrl, // url = base url + request url
|
||||
timeout: 60000, // request timeout
|
||||
// 使用 json-bigint 处理响应数据,解决 int64/uint64 精度丢失问题
|
||||
transformResponse: [
|
||||
function (data) {
|
||||
// 对响应数据进行转换
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
// 使用 JSONBigString 解析,大数会被转为字符串
|
||||
return JSONBigString.parse(data);
|
||||
} catch (err) {
|
||||
// 如果解析失败,返回原始数据
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// request interceptor
|
||||
@@ -204,6 +225,24 @@ function getApiUrl(url: string) {
|
||||
return baseUrl + url + '?' + joinClientParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 websocket
|
||||
*/
|
||||
export const createWebSocket = (url: string): Promise<WebSocket> => {
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
const clientParam = (url.includes('?') ? '&' : '?') + joinClientParams();
|
||||
const socket = new WebSocket(`${config.baseWsUrl}${url}${clientParam}`);
|
||||
|
||||
socket.onopen = () => {
|
||||
resolve(socket);
|
||||
};
|
||||
|
||||
socket.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 组装客户端参数,包括 token 和 clientId
|
||||
export function joinClientParams(): string {
|
||||
return `token=${getToken()}&clientId=${getClientId()}`;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import openApi from './openApi';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
import { notBlank } from './assert';
|
||||
|
||||
var encryptor: any = null;
|
||||
|
||||
export async function getRsaPublicKey() {
|
||||
let publicKey = sessionStorage.getItem('RsaPublicKey');
|
||||
if (publicKey) {
|
||||
return publicKey;
|
||||
}
|
||||
publicKey = (await openApi.getPublicKey()) as string;
|
||||
sessionStorage.setItem('RsaPublicKey', publicKey);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公钥加密指定值
|
||||
*
|
||||
* @param value value
|
||||
* @returns 加密后的值
|
||||
*/
|
||||
export async function RsaEncrypt(value: any) {
|
||||
// 不存在则返回空值
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
|
||||
return encryptor.encrypt(value);
|
||||
}
|
||||
encryptor = new JSEncrypt();
|
||||
const publicKey = (await getRsaPublicKey()) as string;
|
||||
notBlank(publicKey, '获取公钥失败');
|
||||
encryptor.setPublicKey(publicKey); //设置公钥
|
||||
return encryptor.encrypt(value);
|
||||
}
|
||||
41
frontend/src/common/rule.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useI18nPleaseInput, useI18nPleaseSelect } from '@/hooks/useI18n';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
* label: 支持 i18n key
|
||||
*/
|
||||
export const Rules = {
|
||||
requiredInput: (label: string = '', trigger: string[] = ['change', 'blur']) => {
|
||||
return {
|
||||
required: true,
|
||||
message: useI18nPleaseInput(label),
|
||||
trigger: trigger,
|
||||
};
|
||||
},
|
||||
|
||||
requiredSelect: (label: string = '', trigger: string[] = ['change', 'blur']) => {
|
||||
return {
|
||||
required: true,
|
||||
message: useI18nPleaseSelect(label),
|
||||
trigger: trigger,
|
||||
};
|
||||
},
|
||||
|
||||
accountUsername: {
|
||||
pattern: /^[a-zA-Z0-9_.@:-]{5,16}$/,
|
||||
message: i18n.global.t('system.account.usernamePatternErrMsg'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
accountPassword: {
|
||||
pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[`~!@#$%^&*()_+<>?:"{},.\/\\;'[\]])[A-Za-z\d`~!@#$%^&*()_+<>?:"{},.\/\\;'[\]]{8,}$/,
|
||||
message: i18n.global.t('login.passwordRuleTip'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
|
||||
resourceCode: {
|
||||
pattern: /^[a-zA-Z0-9_\-.:]{1,32}$/g,
|
||||
message: i18n.global.t('system.menu.resourceCodePatternErrMsg'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
};
|
||||
@@ -4,15 +4,15 @@ import { h, reactive } from 'vue';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||
|
||||
export function initSysMsgs() {
|
||||
registerDbSqlExecProgress();
|
||||
export async function initSysMsgs() {
|
||||
await registerDbSqlExecProgress();
|
||||
}
|
||||
|
||||
const sqlExecNotifyMap: Map<string, any> = new Map();
|
||||
|
||||
function registerDbSqlExecProgress() {
|
||||
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
|
||||
const content = JSON.parse(message.msg);
|
||||
async function registerDbSqlExecProgress() {
|
||||
await syssocket.registerMsgHandler('sqlScriptRunProgress', function (message: any) {
|
||||
const content = message.params;
|
||||
const id = content.id;
|
||||
let progress = sqlExecNotifyMap.get(id);
|
||||
if (content.terminated) {
|
||||
@@ -38,7 +38,7 @@ function registerDbSqlExecProgress() {
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: h(ProgressNotify, progress.props),
|
||||
type: syssocket.getMsgType(message.type),
|
||||
type: 'info',
|
||||
showClose: false,
|
||||
});
|
||||
sqlExecNotifyMap.set(id, progress);
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import Config from './config';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
|
||||
import { joinClientParams } from './request';
|
||||
import { createWebSocket } from './request';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { MsgSubtypeEnum } from './commonEnum';
|
||||
import EnumValue from './Enum';
|
||||
import { h } from 'vue';
|
||||
import { MessageRenderer } from '@/components/message/message';
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
* socket连接
|
||||
*/
|
||||
socket: any;
|
||||
socket: WebSocket | null = null;
|
||||
|
||||
/**
|
||||
* key -> 消息类别,value -> 消息对应的处理器函数
|
||||
*/
|
||||
categoryHandlers: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
messageTypes: any = {
|
||||
0: 'error',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化全局系统消息websocket
|
||||
*/
|
||||
init() {
|
||||
async init() {
|
||||
// 存在则不需要重新建立连接
|
||||
if (this.socket) {
|
||||
return;
|
||||
@@ -38,9 +31,9 @@ class SysSocket {
|
||||
return null;
|
||||
}
|
||||
console.log('init system ws');
|
||||
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
|
||||
this.socket = SocketBuilder.builder(sysMsgUrl)
|
||||
.message((event: { data: string }) => {
|
||||
try {
|
||||
this.socket = await createWebSocket('/sysmsg');
|
||||
this.socket.onmessage = async (event: { data: string }) => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
@@ -56,23 +49,32 @@ class SysSocket {
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认通知处理
|
||||
const type = this.getMsgType(message.type);
|
||||
let msg = message.msg;
|
||||
let duration = 0;
|
||||
const msgSubtype = EnumValue.getEnumByValue(MsgSubtypeEnum, message.subtype);
|
||||
if (!msgSubtype) {
|
||||
console.log(`not found msg subtype: ${message.subtype}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 动态导入 i18n 或延迟获取 i18n 实例
|
||||
let title = '';
|
||||
try {
|
||||
// 方式1: 动态导入
|
||||
const { i18n } = await import('@/i18n');
|
||||
title = i18n.global.t(msgSubtype?.label);
|
||||
} catch (e) {
|
||||
console.warn('i18n not ready, using default title');
|
||||
}
|
||||
|
||||
ElNotification({
|
||||
duration: duration,
|
||||
title: message.title,
|
||||
message: msg,
|
||||
type: type,
|
||||
duration: 0,
|
||||
title,
|
||||
message: h(MessageRenderer, { content: message.msg }),
|
||||
type: msgSubtype?.extra.notifyType || 'info',
|
||||
});
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.close(() => {
|
||||
console.log('close sys socket');
|
||||
this.socket = null;
|
||||
})
|
||||
.build();
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('open system ws error', e);
|
||||
}
|
||||
}
|
||||
|
||||
destory() {
|
||||
@@ -87,8 +89,7 @@ class SysSocket {
|
||||
* @param category 消息类别
|
||||
* @param handlerFunc 消息处理函数
|
||||
*/
|
||||
registerMsgHandler(category: any, handlerFunc: any) {
|
||||
this.init();
|
||||
async registerMsgHandler(category: any, handlerFunc: any) {
|
||||
if (this.categoryHandlers.has(category)) {
|
||||
console.log(`${category}该类别消息处理器已存在...`);
|
||||
return;
|
||||
@@ -98,10 +99,6 @@ class SysSocket {
|
||||
}
|
||||
this.categoryHandlers.set(category, handlerFunc);
|
||||
}
|
||||
|
||||
getMsgType(msgType: any) {
|
||||
return this.messageTypes[msgType];
|
||||
}
|
||||
}
|
||||
|
||||
// 全局系统消息websocket;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
/**
|
||||
* 导出CSV文件
|
||||
* @param filename 文件名
|
||||
* @param columns 列信息
|
||||
* @param datas 数据
|
||||
*/
|
||||
export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
// 二维数组
|
||||
const cvsData = [columns];
|
||||
@@ -30,6 +38,11 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
exportFile(`${filename}.csv`, csvString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出文件
|
||||
* @param filename 文件名
|
||||
* @param content 文件内容
|
||||
*/
|
||||
export function exportFile(filename: string, content: string) {
|
||||
// 导出
|
||||
let link = document.createElement('a');
|
||||
@@ -42,4 +55,79 @@ export function exportFile(filename: string, content: string) {
|
||||
link.setAttribute('download', `${filename}`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link); // 下载完成后移除元素
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字符串显示宽度(考虑中英文字符差异)
|
||||
* @param str 要计算的字符串
|
||||
* @returns 计算后的宽度值
|
||||
*/
|
||||
function getStringWidth(str: string): number {
|
||||
if (!str) return 0;
|
||||
|
||||
// 统计中文字符数量(包括中文标点)
|
||||
const chineseChars = str.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g);
|
||||
const chineseCount = chineseChars ? chineseChars.length : 0;
|
||||
|
||||
// 英文字符数量
|
||||
const englishCount = str.length - chineseCount;
|
||||
|
||||
// 中文字符按2个单位宽度计算,英文字符按1个单位宽度计算
|
||||
return chineseCount * 2 + englishCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出Excel文件
|
||||
* @param filename 文件名
|
||||
* @param sheets 多个工作表数据,每个工作表包含名称、列信息和数据
|
||||
* 示例: [{name: 'Sheet1', columns: ['列1', '列2'], datas: [{col1: '值1', col2: '值2'}]}]
|
||||
*/
|
||||
export function exportExcel(filename: string, sheets: { name: string; columns: string[]; datas: any[] }[]) {
|
||||
// 创建工作簿
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// 处理每个工作表
|
||||
sheets.forEach((sheet) => {
|
||||
// 准备表头
|
||||
const headers: any = {};
|
||||
sheet.columns.forEach((col) => {
|
||||
headers[col] = col;
|
||||
});
|
||||
|
||||
// 准备数据
|
||||
const data = [headers, ...sheet.datas];
|
||||
|
||||
// 创建工作表
|
||||
const ws = XLSX.utils.json_to_sheet(data, { skipHeader: true });
|
||||
|
||||
// 设置列宽自适应
|
||||
const colWidths: { wch: number }[] = [];
|
||||
sheet.columns.forEach((col, index) => {
|
||||
// 计算列宽:取表头和前几行数据的最大宽度
|
||||
let maxWidth = getStringWidth(col); // 表头宽度
|
||||
const checkCount = Math.min(sheet.datas.length, 10); // 只检查前10行数据
|
||||
|
||||
for (let i = 0; i < checkCount; i++) {
|
||||
const cellData = sheet.datas[i][col];
|
||||
const cellStr = cellData ? String(cellData) : '';
|
||||
const cellWidth = getStringWidth(cellStr);
|
||||
if (cellWidth > maxWidth) {
|
||||
maxWidth = cellWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置最小宽度为8,最大宽度为80
|
||||
colWidths.push({ wch: Math.min(Math.max(maxWidth + 2, 8), 80) });
|
||||
});
|
||||
|
||||
// 应用列宽设置
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
// 添加工作表到工作簿
|
||||
XLSX.utils.book_append_sheet(wb, ws, sheet.name);
|
||||
});
|
||||
|
||||
// 导出文件
|
||||
XLSX.writeFile(wb, `${filename}.xlsx`);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import dayjs from 'dayjs';
|
||||
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
|
||||
* @returns 格式化后内容
|
||||
*/
|
||||
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||
export function formatDate(date?: string | number | Date, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
@@ -30,6 +30,18 @@ export function formatByteSize(size: number, fixed = 2) {
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
export function formatDocSize(size: number, fixed = 2) {
|
||||
if (size === 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
const base = 1000;
|
||||
const exponent = Math.floor(Math.log(size) / Math.log(base));
|
||||
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
/**
|
||||
* 容量转为对应的字节大小,如 1KB转为 1024
|
||||
* @param sizeString 1kb 1gb等
|
||||
@@ -86,8 +98,8 @@ export function formatTime(time: number, unit: string = 's') {
|
||||
let result = '';
|
||||
|
||||
const timeUnits = Object.entries(units).map(([unit, duration]) => {
|
||||
const value = Math.floor(seconds / duration);
|
||||
seconds %= duration;
|
||||
const value = Math.floor(seconds / (duration as any));
|
||||
seconds %= duration as any;
|
||||
return { value, unit };
|
||||
});
|
||||
|
||||
@@ -114,3 +126,47 @@ export function formatAxis(param: any) {
|
||||
else if (hour < 22) return '晚上好';
|
||||
else return '夜里好';
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数据为美观的 JSON 字符串
|
||||
*
|
||||
* - 如果输入是对象,直接格式化为缩进 JSON
|
||||
* - 如果输入是 JSON 字符串,先解析为对象再格式化
|
||||
* - 如果解析失败,返回原始值的字符串形式
|
||||
* - 如果输入为空值,返回空字符串
|
||||
*
|
||||
* @param val - 要格式化的数据(对象或 JSON 字符串)
|
||||
* @returns 格式化后的 JSON 字符串,带 2 空格缩进
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 格式化对象
|
||||
* formatJson({ name: 'test', value: 123 })
|
||||
* // 输出: '{\n "name": "test",\n "value": 123\n}'
|
||||
*
|
||||
* // 格式化 JSON 字符串
|
||||
* formatJson('{"name":"test"}')
|
||||
* // 输出: '{\n "name": "test"\n}'
|
||||
*
|
||||
* // 处理无效输入
|
||||
* formatJson(null)
|
||||
* // 输出: ''
|
||||
* ```
|
||||
*/
|
||||
export function formatJson(val: any) {
|
||||
if (!val) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果val是字符串,尝试解析为对象后再格式化
|
||||
let data = val;
|
||||
if (typeof val === 'string') {
|
||||
data = JSON.parse(val);
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch {
|
||||
// 如果解析失败,直接返回原始值的字符串形式
|
||||
return String(val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { nextTick } from 'vue';
|
||||
import '@/theme/loading.scss';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
/**
|
||||
* 页面全局 Loading
|
||||
@@ -9,33 +11,57 @@ import '@/theme/loading.scss';
|
||||
export const NextLoading = {
|
||||
// 创建 loading
|
||||
start: () => {
|
||||
// 如果已经存在loading元素,则不重复创建
|
||||
if (document.querySelector('.loading-next')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bodys: Element = document.body;
|
||||
const div = <HTMLElement>document.createElement('div');
|
||||
div.setAttribute('class', 'loading-next');
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
if (themeConfig.value.isDark) {
|
||||
div.classList.add('dark');
|
||||
}
|
||||
|
||||
const htmls = `
|
||||
<div class="loading-next-box">
|
||||
<div class="loading-next-box-warp">
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="loading-next-box">
|
||||
<div class="loading-next-box-warp">
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
div.innerHTML = htmls;
|
||||
bodys.insertBefore(div, bodys.childNodes[0]);
|
||||
|
||||
// 插入到body的第一个子元素之前,避免影响布局
|
||||
if (bodys.firstChild) {
|
||||
bodys.insertBefore(div, bodys.firstChild);
|
||||
} else {
|
||||
bodys.appendChild(div);
|
||||
}
|
||||
},
|
||||
// 移除 loading
|
||||
done: (time: number = 1000) => {
|
||||
done: (time: number = 500) => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
const el = <HTMLElement>document.querySelector('.loading-next');
|
||||
el?.parentNode?.removeChild(el);
|
||||
if (el) {
|
||||
// 添加淡出效果
|
||||
el.style.transition = 'opacity 0.3s ease-out';
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
el?.parentNode?.removeChild(el);
|
||||
}, 300);
|
||||
}
|
||||
}, time);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// https://www.npmjs.com/package/mitt
|
||||
import mitt, { Emitter } from 'mitt';
|
||||
|
||||
// 类型
|
||||
const emitter: Emitter<any> = mitt<any>();
|
||||
|
||||
// 导出
|
||||
export default emitter;
|
||||
@@ -106,7 +106,7 @@ export function deepClone(
|
||||
result = Array.isArray(obj) ? [] : {};
|
||||
hash.set(obj, result);
|
||||
for (let key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
let value = obj[key];
|
||||
value = callback(key, value);
|
||||
result[key] = deepClone(value, callback, hash);
|
||||
|
||||
@@ -12,7 +12,13 @@ import { ElMessage } from 'element-plus';
|
||||
export function templateResolve(template: string, param: any) {
|
||||
return template.replace(/\{\w+\}/g, (word) => {
|
||||
const key = word.substring(1, word.length - 1);
|
||||
const value = param[key];
|
||||
let value;
|
||||
// 兼容FormData类型的参数
|
||||
if (param instanceof FormData) {
|
||||
value = param.get(key);
|
||||
} else {
|
||||
value = param[key];
|
||||
}
|
||||
if (value != null || value != undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
/**
|
||||
* 2020.11.29 lyt 整理
|
||||
* 工具类集合,适用于平时开发
|
||||
*/
|
||||
|
||||
// 小数或整数(不可以负数)
|
||||
export function verifyNumberIntegerAndFloat(val: string) {
|
||||
// 匹配空格
|
||||
let v = val.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 只能是数字和小数点,不能是其他输入
|
||||
v = v.replace(/[^\d.]/g, '');
|
||||
// 以0开始只能输入一个
|
||||
v = v.replace(/^0{2}$/g, '0');
|
||||
// 保证第一位只能是数字,不能是点
|
||||
v = v.replace(/^\./g, '');
|
||||
// 小数只能出现1位
|
||||
v = v.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
|
||||
// 小数点后面保留2位
|
||||
v = v.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 正整数验证
|
||||
export function verifiyNumberInteger(val: string) {
|
||||
// 匹配空格
|
||||
let v = val.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 去掉 '.' , 防止贴贴的时候出现问题 如 0.1.12.12
|
||||
v = v.replace(/[\.]*/g, '');
|
||||
// 去掉以 0 开始后面的数, 防止贴贴的时候出现问题 如 00121323
|
||||
v = v.replace(/(^0[\d]*)$/g, '0');
|
||||
// 首位是0,只能出现一次
|
||||
v = v.replace(/^0\d$/g, '0');
|
||||
// 只匹配数字
|
||||
v = v.replace(/[^\d]/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 去掉中文及空格
|
||||
export function verifyCnAndSpace(val: string) {
|
||||
// 匹配中文与空格
|
||||
let v = val.replace(/[\u4e00-\u9fa5\s]+/g, '');
|
||||
// 匹配空格
|
||||
v = v.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 去掉英文及空格
|
||||
export function verifyEnAndSpace(val: string) {
|
||||
// 匹配英文与空格
|
||||
let v = val.replace(/[a-zA-Z]+/g, '');
|
||||
// 匹配空格
|
||||
v = v.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 禁止输入空格
|
||||
export function verifyAndSpace(val: string) {
|
||||
// 匹配空格
|
||||
let v = val.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 金额用 `,` 区分开
|
||||
export function verifyNumberComma(val: string) {
|
||||
// 调用小数或整数(不可以负数)方法
|
||||
let v: any = verifyNumberIntegerAndFloat(val);
|
||||
// 字符串转成数组
|
||||
v = v.toString().split('.');
|
||||
// \B 匹配非单词边界,两边都是单词字符或者两边都是非单词字符
|
||||
v[0] = v[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
// 数组转字符串
|
||||
v = v.join('.');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 匹配文字变色(搜索时)
|
||||
export function verifyTextColor(val: string, text = '', color = 'red') {
|
||||
// 返回内容,添加颜色
|
||||
let v = text.replace(new RegExp(val, 'gi'), `<span style='color: ${color}'>${val}</span>`);
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 数字转中文大写
|
||||
export function verifyNumberCnUppercase(val: any, unit = '仟佰拾亿仟佰拾万仟佰拾元角分', v = '') {
|
||||
// 当前内容字符串添加 2个0,为什么??
|
||||
val += '00';
|
||||
// 返回某个指定的字符串值在字符串中首次出现的位置,没有出现,则该方法返回 -1
|
||||
let lookup = val.indexOf('.');
|
||||
// substring:不包含结束下标内容,substr:包含结束下标内容
|
||||
if (lookup >= 0) val = val.substring(0, lookup) + val.substr(lookup + 1, 2);
|
||||
// 根据内容 val 的长度,截取返回对应大写
|
||||
unit = unit.substr(unit.length - val.length);
|
||||
// 循环截取拼接大写
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
v += '零壹贰叁肆伍陆柒捌玖'.substr(val.substr(i, 1), 1) + unit.substr(i, 1);
|
||||
}
|
||||
// 正则处理
|
||||
v = v
|
||||
.replace(/零角零分$/, '整')
|
||||
.replace(/零[仟佰拾]/g, '零')
|
||||
.replace(/零{2,}/g, '零')
|
||||
.replace(/零([亿|万])/g, '$1')
|
||||
.replace(/零+元/, '元')
|
||||
.replace(/亿零{0,3}万/, '亿')
|
||||
.replace(/^元/, '零元');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// 手机号码
|
||||
export function verifyPhone(val: string) {
|
||||
// false: 手机号码不正确
|
||||
if (!/^((12[0-9])|(13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/.test(val)) return false;
|
||||
// true: 手机号码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 国内电话号码
|
||||
export function verifyTelPhone(val: string) {
|
||||
// false: 国内电话号码不正确
|
||||
if (!/\d{3}-\d{8}|\d{4}-\d{7}/.test(val)) return false;
|
||||
// true: 国内电话号码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 登录账号 (字母开头,允许5-16字节,允许字母数字下划线)
|
||||
export function verifyAccount(val: string) {
|
||||
// false: 登录账号不正确
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]{4,15}$/.test(val)) return false;
|
||||
// true: 登录账号正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 密码 (以字母开头,长度在6~16之间,只能包含字母、数字和下划线)
|
||||
export function verifyPassword(val: string) {
|
||||
// false: 密码不正确
|
||||
if (!/^[a-zA-Z]\w{5,15}$/.test(val)) return false;
|
||||
// true: 密码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 强密码 (字母+数字+特殊字符,长度在6-16之间)
|
||||
export function verifyPasswordPowerful(val: string) {
|
||||
// false: 强密码不正确
|
||||
if (!/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val))
|
||||
return false;
|
||||
// true: 强密码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 密码强度
|
||||
export function verifyPasswordStrength(val: string) {
|
||||
let v = '';
|
||||
// 弱:纯数字,纯字母,纯特殊字符
|
||||
if (/^(?:\d+|[a-zA-Z]+|[!@#$%^&\.*]+){6,16}$/.test(val)) v = '弱';
|
||||
// 中:字母+数字,字母+特殊字符,数字+特殊字符
|
||||
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '中';
|
||||
// 强:字母+数字+特殊字符
|
||||
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '强';
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
// IP地址
|
||||
export function verifyIPAddress(val: string) {
|
||||
// false: IP地址不正确
|
||||
if (!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(val))
|
||||
return false;
|
||||
// true: IP地址正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 邮箱
|
||||
export function verifyEmail(val: string) {
|
||||
// false: 邮箱不正确
|
||||
if (
|
||||
!/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
val
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// true: 邮箱正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 身份证
|
||||
export function verifyIdCard(val: string) {
|
||||
// false: 身份证不正确
|
||||
if (!/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(val)) return false;
|
||||
// true: 身份证正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 姓名
|
||||
export function verifyFullName(val: string) {
|
||||
// false: 姓名不正确
|
||||
if (!/^[\u4e00-\u9fa5]{1,6}(·[\u4e00-\u9fa5]{1,6}){0,2}$/.test(val)) return false;
|
||||
// true: 姓名正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 邮政编码
|
||||
export function verifyPostalCode(val: string) {
|
||||
// false: 邮政编码不正确
|
||||
if (!/^[1-9][0-9]{5}$/.test(val)) return false;
|
||||
// true: 邮政编码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// url
|
||||
export function verifyUrl(val: string) {
|
||||
// false: url不正确
|
||||
if (
|
||||
!/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
|
||||
val
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// true: url正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
// 车牌号
|
||||
export function verifyCarNum(val: string) {
|
||||
// false: 车牌号不正确
|
||||
if (
|
||||
!/^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/.test(
|
||||
val
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// true:车牌号正确
|
||||
else return true;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
const mode = import.meta.env.VITE_ROUTER_MODE;
|
||||
|
||||
/**
|
||||
* @description 获取不同路由模式所对应的 url
|
||||
* @returns {String}
|
||||
*/
|
||||
export function getNowUrl() {
|
||||
const url = {
|
||||
hash: location.hash.substring(1),
|
||||
history: location.pathname + location.search,
|
||||
};
|
||||
return url[mode];
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// vite 打包相关
|
||||
import dotenv from 'dotenv';
|
||||
export interface ViteEnv {
|
||||
VITE_PORT: number;
|
||||
VITE_OPEN: boolean;
|
||||
VITE_PUBLIC_PATH: string;
|
||||
VITE_EDITOR: string;
|
||||
}
|
||||
|
||||
export function loadEnv(): ViteEnv {
|
||||
const env = process.env.NODE_ENV;
|
||||
const ret: any = {};
|
||||
const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,];
|
||||
envList.forEach((e) => {
|
||||
dotenv.config({ path: e });
|
||||
});
|
||||
for (const envName of Object.keys(process.env)) {
|
||||
console.log(envName);
|
||||
let realName = (process.env as any)[envName].replace(/\\n/g, '\n');
|
||||
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
|
||||
if (envName === 'VITE_PORT') realName = Number(realName);
|
||||
if (envName === 'VITE_OPEN') realName = Boolean(realName);
|
||||
ret[envName] = realName;
|
||||
process.env[envName] = realName;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useUserInfo } from '@/store/userInfo';
|
||||
* @param code 权限code
|
||||
* @returns
|
||||
*/
|
||||
export function hasPerm(code: string) {
|
||||
export function hasPerm(code: string): boolean {
|
||||
if (!code) {
|
||||
return true;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function hasPerm(code: string) {
|
||||
* @returns {"xxx:save": true} key->permission code
|
||||
* @param permCodes
|
||||
*/
|
||||
export function hasPerms(permCodes: any[]) {
|
||||
export function hasPerms(permCodes: any[]): Record<string, boolean> {
|
||||
const res = {} as { [key: string]: boolean };
|
||||
for (let permCode of permCodes) {
|
||||
if (hasPerm(permCode)) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<transition @enter="onEnter" name="el-zoom-in-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
:aria-hidden="state.isShow ? 'false' : 'true'"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
role="tooltip"
|
||||
data-popper-placement="bottom"
|
||||
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="state.isShow && !allHide"
|
||||
@contextmenu="headerContextmenuClick"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
@@ -125,6 +126,10 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||
};
|
||||
|
||||
const headerContextmenuClick = (event: any) => {
|
||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||
};
|
||||
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="7" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 7" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="7" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 7" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 31" :key="item" :value="`${item}`">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 12" :key="item" :value="`${item}`">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="6" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 6" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="6" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 6" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="(item, index) of weekList" :label="item" :key="index" :value="`${index + 1}`">{{ $t(item) }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="5" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 5" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="5" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 5" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 9" :key="item" :value="`${item - 1 + fullYear}`" :label="item - 1 + fullYear" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog title="更换头像" v-model="isShowDialog" width="769px">
|
||||
<div class="cropper-warp">
|
||||
<div class="cropper-warp-left">
|
||||
<img :src="cropperImg" class="cropper-warp-left-img" />
|
||||
</div>
|
||||
<div class="cropper-warp-right">
|
||||
<div class="cropper-warp-right-title">预览</div>
|
||||
<div class="cropper-warp-right-item">
|
||||
<div class="cropper-warp-right-value">
|
||||
<img :src="cropperImgBase64" class="cropper-warp-right-value-img" />
|
||||
</div>
|
||||
<div class="cropper-warp-right-label">100 x 100</div>
|
||||
</div>
|
||||
<div class="cropper-warp-right-item">
|
||||
<div class="cropper-warp-right-value">
|
||||
<img :src="cropperImgBase64" class="cropper-warp-right-value-img cropper-size" />
|
||||
</div>
|
||||
<div class="cropper-warp-right-label">50 x 50</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancel" size="small">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" size="small">更 换</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { reactive, toRefs, nextTick } from 'vue';
|
||||
import Cropper from 'cropperjs';
|
||||
import 'cropperjs/dist/cropper.css';
|
||||
export default {
|
||||
name: 'cropperIndex',
|
||||
setup() {
|
||||
const state = reactive({
|
||||
isShowDialog: false,
|
||||
cropperImg: '',
|
||||
cropperImgBase64: '',
|
||||
});
|
||||
// 打开弹窗
|
||||
const openDialog = (imgs: any) => {
|
||||
state.cropperImg = imgs;
|
||||
state.isShowDialog = true;
|
||||
nextTick(() => {
|
||||
initCropper();
|
||||
});
|
||||
};
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
state.isShowDialog = false;
|
||||
};
|
||||
// 取消
|
||||
const onCancel = () => {
|
||||
closeDialog();
|
||||
};
|
||||
// 新增
|
||||
const onSubmit = () => {};
|
||||
// 初始化cropperjs图片裁剪
|
||||
const initCropper = () => {
|
||||
const letImg: any = document.querySelector('.cropper-warp-left-img');
|
||||
const cropper = new Cropper(letImg, {
|
||||
viewMode: 1,
|
||||
dragMode: 'none',
|
||||
initialAspectRatio: 1,
|
||||
aspectRatio: 1,
|
||||
preview: '.before',
|
||||
background: false,
|
||||
autoCropArea: 0.6,
|
||||
zoomOnWheel: false,
|
||||
crop: () => {
|
||||
state.cropperImgBase64 = cropper.getCroppedCanvas().toDataURL('image/jpeg');
|
||||
},
|
||||
});
|
||||
};
|
||||
return {
|
||||
openDialog,
|
||||
closeDialog,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
initCropper,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cropper-warp {
|
||||
display: flex;
|
||||
.cropper-warp-left {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 350px;
|
||||
flex: 1;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
cursor: move;
|
||||
border-radius: 3px;
|
||||
.cropper-warp-left-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.cropper-warp-right {
|
||||
width: 150px;
|
||||
height: 350px;
|
||||
.cropper-warp-right-title {
|
||||
text-align: center;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.cropper-warp-right-item {
|
||||
margin: 15px 0;
|
||||
.cropper-warp-right-value {
|
||||
display: flex;
|
||||
.cropper-warp-right-value-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
.cropper-size {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
.cropper-warp-right-label {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
7
frontend/src/components/df/design.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<FormDesign ref="makingForm" upload preview generate-code generate-json clearable> </FormDesign>
|
||||
<!-- <dev></dev> -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
// import { FormDesign } from 'mayfly-lc';
|
||||
</script>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="dynamic-form-edit w100">
|
||||
<el-table :data="formItems" stripe class="w100">
|
||||
<div class="dynamic-form-edit !w-full">
|
||||
<el-table :data="formItems" stripe class="!w-full">
|
||||
<el-table-column prop="name" label="model" min-width="100px">
|
||||
<template #header>
|
||||
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addItem()"> </el-button>
|
||||
<span class="ml10">model field</span>
|
||||
<span class="ml-2">model field</span>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row['model']" :placeholder="$t('components.df.fieldModelPlaceholder')" clearable> </el-input>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
enums: {
|
||||
type: Object, // 需要为EnumValue类型
|
||||
type: Object || Array, // 需要为EnumValue类型
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,8 +12,9 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [Object, String, Number],
|
||||
type: [Object, String, Number, null, Boolean],
|
||||
required: true,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +41,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const convert = (value: any) => {
|
||||
const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
|
||||
const enumValue = EnumValue.getEnumByValue(props.enums, value);
|
||||
if (!enumValue) {
|
||||
state.enumLabel = '-';
|
||||
state.type = 'danger';
|
||||
@@ -50,8 +51,8 @@ const convert = (value: any) => {
|
||||
|
||||
state.enumLabel = enumValue?.label || '';
|
||||
if (enumValue.tag) {
|
||||
state.color = enumValue.tag.color;
|
||||
state.type = enumValue.tag.type;
|
||||
state.color = enumValue.tag.color || '';
|
||||
state.type = enumValue.tag.type || defaultType;
|
||||
} else {
|
||||
state.type = defaultType;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
<template>
|
||||
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
|
||||
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
|
||||
</el-tooltip>
|
||||
<el-button v-if="loading" :loading="loading" name="loading" link type="primary" />
|
||||
|
||||
{{ fileDetail?.filename }}
|
||||
<template v-else>
|
||||
<el-tooltip :content="fileSize" placement="left">
|
||||
<el-link
|
||||
v-if="props.canDownload"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon="Download"
|
||||
type="primary"
|
||||
:href="getFileUrl(props.fileKey)"
|
||||
></el-link>
|
||||
</el-tooltip>
|
||||
|
||||
{{ fileDetail?.filename }}
|
||||
<!-- 文件大小显示 -->
|
||||
<span v-if="props.showFileSize && fileDetail?.size" class="file-size">({{ fileSize }})</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, Ref, ref, watch } from 'vue';
|
||||
import openApi from '@/common/openApi';
|
||||
import { getFileUrl } from '@/common/request';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
|
||||
const props = defineProps({
|
||||
fileKey: {
|
||||
type: String,
|
||||
@@ -23,8 +37,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showFileSize: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
setFileInfo();
|
||||
});
|
||||
@@ -38,23 +58,38 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const fileSize = computed(() => {
|
||||
return fileDetail.value?.size ? formatByteSize(fileDetail.value.size) : '';
|
||||
});
|
||||
|
||||
const fileDetail: any = ref({});
|
||||
|
||||
const setFileInfo = async () => {
|
||||
if (!props.fileKey) {
|
||||
return;
|
||||
}
|
||||
if (props.files && props.files.length > 0) {
|
||||
const file: any = props.files.find((file: any) => {
|
||||
return file.fileKey === props.fileKey;
|
||||
});
|
||||
fileDetail.value = file;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!props.fileKey) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
if (props.files && props.files.length > 0) {
|
||||
const file: any = props.files.find((file: any) => {
|
||||
return file.fileKey === props.fileKey;
|
||||
});
|
||||
fileDetail.value = file;
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await openApi.getFileDetail([props.fileKey]);
|
||||
fileDetail.value = files?.[0];
|
||||
const files = await openApi.getFileDetail([props.fileKey]);
|
||||
fileDetail.value = files?.[0];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss" scoped>
|
||||
.file-size {
|
||||
margin-left: 1px;
|
||||
color: #909399;
|
||||
font-size: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<el-form-item v-bind="$attrs">
|
||||
<template #label>
|
||||
{{ props.label }}
|
||||
<div class="flex items-center">
|
||||
{{ props.label }}
|
||||
|
||||
<el-tooltip :placement="props.placement">
|
||||
<template #content>
|
||||
<span v-html="props.tooltip"></span>
|
||||
</template>
|
||||
<SvgIcon name="QuestionFilled" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :placement="props.placement">
|
||||
<template #content>
|
||||
<span v-html="props.tooltip"></span>
|
||||
</template>
|
||||
<SvgIcon name="QuestionFilled" class="ml-1" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||
@@ -24,11 +26,11 @@ import { useSlots } from 'vue';
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
require: true,
|
||||
required: true,
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
require: true,
|
||||
required: true,
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="icon-selector w100 h100">
|
||||
<div class="icon-selector !w-full !h-full">
|
||||
<el-input
|
||||
v-model="state.fontIconSearch"
|
||||
:placeholder="state.fontIconPlaceholder"
|
||||
@@ -12,7 +12,7 @@
|
||||
@blur="onIconBlur"
|
||||
>
|
||||
<template #prepend>
|
||||
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14" />
|
||||
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="!text-[14px]" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-popover
|
||||
@@ -25,7 +25,7 @@
|
||||
virtual-triggering
|
||||
>
|
||||
<template #default>
|
||||
<div class="ml5 mt5">{{ $t(title) }}</div>
|
||||
<div class="ml-1 mt-1">{{ $t(title) }}</div>
|
||||
<div class="icon-selector-warp">
|
||||
<el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
|
||||
<el-tab-pane lazy label="ele" name="ele">
|
||||
|
||||
129
frontend/src/components/message/message.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { ElLink, ElText } from 'element-plus';
|
||||
import { defineAsyncComponent, defineComponent, h } from 'vue';
|
||||
|
||||
type Size = 'large' | 'default' | 'small';
|
||||
|
||||
interface ComponentConfig {
|
||||
component: any;
|
||||
getDefaultProps?: (size: Size) => Record<string, any>;
|
||||
}
|
||||
|
||||
const linkConf = {
|
||||
component: ElLink,
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
type: 'primary',
|
||||
verticalAlign: 'baseline',
|
||||
style: {
|
||||
fontSize: size === 'small' ? '12px' : size === 'large' ? '16px' : '14px',
|
||||
verticalAlign: 'baseline',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const components = {
|
||||
'el-link': linkConf,
|
||||
a: linkConf,
|
||||
|
||||
'error-text': {
|
||||
component: ElText,
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
type: 'danger',
|
||||
size,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
'machine-info': {
|
||||
component: defineAsyncComponent(() => import('@/views/ops/machine/component/MachineDetail.vue')),
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
size,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
'db-info': {
|
||||
component: defineAsyncComponent(() => import('@/views/ops/db/component/DbDetail.vue')),
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
size,
|
||||
};
|
||||
},
|
||||
},
|
||||
} as Record<string, ComponentConfig>;
|
||||
|
||||
export const MessageRenderer = defineComponent({
|
||||
props: {
|
||||
content: String,
|
||||
size: {
|
||||
type: String as () => Size,
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const parseContent = (content: string) => {
|
||||
if (!content) {
|
||||
return [h('span', '')];
|
||||
}
|
||||
|
||||
// 创建一个包装容器来处理HTML内容
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = content;
|
||||
|
||||
const parseNode = (node: Node): any => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as HTMLElement;
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
let attrs: Record<string, any> = {};
|
||||
|
||||
// 提取属性
|
||||
for (let i = 0; i < element.attributes.length; i++) {
|
||||
const attr = element.attributes[i];
|
||||
attrs[attr.name] = attr.value;
|
||||
}
|
||||
|
||||
const componentConf = components[tagName];
|
||||
if (!componentConf) {
|
||||
return h(tagName, attrs, Array.from(element.childNodes).map(parseNode));
|
||||
}
|
||||
|
||||
// 存在默认组件配置,则合并
|
||||
if (componentConf.getDefaultProps) {
|
||||
const defaultProps = componentConf.getDefaultProps(props.size);
|
||||
attrs = {
|
||||
...defaultProps,
|
||||
...attrs,
|
||||
};
|
||||
}
|
||||
|
||||
return h(componentConf.component, attrs, {
|
||||
default: () => Array.from(element.childNodes).map(parseNode),
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
return Array.from(container.childNodes).map(parseNode);
|
||||
};
|
||||
|
||||
return () => {
|
||||
// 根据 size 属性确定根元素的 class
|
||||
const rootClass = props.size === 'small' ? 'text-sm' : props.size === 'large' ? 'text-lg' : 'text-base';
|
||||
try {
|
||||
const elements = parseContent(props.content || '');
|
||||
return h('div', { class: rootClass }, elements);
|
||||
} catch (e) {
|
||||
console.error('消息渲染失败:', e);
|
||||
return h('div', { class: rootClass }, props.content || '');
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5); height: 100%">
|
||||
<div class="monaco-editor-custom relative h-full">
|
||||
<div class="monaco-editor-content" ref="monacoTextareaRef" :style="{ height: height }"></div>
|
||||
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable>
|
||||
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
|
||||
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable size="small">
|
||||
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, toRefs, reactive, onMounted, onBeforeUnmount, useTemplateRef, Ref } from 'vue';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import * as monaco from 'monaco-editor';
|
||||
// 相关语言
|
||||
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js';
|
||||
@@ -31,19 +31,12 @@ import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
|
||||
// 提示
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
|
||||
|
||||
import { editor, languages } from 'monaco-editor';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
// 主题仓库 https://github.com/brijeshb42/monaco-themes
|
||||
// 主题例子 https://editor.bitwiser.in/
|
||||
// import Monokai from 'monaco-themes/themes/Monokai.json'
|
||||
// import Active4D from 'monaco-themes/themes/Active4D.json'
|
||||
// import ahe from 'monaco-themes/themes/All Hallows Eve.json'
|
||||
// import bop from 'monaco-themes/themes/Birds of Paradise.json'
|
||||
// import krTheme from 'monaco-themes/themes/krTheme.json'
|
||||
// import Dracula from 'monaco-themes/themes/Dracula.json'
|
||||
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
|
||||
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
||||
import SolarizedLight from './themes/Solarized-light.json';
|
||||
import SolarizedDark from './themes/Solarized-dark.json';
|
||||
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
||||
|
||||
import { ElOption, ElSelect } from 'element-plus';
|
||||
@@ -97,7 +90,11 @@ const languageArr = [
|
||||
},
|
||||
{
|
||||
value: 'html',
|
||||
label: 'XML/HTML',
|
||||
label: 'Html',
|
||||
},
|
||||
{
|
||||
value: 'xml',
|
||||
label: 'Xml',
|
||||
},
|
||||
{
|
||||
value: 'python',
|
||||
@@ -134,6 +131,7 @@ const defaultOptions = {
|
||||
theme: 'SolarizedLight',
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
foldingStrategy: 'indentation', //代码可分小段折叠
|
||||
folding: true,
|
||||
roundedSelection: false, // 禁用选择文本背景的圆角
|
||||
matchBrackets: 'near',
|
||||
linkedEditing: true,
|
||||
@@ -149,7 +147,14 @@ const defaultOptions = {
|
||||
minimap: {
|
||||
enabled: false, // 不要小地图
|
||||
},
|
||||
};
|
||||
renderLineHighlight: 'all',
|
||||
selectOnLineNumbers: false,
|
||||
readOnly: false,
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
lineNumbersMinChars: 3,
|
||||
fixedOverflowWidgets: true, // 使弹出层不被容器限制
|
||||
} as editor.IStandaloneEditorConstructionOptions;
|
||||
|
||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||
|
||||
@@ -161,6 +166,9 @@ self.MonacoEnvironment = {
|
||||
if (label === 'json') {
|
||||
return new JsonWorker();
|
||||
}
|
||||
if (label === 'html') {
|
||||
return new HtmlWorker();
|
||||
}
|
||||
return new EditorWorker();
|
||||
},
|
||||
};
|
||||
@@ -219,14 +227,18 @@ const initMonacoEditorIns = () => {
|
||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||
// 初始化一些主题
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
monaco.editor.defineTheme('SolarizedDark', SolarizedDark);
|
||||
defaultOptions.language = state.languageMode;
|
||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, Object.assign(defaultOptions, props.options as any));
|
||||
let options = Object.assign(defaultOptions, props.options as any);
|
||||
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, options);
|
||||
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
modelValue.value = monacoEditorIns.getModel()?.getValue();
|
||||
});
|
||||
if (!options.readOnly) {
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
modelValue.value = monacoEditorIns.getModel()?.getValue();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const changeLanguage = (value: any) => {
|
||||
@@ -309,13 +321,16 @@ defineExpose({ getEditor, format, focus });
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.monaco-editor {
|
||||
.monaco-editor-custom {
|
||||
.code-mode-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 130px;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,16 +9,18 @@ export type MonacoEditorDialogProps = {
|
||||
language: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
options?: any; // 可选项,如字体大小等
|
||||
options?: any; // 可选项,如字体大小等
|
||||
canChangeLang?: boolean; // 是否可以切换语言
|
||||
showConfirmButton?: boolean;
|
||||
confirmFn?: Function; // 点击确认的回调函数,入参editor value
|
||||
confirmFn?: Function; // 点击确认的回调函数,入参editor value
|
||||
closeFn?: Function; // 点击取消 或 关闭弹窗的回调函数
|
||||
completionItemProvider?: monaco.languages.CompletionItemProvider; // 自定义补全项
|
||||
useDrawer?: boolean; // 是否使用drawer而不是dialog,默认false
|
||||
drawerSize?: string | number; // drawer尺寸,默认'50%'
|
||||
};
|
||||
|
||||
const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
|
||||
const boxId = 'monaco-editor-dialog-id';
|
||||
const boxId = props.useDrawer ? 'monaco-editor-drawer-id' : 'monaco-editor-dialog-id';
|
||||
let boxInstance: VNode;
|
||||
|
||||
const container = document.getElementById(boxId);
|
||||
@@ -35,6 +37,9 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
|
||||
if (props.content === undefined) {
|
||||
props.content = '';
|
||||
}
|
||||
if (props.useDrawer === undefined) {
|
||||
props.useDrawer = false;
|
||||
}
|
||||
|
||||
// 创建 虚拟dom
|
||||
boxInstance = h(MonacoEditorDialog, {
|
||||
@@ -53,6 +58,7 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
|
||||
}
|
||||
// 移除 container DOM 元素
|
||||
document.body.removeChild(container);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
props.closeFn && props.closeFn();
|
||||
},
|
||||
onConfirm: () => {
|
||||
@@ -72,6 +78,7 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
|
||||
// 压缩json字符串
|
||||
value = JSON.stringify(val);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
props.confirmFn && props.confirmFn(value);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :title="props.title" v-model="dialogVisible" :width="props.width" @close="close">
|
||||
<!-- Dialog 模式 -->
|
||||
<el-dialog :title="props.title" v-model="dialogVisible" :width="props.width" @close="close" v-if="!props.useDrawer">
|
||||
<monaco-editor
|
||||
ref="editorRef"
|
||||
:height="props.height"
|
||||
@@ -17,12 +18,40 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Drawer 模式 -->
|
||||
<el-drawer
|
||||
:title="props.title"
|
||||
v-model="dialogVisible"
|
||||
:size="props.drawerSize || '50%'"
|
||||
@close="close"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true"
|
||||
class="monaco-editor-drawer"
|
||||
v-else
|
||||
>
|
||||
<monaco-editor
|
||||
ref="editorRef"
|
||||
:height="props.height || 'calc(100vh - 120px)'"
|
||||
class="editor"
|
||||
:language="props.language"
|
||||
v-model="modelValue"
|
||||
:options="props.options"
|
||||
:can-change-mode="props.canChangeLang"
|
||||
/>
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="dialogVisible = false">{{ i18n.global.t('common.cancel') }}</el-button>
|
||||
<el-button v-if="props.showConfirmButton" @click="confirm" type="primary">{{ i18n.global.t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { ElDialog, ElButton, ElMessage } from 'element-plus';
|
||||
import { ElDialog, ElDrawer, ElButton, ElMessage } from 'element-plus';
|
||||
// import base style
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { MonacoEditorDialogProps } from './MonacoEditorBox';
|
||||
@@ -45,6 +74,18 @@ const dialogVisible = defineModel<boolean>('visible', {
|
||||
|
||||
const emit = defineEmits(['close', 'confirm']);
|
||||
|
||||
const formatXML = function (xml: string, tab?: string) {
|
||||
let formatted = '',
|
||||
indent = '';
|
||||
tab = tab || ' ';
|
||||
xml.split(/>\s*</).forEach(function (node) {
|
||||
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
|
||||
formatted += indent + '<' + node + '>\r\n';
|
||||
if (node.match(/^<?\w[^>]*[^\/]$/)) indent += tab;
|
||||
});
|
||||
return formatted.substring(1, formatted.length - 3);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.language,
|
||||
() => {
|
||||
@@ -103,26 +144,11 @@ const close = () => {
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const formatXML = function (xml: string, tab?: string) {
|
||||
let formatted = '',
|
||||
indent = '';
|
||||
tab = tab || ' ';
|
||||
xml.split(/>\s*</).forEach(function (node) {
|
||||
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
|
||||
formatted += indent + '<' + node + '>\r\n';
|
||||
if (node.match(/^<?\w[^>]*[^\/]$/)) indent += tab;
|
||||
});
|
||||
return formatted.substring(1, formatted.length - 3);
|
||||
};
|
||||
|
||||
function compressHTML(html: string) {
|
||||
return (
|
||||
html
|
||||
.replace(/[\r\n\t]+/g, ' ') // 移除换行符和制表符
|
||||
// .replace(/<!--[\s\S]*?-->/g, '') // 移除注释
|
||||
.replace(/\s{2,}/g, ' ') // 合并多个空格为一个空格
|
||||
.replace(/>\s+</g, '><')
|
||||
); // 移除标签之间的空格
|
||||
return html
|
||||
.replace(/[\r\n\t]+/g, ' ') // 移除换行符和制表符
|
||||
.replace(/\s{2,}/g, ' ') // 合并多个空格为一个空格
|
||||
.replace(/>\s+</g, '><'); // 移除标签之间的空格
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@@ -130,4 +156,23 @@ function compressHTML(html: string) {
|
||||
font-size: 9pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
:deep(.monaco-editor-drawer) {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.el-drawer__body {
|
||||
padding: 0;
|
||||
}
|
||||
.el-drawer__footer {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
73
frontend/src/components/monaco/RealLogViewer.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<monaco-editor
|
||||
ref="editorRef"
|
||||
:height="props.height"
|
||||
class="editor"
|
||||
language="text"
|
||||
v-model="modelValue"
|
||||
:options="{
|
||||
readOnly: true,
|
||||
}"
|
||||
:can-change-mode="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef, watch } from 'vue';
|
||||
import { useWebSocket } from '@vueuse/core';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
wsUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const websocketUrl = ref(props.wsUrl);
|
||||
|
||||
const { data } = useWebSocket(websocketUrl);
|
||||
|
||||
const editorRef = useTemplateRef<InstanceType<typeof MonacoEditor>>('editorRef');
|
||||
|
||||
const modelValue = defineModel<string>('modelValue', {
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
watch(data, (value) => {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
||||
setTimeout(() => {
|
||||
revealLastLine();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
const reload = (wsUrl: string) => {
|
||||
modelValue.value = '';
|
||||
websocketUrl.value = wsUrl;
|
||||
revealLastLine();
|
||||
};
|
||||
|
||||
const revealLastLine = () => {
|
||||
const editor = editorRef.value?.getEditor();
|
||||
const lineCount = editor?.getModel()?.getLineCount();
|
||||
editor?.revealLine(lineCount || 0);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.editor {
|
||||
font-size: 9pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
1086
frontend/src/components/monaco/themes/Solarized-dark.json
Normal file
1077
frontend/src/components/monaco/themes/Solarized-light.json
Normal file
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="el-zoom-in-top">
|
||||
<div class="h-full flex flex-col flex-1 overflow-hidden">
|
||||
<transition name="page-table-search-form">
|
||||
<!-- 查询表单 -->
|
||||
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search"
|
||||
:reset="reset" :search-col="searchCol">
|
||||
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
|
||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||
<template v-for="(_, key) in useSlots()" v-slot:[key]>
|
||||
<slot :name="key"></slot>
|
||||
@@ -11,83 +10,104 @@
|
||||
</SearchForm>
|
||||
</transition>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-main">
|
||||
<!-- 表格头部 操作按钮 -->
|
||||
<div class="table-header">
|
||||
<div class="header-button-lf">
|
||||
<slot name="tableHeader" />
|
||||
</div>
|
||||
|
||||
<div v-if="toolButton" class="header-button-ri">
|
||||
<slot name="toolButton">
|
||||
<div class="tool-button">
|
||||
<!-- 简易单个搜索项 -->
|
||||
<div v-if="nowSearchItem" class="simple-search-form">
|
||||
<el-dropdown v-if="searchItems?.length > 1">
|
||||
<SvgIcon :size="16" name="CaretBottom" class="mr4 mt6 simple-search-form-btn" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="searchItem in searchItems"
|
||||
:key="searchItem.prop" @click="changeSimpleFormItem(searchItem)">
|
||||
{{ $t(searchItem.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<div class="simple-search-form-label mt5">
|
||||
<el-text truncated tag="b">{{ `${$t(nowSearchItem?.label)} : ` }}</el-text>
|
||||
</div>
|
||||
|
||||
<el-form-item style="width: 200px" :key="nowSearchItem.prop">
|
||||
<SearchFormItem @keyup.enter.native="searchFormItemKeyUpEnter"
|
||||
v-if="!nowSearchItem.slot" :item="nowSearchItem"
|
||||
v-model="queryForm[nowSearchItem.prop]" />
|
||||
|
||||
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else
|
||||
:name="nowSearchItem.slot">
|
||||
</slot>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search"
|
||||
circle @click="search" />
|
||||
|
||||
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
|
||||
|
||||
<el-button v-if="showToolButton('search') && searchItems?.length > 1"
|
||||
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'" circle
|
||||
@click="isShowSearch = !isShowSearch" />
|
||||
|
||||
<el-popover placement="bottom" title="表格配置"
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px" width="auto"
|
||||
trigger="click">
|
||||
<div v-for="(item, index) in tableColumns" :key="index">
|
||||
<el-checkbox v-model="item.show" :label="item.label" :true-value="true"
|
||||
:false-value="false" />
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button icon="Operation" circle :size="props.size"></el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<el-card class="h-full" body-class="h-full flex flex-col">
|
||||
<!-- 表格头部 操作按钮 -->
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<slot name="tableHeader" />
|
||||
</div>
|
||||
|
||||
<el-table ref="tableRef" v-bind="$attrs" :max-height="tableMaxHeight"
|
||||
@selection-change="handleSelectionChange" :data="tableData" highlight-current-row
|
||||
v-loading="loading" :size="props.size as any" :border="border">
|
||||
<slot v-if="toolButton" name="toolButton">
|
||||
<div class="flex">
|
||||
<!-- 简易单个搜索项 -->
|
||||
<div v-if="nowSearchItem" class="flex">
|
||||
<el-dropdown v-if="props.searchItems?.length > 1">
|
||||
<SvgIcon :size="16" name="CaretBottom" class="!mr-1 !mt-1.5 simple-search-form-btn" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="searchItem in searchItems" :key="searchItem.prop" @click="changeSimpleFormItem(searchItem)">
|
||||
{{ $t(searchItem.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<div class="text-right mr-1.5 mt-1">
|
||||
<el-text truncated tag="b">{{ `${$t(nowSearchItem?.label)} : ` }}</el-text>
|
||||
</div>
|
||||
|
||||
<el-form-item class="w-[200px]" :key="nowSearchItem.prop">
|
||||
<SearchFormItem
|
||||
@keyup.enter.native="searchFormItemKeyUpEnter"
|
||||
v-if="!nowSearchItem.slot"
|
||||
:item="nowSearchItem"
|
||||
v-model="queryForm[nowSearchItem.prop]"
|
||||
/>
|
||||
|
||||
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else :name="nowSearchItem.slot"> </slot>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="ml-2">
|
||||
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search" circle @click="search" />
|
||||
|
||||
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
|
||||
|
||||
<el-button
|
||||
v-if="showToolButton('search') && props.searchItems?.length > 1"
|
||||
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
|
||||
circle
|
||||
@click="isShowSearch = !isShowSearch"
|
||||
/>
|
||||
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
title="表格配置"
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||
width="auto"
|
||||
trigger="click"
|
||||
>
|
||||
<div v-for="(item, index) in tableColumns" :key="index">
|
||||
<el-checkbox v-model="item.show" :label="$t(item.label)" :true-value="1" :false-value="0" />
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button icon="Operation" circle :size="props.size"></el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto">
|
||||
<el-table
|
||||
v-show="showTable"
|
||||
ref="tableRef"
|
||||
v-bind="$attrs"
|
||||
height="100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
:data="tableData"
|
||||
highlight-current-row
|
||||
v-loading="loading"
|
||||
:size="props.size as any"
|
||||
:border="border"
|
||||
>
|
||||
<el-table-column v-if="props.showSelection" :selectable="selectable" type="selection" width="40" />
|
||||
|
||||
<template v-for="(item, index) in tableColumns">
|
||||
<el-table-column :key="index" v-if="item.show" :prop="item.prop" :label="$t(item.label)"
|
||||
:fixed="item.fixed" :align="item.align" :show-overflow-tooltip="item.showOverflowTooltip"
|
||||
:min-width="item.minWidth" :sortable="item.sortable || false" :type="item.type"
|
||||
:width="item.width">
|
||||
<el-table-column
|
||||
:key="index"
|
||||
v-if="item.show"
|
||||
:prop="item.prop"
|
||||
:label="$t(item.label)"
|
||||
:fixed="item.fixed"
|
||||
:align="item.align"
|
||||
:show-overflow-tooltip="item.showOverflowTooltip"
|
||||
:min-width="item.minWidth"
|
||||
:sortable="item.sortable || false"
|
||||
:type="item.type"
|
||||
:width="item.width"
|
||||
>
|
||||
<!-- 插槽:预留功能 -->
|
||||
<template #default="scope" v-if="item.slot">
|
||||
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
|
||||
@@ -95,21 +115,29 @@
|
||||
|
||||
<!-- 枚举类型使用tab展示 -->
|
||||
<template #default="scope" v-else-if="item.type == 'tag'">
|
||||
<enum-tag :size="props.size" :enums="item.typeParam"
|
||||
:value="item.getValueByData(scope.row)"></enum-tag>
|
||||
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else>
|
||||
<!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
|
||||
<el-popover v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
|
||||
effect="light" trigger="click" placement="top" width="600px">
|
||||
<el-popover
|
||||
v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
|
||||
effect="light"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
width="600px"
|
||||
>
|
||||
<template #default>
|
||||
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal"
|
||||
type="textarea" />
|
||||
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal" type="textarea" />
|
||||
</template>
|
||||
<template #reference>
|
||||
<el-link @click="formatText(item.getValueByData(scope.row))" :underline="false"
|
||||
type="success" icon="MagicStick" class="mr5"></el-link>
|
||||
<el-link
|
||||
@click="formatText(item.getValueByData(scope.row))"
|
||||
underline="never"
|
||||
type="success"
|
||||
icon="MagicStick"
|
||||
class="mr-1"
|
||||
></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
@@ -120,38 +148,42 @@
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
|
||||
<el-pagination :small="props.size == 'small'" @current-change="pageNumChange"
|
||||
@size-change="pageSizeChange" style="text-align: right" layout="prev, pager, next, total, sizes"
|
||||
:total="total" v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize"
|
||||
:page-sizes="pageSizes" />
|
||||
<el-row v-if="props.pageable" class="mt-4" type="flex" justify="end">
|
||||
<el-pagination
|
||||
:small="props.size == 'small'"
|
||||
@current-change="pageNumChange"
|
||||
@size-change="pageSizeChange"
|
||||
layout="prev, pager, next, total, sizes"
|
||||
:total="total"
|
||||
v-model:current-page="queryForm.pageNum"
|
||||
v-model:page-size="queryForm.pageSize"
|
||||
:page-sizes="pageSizes"
|
||||
/>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue } from 'vue';
|
||||
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue, h } from 'vue';
|
||||
import { TableColumn } from './index';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import Api from '@/common/Api';
|
||||
import SearchForm from '@/components/SearchForm/index.vue';
|
||||
import { SearchItem } from '../SearchForm/index';
|
||||
import SearchFormItem from '../SearchForm/components/SearchFormItem.vue';
|
||||
import SearchForm from '@/components/pagetable/SearchForm/index.vue';
|
||||
import { SearchItem } from './SearchForm/index';
|
||||
import SearchFormItem from './SearchForm/components/SearchFormItem.vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { usePageTable } from '@/hooks/usePageTable';
|
||||
import { ElTable } from 'element-plus';
|
||||
|
||||
import { ElInput, ElTable } from 'element-plus';
|
||||
|
||||
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
|
||||
|
||||
export interface PageTableProps {
|
||||
size?: string;
|
||||
pageApi?: Api; // 请求表格数据的 api
|
||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||
columns: TableColumn[] | any[]; // 列配置项 ==> 必传
|
||||
showSelection?: boolean;
|
||||
selectable?: (row: any) => boolean; // 是否可选
|
||||
pageable?: boolean;
|
||||
@@ -208,6 +240,10 @@ const showToolButton = (key: 'setting' | 'search') => {
|
||||
|
||||
const nowSearchItem: Ref<SearchItem> = ref(null) as any;
|
||||
|
||||
// 是否已经计算列宽度
|
||||
const isCalculatedWidth: Ref<boolean> = ref(false);
|
||||
const showTable: Ref<boolean> = ref(false);
|
||||
|
||||
/**
|
||||
* 改变当前的搜索项
|
||||
* @param searchItem 当前点击的搜索项
|
||||
@@ -239,24 +275,35 @@ const state = reactive({
|
||||
pageSizes: [] as any, // 可选每页显示的数据量
|
||||
// 输入框宽度
|
||||
formatVal: '', // 格式化后的值
|
||||
tableMaxHeight: '500px',
|
||||
});
|
||||
|
||||
const { pageSizes, formatVal, tableMaxHeight } = toRefs(state);
|
||||
const { pageSizes, formatVal } = toRefs(state);
|
||||
|
||||
watch(tableData, (newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(tableData.value);
|
||||
}
|
||||
});
|
||||
calculateTableColumnMinWidth();
|
||||
// 需要计算完才能显示表格,否则会有表格闪烁的问题
|
||||
if (!showTable.value) {
|
||||
showTable.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
watch(isShowSearch, () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
/**
|
||||
* 计算表格列宽
|
||||
*/
|
||||
const calculateTableColumnMinWidth = () => {
|
||||
if (isCalculatedWidth.value || !tableData.value || tableData.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算表格列宽
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(tableData.value);
|
||||
}
|
||||
});
|
||||
|
||||
isCalculatedWidth.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
@@ -266,9 +313,6 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
calcuTableHeight();
|
||||
useEventListener(window, 'resize', calcuTableHeight);
|
||||
|
||||
if (props.searchItems.length > 0) {
|
||||
nowSearchItem.value = props.searchItems[0];
|
||||
}
|
||||
@@ -292,11 +336,6 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const calcuTableHeight = () => {
|
||||
const headerHeight = isShowSearch.value ? 330 : 250;
|
||||
state.tableMaxHeight = window.innerHeight - headerHeight + 'px';
|
||||
};
|
||||
|
||||
const searchFormItemKeyUpEnter = (event: any) => {
|
||||
event.preventDefault();
|
||||
search();
|
||||
@@ -327,113 +366,21 @@ defineExpose({
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.table-box,
|
||||
.table-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.page-table-search-form-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
// 表格 header 样式
|
||||
.table-header {
|
||||
width: 100%;
|
||||
.page-table-search-form-leave-active {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
.header-button-lf {
|
||||
float: left;
|
||||
}
|
||||
.page-table-search-form-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
|
||||
.header-button-ri {
|
||||
float: right;
|
||||
|
||||
.tool-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.simple-search-form {
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
::v-deep(.el-form-item__content > *) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.simple-search-form-label {
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.simple-search-form-btn:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// el-table 表格样式
|
||||
.el-table {
|
||||
flex: 1;
|
||||
|
||||
// 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// .el-table__header th {
|
||||
// height: 45px;
|
||||
// font-size: 15px;
|
||||
// font-weight: bold;
|
||||
// color: var(--el-text-color-primary);
|
||||
// background: var(--el-fill-color-light);
|
||||
// }
|
||||
|
||||
// .el-table__row {
|
||||
// height: 45px;
|
||||
// font-size: 14px;
|
||||
|
||||
// .move {
|
||||
// cursor: move;
|
||||
|
||||
// .el-icon {
|
||||
// cursor: move;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 设置 el-table 中 header 文字不换行,并省略
|
||||
.el-table__header .el-table__cell>.cell {
|
||||
// white-space: nowrap;
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
// 解决表格数据为空时样式不居中问题(仅在element-plus中)
|
||||
// .el-table__empty-block {
|
||||
// position: absolute;
|
||||
// top: 50%;
|
||||
// left: 50%;
|
||||
// transform: translate(-50%, -50%);
|
||||
|
||||
// .table-empty {
|
||||
// line-height: 30px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// table 中 image 图片样式
|
||||
.table-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.el-form-item__label) {
|
||||
font-weight: bold;
|
||||
}
|
||||
.page-table-search-form-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
||||