Compare commits
444 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9366a76b84 | ||
|
|
e03ceecd9a | ||
|
|
99a746085b | ||
|
|
74ae031853 | ||
|
|
af14be9801 | ||
|
|
df7413a9ea | ||
|
|
e967e02095 | ||
|
|
c1d09f447d | ||
|
|
5e5afd49dc | ||
|
|
2118acf244 | ||
|
|
44a1bd626e | ||
|
|
ea3c70a8a8 | ||
|
|
6343173cf8 | ||
|
|
6837a9c867 | ||
|
|
a726927a28 | ||
|
|
e135e4ce64 | ||
|
|
43edef412c | ||
|
|
2deb3109c2 | ||
|
|
a80221a950 | ||
|
|
10630847df | ||
|
|
f43851698e | ||
|
|
73884bb693 | ||
|
|
1b5bb1de8b | ||
|
|
4814793546 | ||
|
|
d85bbff270 | ||
|
|
bb1522f4dc | ||
|
|
a7632fbf58 | ||
|
|
c4cb4234fd | ||
|
|
89e12678eb | ||
|
|
137ebb8e9e | ||
|
|
05625bd8c1 | ||
|
|
4afeac5fdd | ||
|
|
1d0e91f1af | ||
|
|
cf5111a325 | ||
|
|
78957a8ebd | ||
|
|
4ed892a656 | ||
|
|
3486b07003 | ||
|
|
a5cd7caf19 | ||
|
|
f2c7ef78c0 | ||
|
|
653953ee76 | ||
|
|
a831614d5a | ||
|
|
ebe73e2f19 | ||
|
|
29fd5a25d2 | ||
|
|
44805ce580 | ||
|
|
2a6d620830 | ||
|
|
01d3e1ad28 | ||
|
|
f4162c38db | ||
|
|
1a4626c24d | ||
|
|
d6eb9683d1 | ||
|
|
e2b524dadb | ||
|
|
8998a21626 | ||
|
|
abc015aec0 | ||
|
|
4ef8d27b1e | ||
|
|
40b6e603fc | ||
|
|
21498584b1 | ||
|
|
408bac09a1 | ||
|
|
582d879a77 | ||
|
|
38ff5152e0 | ||
|
|
d1d372e1bf | ||
|
|
5e4793433b | ||
|
|
54ad19f97e | ||
|
|
fc166650b3 | ||
|
|
2acc295259 | ||
|
|
4b3ed1310d | ||
|
|
b2cfd1517c | ||
|
|
b13d27ccd6 | ||
|
|
68e0088016 | ||
|
|
bd1e83989d | ||
|
|
263dfa6be7 | ||
|
|
eb55f93864 | ||
|
|
8589105e44 | ||
|
|
986b187f0a | ||
|
|
008d34c453 | ||
|
|
49d3f988c9 | ||
|
|
76475e807e | ||
|
|
f93231da61 | ||
|
|
bf75483a3c | ||
|
|
b56b0187cf | ||
|
|
7e7f02b502 | ||
|
|
878985f7c5 | ||
|
|
2133d9b737 | ||
|
|
d711a36749 | ||
|
|
9dbf104ef1 | ||
|
|
20eb06fb28 | ||
|
|
9c20bdef39 | ||
|
|
3fdd98a390 | ||
|
|
d4f456c0cf | ||
|
|
f2b6e15cf4 | ||
|
|
6be0ea6aed | ||
|
|
eee08be2cc | ||
|
|
252fc553f2 | ||
|
|
ac2ceed3f9 | ||
|
|
3f828cc5b0 | ||
|
|
fc1b9ef35d | ||
|
|
d0b71a1c40 | ||
|
|
a743a6a05a | ||
|
|
0e6b9713ce | ||
|
|
b9afbc764d | ||
|
|
923e183a67 | ||
|
|
7e9a381641 | ||
|
|
bed95254d0 | ||
|
|
e4d13f3377 | ||
|
|
d530365ef9 | ||
|
|
070d4ea104 | ||
|
|
3fc86f0fae | ||
|
|
3b77ab2727 | ||
|
|
76cb991282 | ||
|
|
9efd20f1b9 | ||
|
|
de5b9e46d3 | ||
|
|
f27d3d200f | ||
|
|
f4a64b96a9 | ||
|
|
9a59749763 | ||
|
|
b017b902f8 | ||
|
|
7c53353c60 | ||
|
|
63f0615445 | ||
|
|
94da6df33e | ||
|
|
cc3981d99c | ||
|
|
8332d9b354 | ||
|
|
493925064c | ||
|
|
c0232c4c75 | ||
|
|
b873855b44 | ||
|
|
9c524292f0 | ||
|
|
bfd346e65a | ||
|
|
bc811cbd49 | ||
|
|
bbec3eca0d | ||
|
|
3857d674ba | ||
|
|
25b0ae4d2f | ||
|
|
b7aa281611 | ||
|
|
3c89a285f5 | ||
|
|
d3d26c85c3 | ||
|
|
a764c4f974 | ||
|
|
af454f7d5d | ||
|
|
eea759e10e | ||
|
|
e158422091 | ||
|
|
5ada63d4a1 | ||
|
|
5ef35001cc | ||
|
|
0be50f0995 | ||
|
|
61e1b0ca70 | ||
|
|
85910bf440 | ||
|
|
7a7a7020b4 | ||
|
|
ae3d2659aa | ||
|
|
76fd6675b5 | ||
|
|
664118a709 | ||
|
|
e344722794 | ||
|
|
1a7d425f60 | ||
|
|
a0582192bf | ||
|
|
4ac9f02d6a | ||
|
|
7e0febef8f | ||
|
|
94ed4b77d6 | ||
|
|
2b419bca11 | ||
|
|
54a0f0b3c7 | ||
|
|
86bccc3b3d | ||
|
|
0e601b5033 | ||
|
|
85d745fcee | ||
|
|
550631c03b | ||
|
|
f29a1560aa | ||
|
|
8c4c41cf0b | ||
|
|
f5c90277b1 | ||
|
|
2ae0cd7ab4 | ||
|
|
1f6c14ee2f | ||
|
|
574d27f6da | ||
|
|
7d62841783 | ||
|
|
970d74bd70 | ||
|
|
0c797f8da9 | ||
|
|
68f8603c75 | ||
|
|
06bce33c48 | ||
|
|
f8837f28c3 | ||
|
|
61aed08dde | ||
|
|
a5a813f95f | ||
|
|
18cf2e54c4 | ||
|
|
5c72b1de57 | ||
|
|
6e44e90d67 | ||
|
|
0e699ba20e | ||
|
|
cf24c2671f | ||
|
|
d3b99ec88d | ||
|
|
0b5ab090a4 | ||
|
|
454698286c | ||
|
|
14e0aadbba | ||
|
|
73986a834c | ||
|
|
8faf1831d9 | ||
|
|
d86ef0a9ab | ||
|
|
c2bb0c589e | ||
|
|
9994f20a2c | ||
|
|
b5014c307f | ||
|
|
75bd4ca3df | ||
|
|
d00bd2ed72 | ||
|
|
e444500835 | ||
|
|
6709135a0b | ||
|
|
59a7ff9ac7 | ||
|
|
172c729535 | ||
|
|
ac5198db1c | ||
|
|
5c5c2c2037 | ||
|
|
1db990b554 | ||
|
|
70c887a16a | ||
|
|
2430c4f6aa | ||
|
|
84fd14c129 | ||
|
|
a376a82240 | ||
|
|
e1e03dc09a | ||
|
|
790d644c34 | ||
|
|
9de8dae954 | ||
|
|
57361d8241 | ||
|
|
b347bd7ef5 | ||
|
|
070c8ac0da | ||
|
|
e221c2f42e | ||
|
|
c7bab3a71b | ||
|
|
82c17a51a2 | ||
|
|
e4447e6bc2 | ||
|
|
b9570d9a5f | ||
|
|
01e8a2c14d | ||
|
|
64bd51c3b0 | ||
|
|
54ab34df3f | ||
|
|
206490ba3e | ||
|
|
16612d2c9c | ||
|
|
6b65605360 | ||
|
|
bb37ed3b95 | ||
|
|
d102cc8c08 | ||
|
|
a6df74d63d | ||
|
|
f79760943e | ||
|
|
a40ec21a05 | ||
|
|
43230267b6 | ||
|
|
0ae99cdaf9 | ||
|
|
f234c72514 | ||
|
|
76527d95bd | ||
|
|
27c53385f2 | ||
|
|
a1b25e9766 | ||
|
|
abad0ed481 | ||
|
|
eddda41291 | ||
|
|
d9adf0fd25 | ||
|
|
0ce82b41ba | ||
|
|
37026f3269 | ||
|
|
3155380f16 | ||
|
|
f2b0f294d8 | ||
|
|
12f63ef3dd | ||
|
|
a1303b52eb | ||
|
|
10f6b03fb5 | ||
|
|
45d2449221 | ||
|
|
9e5f146e05 | ||
|
|
2b91bbe185 | ||
|
|
747ea6404d | ||
|
|
ccfc6bd1df | ||
|
|
361eafedae | ||
|
|
a64b894b08 | ||
|
|
0ad805c170 | ||
|
|
ba82b5b516 | ||
|
|
f04b82c933 | ||
|
|
23b137ab9b | ||
|
|
a4d3a4627a | ||
|
|
77ae6e3bab | ||
|
|
e0f1f40ba0 | ||
|
|
d300f604f1 | ||
|
|
2c2c0ff40b | ||
|
|
b4ddbbd38f | ||
|
|
7544288451 | ||
|
|
41443dccc0 | ||
|
|
22e218fc5f | ||
|
|
4da0d1abaa | ||
|
|
6563b53436 | ||
|
|
fac71a4794 | ||
|
|
92dff6fdc3 | ||
|
|
a1eca3d691 | ||
|
|
6681dc1057 | ||
|
|
829a68feaa | ||
|
|
72677e270d | ||
|
|
dd4ac390de | ||
|
|
0bd7d38c23 | ||
|
|
ead3b0d0d8 | ||
|
|
4b973b22a4 | ||
|
|
e4e68d02bc | ||
|
|
ef8822d671 | ||
|
|
8e75e1f6ef | ||
|
|
08c381fa60 | ||
|
|
d7a10d4032 | ||
|
|
c324a030f9 | ||
|
|
b618b8f93b | ||
|
|
4d2e110e1e | ||
|
|
ecd79a2e15 | ||
|
|
f4f297d3f7 | ||
|
|
b5549c0fae | ||
|
|
929bfb3200 | ||
|
|
7d3593a944 | ||
|
|
9e0db2bc99 | ||
|
|
25b0d276b3 | ||
|
|
0cb7a7cf83 | ||
|
|
52f72400ba | ||
|
|
0eaff33168 | ||
|
|
086dbf278b | ||
|
|
57a5e237ae | ||
|
|
eee6cf7b14 | ||
|
|
b9c6ac8d6d | ||
|
|
618d782af3 | ||
|
|
d0ac7de4cb | ||
|
|
baf8053613 | ||
|
|
b973d63331 | ||
|
|
85b64d7e8d | ||
|
|
86ad183c41 | ||
|
|
f7b685cfad | ||
|
|
649116a0b8 | ||
|
|
899a3a8243 | ||
|
|
d51cd4b289 | ||
|
|
537b179e78 | ||
|
|
1e5b1868ab | ||
|
|
245406673c | ||
|
|
51fa197af6 | ||
|
|
649b2bb165 | ||
|
|
3634c902d0 | ||
|
|
756e580469 | ||
|
|
4e1350d1cc | ||
|
|
2e969d46fb | ||
|
|
a5bcbe151d | ||
|
|
c4abba361a | ||
|
|
24b46b1133 | ||
|
|
3ae7e0de75 | ||
|
|
c2ee4f9955 | ||
|
|
2479412334 | ||
|
|
6da8d7fd67 | ||
|
|
0f596a712d | ||
|
|
8f37b71d7f | ||
|
|
5083b2bdfe | ||
|
|
155ae65b4a | ||
|
|
ffacfc3ae8 | ||
|
|
b1ab66ecf9 | ||
|
|
f5bb0cad3e | ||
|
|
a0de5afcb0 | ||
|
|
358d33d60e | ||
|
|
062d28b6e6 | ||
|
|
513f8ea012 | ||
|
|
179b58e557 | ||
|
|
b7450f8869 | ||
|
|
7f9e972828 | ||
|
|
7b51705f4e | ||
|
|
6bd9e5333d | ||
|
|
112d735ac0 | ||
|
|
52553ed53f | ||
|
|
70d84e32d1 | ||
|
|
3269dfa5d6 | ||
|
|
183a6e4905 | ||
|
|
5463ae9d7e | ||
|
|
f25bdb07ce | ||
|
|
aa5c08d564 | ||
|
|
dc9a2985f3 | ||
|
|
f4ac6d8360 | ||
|
|
3266039aaf | ||
|
|
e3f4c298b0 | ||
|
|
fa58f6d2de | ||
|
|
ae5a1fd7de | ||
|
|
c240079df4 | ||
|
|
aca4e6751e | ||
|
|
ce32fc7f2c | ||
|
|
d423572e01 | ||
|
|
d9807b1bf0 | ||
|
|
1bc53b4c80 | ||
|
|
6bc2603a4d | ||
|
|
e2c929aae1 | ||
|
|
0d155d592b | ||
|
|
ae510ff1ff | ||
|
|
5b0654ad2c | ||
|
|
466f97ecbe | ||
|
|
27a14c22d7 | ||
|
|
4709edcd1c | ||
|
|
414de9f2eb | ||
|
|
a53e7e7dab | ||
|
|
7fa6628dc5 | ||
|
|
62c25afea8 | ||
|
|
481b622e3b | ||
|
|
64f8f9a200 | ||
|
|
0eca951465 | ||
|
|
ef4e34c584 | ||
|
|
d91acbc7ee | ||
|
|
b397d1022e | ||
|
|
b42a98aff5 | ||
|
|
adc65439e4 | ||
|
|
445cf3716b | ||
|
|
f4b59b8503 | ||
|
|
4f08975df2 | ||
|
|
570db453d7 | ||
|
|
b93984bf6f | ||
|
|
e483db1b97 | ||
|
|
17d96acceb | ||
|
|
9900b236ef | ||
|
|
4fa52412c1 | ||
|
|
0076869deb | ||
|
|
af55193591 | ||
|
|
1d858118d5 | ||
|
|
8e64ba67fa | ||
|
|
58fb11b78f | ||
|
|
f6e9076a40 | ||
|
|
2fefa43aea | ||
|
|
f43b0467ba | ||
|
|
14da4d77e0 | ||
|
|
cefad86b85 | ||
|
|
738ff86344 | ||
|
|
110abc4ac7 | ||
|
|
5f1aaa40d8 | ||
|
|
0695ad9a85 | ||
|
|
7c086bbec8 | ||
|
|
c75fe7135a | ||
|
|
edf29976dd | ||
|
|
7b5f963ec4 | ||
|
|
f22badb861 | ||
|
|
8c501c90cd | ||
|
|
11eebdfcf0 | ||
|
|
46e331783f | ||
|
|
7e33e64659 | ||
|
|
7e4152ad6d | ||
|
|
d189ee7c22 | ||
|
|
641c2abb24 | ||
|
|
3ab4ac891b | ||
|
|
70b586e45a | ||
|
|
77aa724003 | ||
|
|
db7b50d96a | ||
|
|
00fee24a85 | ||
|
|
fa0cb73ec9 | ||
|
|
6ee815b71d | ||
|
|
311a048af5 | ||
|
|
3da9ecfaa3 | ||
|
|
4c2c6f613e | ||
|
|
ba63736871 | ||
|
|
ed3dbafc35 | ||
|
|
fdeffbd495 | ||
|
|
9870812e6b | ||
|
|
46f35e14c4 | ||
|
|
e89cf96ff4 | ||
|
|
5cd19bf38d | ||
|
|
a6f69f2b62 | ||
|
|
e34b9adada | ||
|
|
9f43f731b5 | ||
|
|
594ca43505 | ||
|
|
2a91cdb67a | ||
|
|
11148e720b | ||
|
|
c5835d6d8c | ||
|
|
4a26bb3ba5 | ||
|
|
4fec38724d | ||
|
|
85349df8a1 | ||
|
|
ffe250f8a9 | ||
|
|
eeb310a1d2 | ||
|
|
a42c606d20 | ||
|
|
4e64d46cd2 | ||
|
|
9886ee6828 | ||
|
|
0e1bde09c3 | ||
|
|
dda600709b | ||
|
|
15f38491b2 | ||
|
|
4b140732f7 | ||
|
|
f2119b2c52 | ||
|
|
f15c45793b |
6
.gitignore
vendored
@@ -17,4 +17,10 @@
|
||||
*/node_modules/
|
||||
**/vendor/
|
||||
.idea
|
||||
.vscode
|
||||
out
|
||||
|
||||
server/docs/docker-compose
|
||||
server/config.yml
|
||||
server/ip2region.xdb
|
||||
mayfly-go.log
|
||||
|
||||
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
ARG BASEIMAGES=m.daocloud.io/docker.io/alpine:3.20.2
|
||||
|
||||
FROM ${BASEIMAGES} AS builder
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG MAYFLY_GO_VERSION
|
||||
ARG MAYFLY_GO_DIR_NAME=mayfly-go-linux-${TARGETARCH}
|
||||
ARG MAYFLY_GO_URL=https://gitee.com/dromara/mayfly-go/releases/download/${MAYFLY_GO_VERSION}/${MAYFLY_GO_DIR_NAME}.zip
|
||||
|
||||
RUN wget -cO mayfly-go.zip ${MAYFLY_GO_URL} && \
|
||||
unzip mayfly-go.zip && \
|
||||
mv ${MAYFLY_GO_DIR_NAME}/* /opt
|
||||
|
||||
|
||||
FROM ${BASEIMAGES}
|
||||
|
||||
ARG TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
COPY --from=builder /opt/mayfly-go /usr/local/bin/mayfly-go
|
||||
|
||||
WORKDIR /mayfly-go
|
||||
|
||||
EXPOSE 18888
|
||||
|
||||
CMD ["mayfly-go"]
|
||||
39
Dockerfile.sourcebuild
Normal file
@@ -0,0 +1,39 @@
|
||||
# 构建前端资源
|
||||
FROM m.daocloud.io/docker.io/node:18-bookworm-slim as fe-builder
|
||||
|
||||
WORKDIR /mayfly
|
||||
|
||||
COPY frontend .
|
||||
|
||||
RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||
yarn install && \
|
||||
yarn build
|
||||
|
||||
# 构建后端资源
|
||||
FROM m.daocloud.io/docker.io/golang:1.23 as be-builder
|
||||
|
||||
ENV GOPROXY https://goproxy.cn
|
||||
WORKDIR /mayfly
|
||||
|
||||
# Copy the go source for building server
|
||||
COPY server .
|
||||
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
||||
|
||||
# Build
|
||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
||||
go build -a -ldflags=-w \
|
||||
-o mayfly-go main.go
|
||||
|
||||
FROM m.daocloud.io/docker.io/alpine:3.20.2
|
||||
|
||||
ARG TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
WORKDIR /mayfly-go
|
||||
|
||||
COPY --from=be-builder /mayfly/mayfly-go /usr/local/bin/mayfly-go
|
||||
|
||||
CMD ["mayfly-go"]
|
||||
99
README.md
@@ -1,84 +1,109 @@
|
||||
# 🌈mayfly-go
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/objs/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
<a href="./README_EN.md">English</a> |
|
||||
<a href="https://www.yuque.com/may-fly/mayfly-go">项目文档</a> |
|
||||
<a href="https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854">操作视频</a> |
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/dromara/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/may-fly/mayfly-go" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/may-fly/mayfly-go.svg?style=social" alt="github star"/>
|
||||
<img src="https://img.shields.io/github/forks/may-fly/mayfly-go.svg?style=social" alt="github fork"/>
|
||||
<a href="https://github.com/dromara/mayfly-go" target="_blank">
|
||||
<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://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.18%2B-yellow.svg" alt="golang"/>
|
||||
<img src="https://img.shields.io/badge/Golang-1.22%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">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 前言
|
||||
|
||||
### 介绍
|
||||
web版 **linux(终端[终端回放] 文件 脚本 进程)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)。数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作、数据同步、数据迁移。redis(单机 哨兵 集群)。mongo 等集工单流程审批于一体的统一管理操作平台。**
|
||||
|
||||
## 开发语言与主要框架
|
||||
|
||||
### 开发语言与主要框架
|
||||
- 前端:typescript、vue3、element-plus
|
||||
- 后端:golang、gin、gorm
|
||||
|
||||
## 交流及问题反馈加 QQ 群
|
||||
|
||||
### 交流及问题反馈加 QQ 群
|
||||
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=IdJSHW0jTMhmWFHBUS9a83wxtrxDDhFj&jump_from=webapi">119699946</a>
|
||||
|
||||
## 演示环境
|
||||
|
||||
### 系统相关资料
|
||||
- 项目文档: https://objs.gitee.io/mayfly-go-docs
|
||||
- 系统操作视频: https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854
|
||||
- 安装包下载:https://gitee.com/objs/mayfly-go/releases
|
||||
http://go.mayfly.run
|
||||
账号/密码:test/test123.
|
||||
|
||||
## 系统核心功能截图
|
||||
|
||||
### 系统核心功能截图
|
||||
#### 首页
|
||||
|
||||
##### 记录操作记录
|
||||

|
||||

|
||||
|
||||
#### 机器操作
|
||||
##### 状态查看
|
||||

|
||||
|
||||
##### ssh终端
|
||||

|
||||
##### 状态查看
|
||||
|
||||

|
||||
|
||||
##### ssh 终端
|
||||
|
||||

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

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 数据库操作
|
||||
##### sql编辑器
|
||||

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

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

|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
#### 系统管理
|
||||
|
||||
##### 系统管理
|
||||
##### 账号管理
|
||||

|
||||
|
||||

|
||||
|
||||
##### 角色管理
|
||||

|
||||
|
||||
##### 资源管理
|
||||

|
||||

|
||||
|
||||
##### 菜单资源管理
|
||||
|
||||
**其他更多功能&操作指南可查看在线文档**: https://objs.gitee.io/mayfly-go-docs
|
||||

|
||||
|
||||
**其他更多功能&操作指南可查看上述项目文档**
|
||||
|
||||
## 💌 支持作者
|
||||
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <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,这将是对我极大的鼓励与支持。
|
||||
|
||||
105
README_EN.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 🌈mayfly-go
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">中文介绍</a> |
|
||||
<a href="https://www.yuque.com/may-fly/mayfly-go">Documentation</a> |
|
||||
<a href="https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854">Operate Video</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/dromara/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/dromara/mayfly-go" target="_blank">
|
||||
<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://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"/>
|
||||
</a>
|
||||
<a href="https://cn.vuejs.org" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 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.**
|
||||
|
||||
## Development languages and major frameworks
|
||||
|
||||
- frontend:typescript、vue3、element-plus
|
||||
- backend:golang、gin、gorm
|
||||
|
||||
## Demo
|
||||
|
||||
http://go.mayfly.run
|
||||
account/password:test/test123.
|
||||
|
||||
## Screenshots of core features
|
||||
|
||||
#### Home page
|
||||
|
||||

|
||||
|
||||
#### Machine Operation
|
||||
|
||||
##### Status
|
||||
|
||||

|
||||
|
||||
##### SSH Terminal
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||
#### Database Operation
|
||||
|
||||
##### SQL Editor
|
||||
|
||||

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

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

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

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

|
||||
|
||||
#### System Management
|
||||
|
||||
##### Account
|
||||
|
||||

|
||||
|
||||
##### Role
|
||||
|
||||

|
||||
|
||||
##### Menu & Permission
|
||||
|
||||

|
||||
|
||||
**Additional features & instructions can be found in the project documentation above.**
|
||||
|
||||
## 💌 Supporting Author
|
||||
|
||||
If you think the project is good, or you are already using it, I hope you can go to <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> to help me click ⭐ Star, which will be a great encouragement and support for me.
|
||||
150
build_release.sh
@@ -8,7 +8,7 @@ project_path=`pwd`
|
||||
# 构建后的二进制执行文件名
|
||||
exec_file_name="mayfly-go"
|
||||
# web项目目录
|
||||
web_folder="${project_path}/mayfly_go_web"
|
||||
web_folder="${project_path}/frontend"
|
||||
# server目录
|
||||
server_folder="${project_path}/server"
|
||||
|
||||
@@ -28,13 +28,13 @@ function buildWeb() {
|
||||
cd ${web_folder}
|
||||
copy2Server=$1
|
||||
|
||||
echo_yellow "-------------------打包前端开始-------------------"
|
||||
echo_yellow "-------------------Start bundling frontends-------------------"
|
||||
yarn run build
|
||||
if [ "${copy2Server}" == "2" ] ; then
|
||||
echo_green '将打包后的静态文件拷贝至server/static/static'
|
||||
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 ">>>>>>>>>>>>>>>>>>>打包前端结束<<<<<<<<<<<<<<<<<<<<\n"
|
||||
echo_yellow ">>>>>>>>>>>>>>>>>>>End of packaging frontend<<<<<<<<<<<<<<<<<<<<\n"
|
||||
}
|
||||
|
||||
function build() {
|
||||
@@ -44,43 +44,47 @@ function build() {
|
||||
toFolder=$1
|
||||
os=$2
|
||||
arch=$3
|
||||
copyStatic=$4
|
||||
copyDocScript=$4
|
||||
|
||||
echo_yellow "-------------------${os}-${arch}打包构建开始-------------------"
|
||||
echo_yellow "-------------------Start a bundle build - ${os}-${arch}-------------------"
|
||||
|
||||
cd ${server_folder}
|
||||
echo_green "打包构建可执行文件..."
|
||||
echo_green "Package build executables..."
|
||||
|
||||
execFileName=${exec_file_name}
|
||||
# 如果是windows系统,可执行文件需要添加.exe结尾
|
||||
if [ "${os}" == "windows" ];then
|
||||
execFileName="${execFileName}.exe"
|
||||
fi
|
||||
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -o ${execFileName} main.go
|
||||
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
|
||||
|
||||
if [ -d ${toFolder} ] ; then
|
||||
echo_green "目标文件夹已存在,清空文件夹"
|
||||
echo_green "The desired folder already exists. Clear the folder"
|
||||
sudo rm -rf ${toFolder}
|
||||
fi
|
||||
echo_green "创建'${toFolder}'目录"
|
||||
echo_green "Create '${toFolder}' Directory"
|
||||
mkdir ${toFolder}
|
||||
|
||||
echo_green "移动二进制文件至'${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
|
||||
# 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、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
|
||||
cp ${server_folder}/config.yml.example ${toFolder}
|
||||
cp ${server_folder}/readme.txt ${toFolder}
|
||||
cp ${server_folder}/readme_cn.txt ${toFolder}
|
||||
cp ${server_folder}/resources/script/startup.sh ${toFolder}
|
||||
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
|
||||
cp ${server_folder}/resources/script/sql/mayfly-go.sql ${toFolder}
|
||||
cp ${server_folder}/resources/data/mayfly-go.sqlite ${toFolder}
|
||||
fi
|
||||
|
||||
echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
|
||||
cp ${server_folder}/config.yml ${toFolder}
|
||||
cp ${server_folder}/mayfly-go.sql ${toFolder}
|
||||
cp ${server_folder}/readme.txt ${toFolder}
|
||||
cp ${server_folder}/startup.sh ${toFolder}
|
||||
cp ${server_folder}/shutdown.sh ${toFolder}
|
||||
|
||||
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"
|
||||
echo_yellow ">>>>>>>>>>>>>>>>>>> ${os}-${arch} - Bundle build complete <<<<<<<<<<<<<<<<<<<<\n"
|
||||
}
|
||||
|
||||
function buildLinuxAmd64() {
|
||||
@@ -99,45 +103,99 @@ function buildMac() {
|
||||
build "$1/mayfly-go-mac" "darwin" "amd64" $2
|
||||
}
|
||||
|
||||
function runBuild() {
|
||||
# 构建结果的目的路径
|
||||
read -p "请输入构建产物输出目录: " toPath
|
||||
if [ ! -d ${toPath} ] ; then
|
||||
echo_red "构建产物输出目录不存在!"
|
||||
exit;
|
||||
fi
|
||||
# 进入目标路径,并赋值全路径
|
||||
cd ${toPath}
|
||||
toPath=`pwd`
|
||||
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-------------------"
|
||||
}
|
||||
|
||||
read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
|
||||
read -p "请选择构建版本[0|其他->全部 1->linux-amd64 2->linux-arm64 3->windows 4->mac]: " buildType
|
||||
|
||||
|
||||
if [ "${runBuildWeb}" == "1" ] || [ "${runBuildWeb}" == "2" ] ; then
|
||||
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}
|
||||
fi
|
||||
|
||||
if [[ "${buildType}" == "5" ]] || [[ "${buildType}" == "6" ]] ; then
|
||||
read -p "Please enter the docker image version (default latest) : " imageVersion
|
||||
|
||||
if [ "${imageVersion}" == "" ] ; then
|
||||
imageVersion="latest"
|
||||
fi
|
||||
fi
|
||||
|
||||
case ${buildType} in
|
||||
"1")
|
||||
buildLinuxAmd64 ${toPath} ${runBuildWeb}
|
||||
buildLinuxAmd64 ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"2")
|
||||
buildLinuxArm64 ${toPath} ${runBuildWeb}
|
||||
buildLinuxArm64 ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"3")
|
||||
buildWindows ${toPath} ${runBuildWeb}
|
||||
buildWindows ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"4")
|
||||
buildMac ${toPath} ${runBuildWeb}
|
||||
buildMac ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"5")
|
||||
buildDocker ${imageVersion}
|
||||
;;
|
||||
"6")
|
||||
buildxDocker ${imageVersion}
|
||||
;;
|
||||
*)
|
||||
buildLinuxAmd64 ${toPath} ${runBuildWeb}
|
||||
buildLinuxArm64 ${toPath} ${runBuildWeb}
|
||||
buildWindows ${toPath} ${runBuildWeb}
|
||||
buildMac ${toPath} ${runBuildWeb}
|
||||
buildLinuxAmd64 ${toPath} ${copyDocScript}
|
||||
buildLinuxArm64 ${toPath} ${copyDocScript}
|
||||
buildWindows ${toPath} ${copyDocScript}
|
||||
buildMac ${toPath} ${copyDocScript}
|
||||
;;
|
||||
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
|
||||
fi
|
||||
}
|
||||
|
||||
runBuild
|
||||
runBuild
|
||||
|
||||
31
docker-compose.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: "mysql:8"
|
||||
container_name: mayfly-go-mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 111049
|
||||
MYSQL_DATABASE: mayfly-go
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- ./server/docs/docker-compose/mysql/data/mydir:/mydir
|
||||
- ./server/docs/docker-compose/mysql/data/datadir:/var/lib/mysql
|
||||
restart: always
|
||||
|
||||
server:
|
||||
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: mayfly-go-server
|
||||
ports:
|
||||
- "8888:8888"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: mysql:3306
|
||||
volumes:
|
||||
- ./server/config.yml.example:/mayfly/config.yml
|
||||
depends_on:
|
||||
- mysql
|
||||
restart: always
|
||||
@@ -5,4 +5,6 @@ VITE_PORT = 8889
|
||||
VITE_OPEN = false
|
||||
|
||||
# public path 配置线上环境路径(打包)
|
||||
VITE_PUBLIC_PATH = ''
|
||||
VITE_PUBLIC_PATH = ''
|
||||
|
||||
VITE_EDITOR=idea
|
||||
11
frontend/.env.development
Normal file
@@ -0,0 +1,11 @@
|
||||
# 本地环境
|
||||
ENV = 'development'
|
||||
|
||||
VITE_OPEN = true
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
9
frontend/.env.production
Normal file
@@ -0,0 +1,9 @@
|
||||
# 线上环境
|
||||
ENV = 'production'
|
||||
|
||||
# 线上环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
76
frontend/.eslintrc.js
Normal file
@@ -0,0 +1,76 @@
|
||||
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',
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
*.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
39
frontend/.prettierrc.cjs
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
// 一行最多多少个字符
|
||||
printWidth: 160,
|
||||
// 指定每个缩进级别的空格数
|
||||
tabWidth: 4,
|
||||
// 使用制表符而不是空格缩进行
|
||||
useTabs: false,
|
||||
// 在语句末尾打印分号
|
||||
semi: true,
|
||||
// 使用单引号而不是双引号
|
||||
singleQuote: true,
|
||||
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
|
||||
quoteProps: 'as-needed',
|
||||
// 在JSX中使用单引号而不是双引号
|
||||
jsxSingleQuote: false,
|
||||
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
|
||||
trailingComma: 'es5',
|
||||
// 在对象文字中的括号之间打印空格
|
||||
bracketSpacing: true,
|
||||
// jsx 标签的反尖括号需要换行
|
||||
jsxBracketSameLine: false,
|
||||
// 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
|
||||
arrowParens: 'always',
|
||||
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
// 指定要使用的解析器,不需要写文件开头的 @prettier
|
||||
requirePragma: false,
|
||||
// 不需要自动在文件开头插入 @prettier
|
||||
insertPragma: false,
|
||||
// 使用默认的折行标准 always\never\preserve
|
||||
proseWrap: 'preserve',
|
||||
// 指定HTML文件的全局空格敏感度 css\strict\ignore
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
// Vue文件脚本和样式标签缩进
|
||||
vueIndentScriptAndStyle: false,
|
||||
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
|
||||
endOfLine: 'lf',
|
||||
};
|
||||
21
frontend/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh_CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="keywords" content="mayfly-go" />
|
||||
<meta name="description" content="" />
|
||||
|
||||
<link type="favicon" rel="shortcut icon" href="favicon.ico" />
|
||||
<title>mayfly-go</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="application/javascript" src="./config.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
72
frontend/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "mayfly-go-frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build-preview": "npm run build && npm run preview",
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"asciinema-player": "^3.8.1",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"cropperjs": "^1.6.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.8.8",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.2.6",
|
||||
"qrcode.vue": "^3.5.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.3",
|
||||
"splitpanes": "^3.1.5",
|
||||
"sql-formatter": "^15.4.5",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^10.0.4",
|
||||
"vue-router": "^4.4.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
"xterm-addon-web-links": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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.0",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.81.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.11",
|
||||
"vue-eslint-parser": "^9.4.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
25
frontend/public/config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
window.globalConfig = {
|
||||
// 默认为空,以访问根目录为api请求地址。若前后端分离部署可单独配置该后端api请求地址
|
||||
BaseApiUrl: '',
|
||||
BaseWsUrl: '',
|
||||
};
|
||||
|
||||
// index.html添加百秒级时间戳,防止被浏览器缓存
|
||||
// !(function () {
|
||||
// let t = 't=' + new Date().getTime().toString().substring(0, 8);
|
||||
// let search = location.search;
|
||||
// let m = search && search.match(/t=\d*/g);
|
||||
|
||||
// console.log(location);
|
||||
// if (m[0]) {
|
||||
// if (m[0] !== t) {
|
||||
// location.search = search.replace(m[0], t);
|
||||
// }
|
||||
// } else {
|
||||
// if (search.indexOf('?') > -1) {
|
||||
// location.search = search + '&' + t;
|
||||
// } else {
|
||||
// location.search = t;
|
||||
// }
|
||||
// }
|
||||
// })();
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
137
frontend/src/App.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<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" />
|
||||
|
||||
<LockScreen v-if="themeConfig.isLockScreen" />
|
||||
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="app">
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch, computed } 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 route = useRoute();
|
||||
|
||||
const themeConfigStores = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(themeConfigStores);
|
||||
|
||||
// 定义变量内容
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
// 布局配置弹窗打开
|
||||
const openSetingsDrawer = () => {
|
||||
setingsRef.value.openDrawer();
|
||||
};
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 监听布局配置弹窗点击打开
|
||||
mittBus.on('openSetingsDrawer', () => {
|
||||
openSetingsDrawer();
|
||||
});
|
||||
|
||||
// 初始化系统主题
|
||||
themeConfigStores.initThemeConfig();
|
||||
});
|
||||
});
|
||||
|
||||
// 监听 themeConfig isWartermark配置文件的变化
|
||||
watch(
|
||||
() => themeConfig.value.isWatermark,
|
||||
(val) => {
|
||||
if (val) {
|
||||
setTimeout(() => {
|
||||
setWatermarkContent();
|
||||
refreshWatermarkTime();
|
||||
resume();
|
||||
}, 500);
|
||||
} else {
|
||||
pause();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => themeConfig.value.globalI18n,
|
||||
(val) => {
|
||||
locale.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
themeConfig,
|
||||
(val) => {
|
||||
saveThemeConfig(val);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 获取全局组件大小
|
||||
const getGlobalComponentSize = computed(() => {
|
||||
return themeConfig.value.globalComponentSize;
|
||||
});
|
||||
|
||||
// 获取全局 i18n
|
||||
const getGlobalI18n = computed(() => {
|
||||
return EnumValue.getEnumByValue(I18nEnum, locale.value)?.extra.el;
|
||||
});
|
||||
|
||||
// 刷新水印时间
|
||||
const { pause, resume } = useIntervalFn(() => {
|
||||
if (!themeConfig.value.isWatermark) {
|
||||
pause();
|
||||
}
|
||||
refreshWatermarkTime();
|
||||
}, 60000);
|
||||
|
||||
const setWatermarkContent = () => {
|
||||
themeConfigStores.setWatermarkUser();
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新水印时间
|
||||
*/
|
||||
const refreshWatermarkTime = () => {
|
||||
themeConfigStores.setWatermarkNowTime();
|
||||
};
|
||||
|
||||
// 页面销毁时,关闭监听布局配置
|
||||
onUnmounted(() => {
|
||||
mittBus.off('openSetingsDrawer', () => {});
|
||||
});
|
||||
|
||||
// 监听路由的变化,设置网站标题
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
document.title = `${t((route.meta.title as string) || '')} - ${themeConfig.value.globalTitle}` || themeConfig.value.globalTitle;
|
||||
});
|
||||
}
|
||||
);
|
||||
</script>
|
||||
66
frontend/src/assets/iconfont/iconfont.js
Normal file
121
frontend/src/assets/iconfont/iconfont.json
Normal file
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"id": "3953964",
|
||||
"name": "mayfly-go",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "2967035",
|
||||
"name": "符号-英文",
|
||||
"font_class": "fuhao-yingwen",
|
||||
"unicode": "e712",
|
||||
"unicode_decimal": 59154
|
||||
},
|
||||
{
|
||||
"icon_id": "26283783",
|
||||
"name": "符号-中文",
|
||||
"font_class": "fuhao-zhongwen",
|
||||
"unicode": "e603",
|
||||
"unicode_decimal": 58883
|
||||
},
|
||||
{
|
||||
"icon_id": "23957582",
|
||||
"name": "MongoDB",
|
||||
"font_class": "mongo",
|
||||
"unicode": "e646",
|
||||
"unicode_decimal": 58950
|
||||
},
|
||||
{
|
||||
"icon_id": "4969649",
|
||||
"name": "Redis",
|
||||
"font_class": "op-redis",
|
||||
"unicode": "e728",
|
||||
"unicode_decimal": 59176
|
||||
},
|
||||
{
|
||||
"icon_id": "22442993",
|
||||
"name": "PostgreSQL",
|
||||
"font_class": "op-postgres",
|
||||
"unicode": "e8b7",
|
||||
"unicode_decimal": 59575
|
||||
},
|
||||
{
|
||||
"icon_id": "12295203",
|
||||
"name": "达梦数据库",
|
||||
"font_class": "db-dm",
|
||||
"unicode": "e6f0",
|
||||
"unicode_decimal": 59120
|
||||
},
|
||||
{
|
||||
"icon_id": "10055634",
|
||||
"name": "云数据库MongoDB",
|
||||
"font_class": "op-mongo",
|
||||
"unicode": "e7d7",
|
||||
"unicode_decimal": 59351
|
||||
},
|
||||
{
|
||||
"icon_id": "10055642",
|
||||
"name": "云数据库 RDS MySQL",
|
||||
"font_class": "op-mysql",
|
||||
"unicode": "e7d8",
|
||||
"unicode_decimal": 59352
|
||||
},
|
||||
{
|
||||
"icon_id": "3876165",
|
||||
"name": "redis",
|
||||
"font_class": "redis",
|
||||
"unicode": "e619",
|
||||
"unicode_decimal": 58905
|
||||
},
|
||||
{
|
||||
"icon_id": "25271976",
|
||||
"name": "oracle",
|
||||
"font_class": "oracle",
|
||||
"unicode": "e507",
|
||||
"unicode_decimal": 58631
|
||||
},
|
||||
{
|
||||
"icon_id": "8105644",
|
||||
"name": "mariadb",
|
||||
"font_class": "mariadb",
|
||||
"unicode": "e513",
|
||||
"unicode_decimal": 58643
|
||||
},
|
||||
{
|
||||
"icon_id": "13601813",
|
||||
"name": "sqlite",
|
||||
"font_class": "sqlite",
|
||||
"unicode": "e546",
|
||||
"unicode_decimal": 58694
|
||||
},
|
||||
{
|
||||
"icon_id": "29340317",
|
||||
"name": "temp-mssql",
|
||||
"font_class": "MSSQLNATIVE",
|
||||
"unicode": "e600",
|
||||
"unicode_decimal": 58880
|
||||
},
|
||||
{
|
||||
"icon_id": "7699332",
|
||||
"name": "gaussdb",
|
||||
"font_class": "gauss",
|
||||
"unicode": "e683",
|
||||
"unicode_decimal": 59011
|
||||
},
|
||||
{
|
||||
"icon_id": "34836637",
|
||||
"name": "kingbase",
|
||||
"font_class": "kingbase",
|
||||
"unicode": "e882",
|
||||
"unicode_decimal": 59522
|
||||
},
|
||||
{
|
||||
"icon_id": "33047500",
|
||||
"name": "vastbase",
|
||||
"font_class": "vastbase",
|
||||
"unicode": "e62b",
|
||||
"unicode_decimal": 58923
|
||||
}
|
||||
]
|
||||
}
|
||||
1
frontend/src/assets/image/401.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
1
frontend/src/assets/image/404.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
frontend/src/assets/image/login-bg-main.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
19
frontend/src/assets/image/login-bg-split.svg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
126
frontend/src/common/Api.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import request from './request';
|
||||
import { useApiFetch } from '@/hooks/useRequest';
|
||||
|
||||
/**
|
||||
* 可用于各模块定义各自api请求
|
||||
*/
|
||||
class Api {
|
||||
/**
|
||||
* 请求url
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* 请求方法
|
||||
*/
|
||||
method: string;
|
||||
|
||||
/**
|
||||
* 请求前处理函数
|
||||
* param1: param请求参数
|
||||
*/
|
||||
beforeHandler: Function;
|
||||
|
||||
constructor(url: string, method: string) {
|
||||
this.url = url;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求前处理回调函数
|
||||
* @param func 请求前处理器
|
||||
* @returns this
|
||||
*/
|
||||
withBeforeHandler(func: Function) {
|
||||
this.beforeHandler = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限的完整url
|
||||
*/
|
||||
getUrl() {
|
||||
return request.getApiUrl(this.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式使用该api
|
||||
* @param params 响应式params
|
||||
* @param reqOptions 其他可选值
|
||||
* @returns
|
||||
*/
|
||||
useApi<T>(params: any = null, reqOptions: RequestInit = {}) {
|
||||
return useApiFetch<T>(this, params, reqOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch 请求对应的该api
|
||||
* @param {Object} param 请求该api的参数
|
||||
*/
|
||||
async request(param: any = null, options: any = {}): Promise<any> {
|
||||
const { execute, data } = this.useApi(param, options);
|
||||
await execute();
|
||||
return data.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* xhr 请求对应的该api
|
||||
* @param {Object} param 请求该api的参数
|
||||
*/
|
||||
async xhrReq(param: any = null, options: any = {}): Promise<any> {
|
||||
if (this.beforeHandler) {
|
||||
await this.beforeHandler(param);
|
||||
}
|
||||
return request.xhrReq(this.method, this.url, param, options);
|
||||
}
|
||||
|
||||
/** 静态方法 **/
|
||||
|
||||
/**
|
||||
* 静态工厂,返回Api对象,并设置url与method属性
|
||||
* @param url url
|
||||
* @param method 请求方法(get,post,put,delete...)
|
||||
*/
|
||||
static create(url: string, method: string): Api {
|
||||
return new Api(url, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建get api
|
||||
* @param url url
|
||||
*/
|
||||
static newGet(url: string): Api {
|
||||
return Api.create(url, 'get');
|
||||
}
|
||||
|
||||
/**
|
||||
* new post api
|
||||
* @param url url
|
||||
*/
|
||||
static newPost(url: string): Api {
|
||||
return Api.create(url, 'post');
|
||||
}
|
||||
|
||||
/**
|
||||
* new put api
|
||||
* @param url url
|
||||
*/
|
||||
static newPut(url: string): Api {
|
||||
return Api.create(url, 'put');
|
||||
}
|
||||
|
||||
/**
|
||||
* new delete api
|
||||
* @param url url
|
||||
*/
|
||||
static newDelete(url: string): Api {
|
||||
return Api.create(url, 'delete');
|
||||
}
|
||||
}
|
||||
|
||||
export default Api;
|
||||
|
||||
export class PageRes {
|
||||
list: any[] = [];
|
||||
total: number = 0;
|
||||
}
|
||||
102
frontend/src/common/Enum.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
export interface EnumValueTag {
|
||||
color?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举值
|
||||
*/
|
||||
export class EnumValue {
|
||||
/**
|
||||
* 枚举值
|
||||
*/
|
||||
value: any;
|
||||
|
||||
/**
|
||||
* 枚举描述
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* 展示的标签信息
|
||||
*/
|
||||
tag: EnumValueTag;
|
||||
|
||||
extra: any;
|
||||
|
||||
constructor(value: any, label: string) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
setTagType(type: string = 'primary'): EnumValue {
|
||||
this.tag = { type };
|
||||
return this;
|
||||
}
|
||||
|
||||
tagTypeInfo(): EnumValue {
|
||||
return this.setTagType('info');
|
||||
}
|
||||
|
||||
tagTypeSuccess(): EnumValue {
|
||||
return this.setTagType('success');
|
||||
}
|
||||
|
||||
tagTypeDanger(): EnumValue {
|
||||
return this.setTagType('danger');
|
||||
}
|
||||
|
||||
tagTypeWarning(): EnumValue {
|
||||
return this.setTagType('warning');
|
||||
}
|
||||
|
||||
setTagColor(color: string): EnumValue {
|
||||
this.tag = { color };
|
||||
return this;
|
||||
}
|
||||
|
||||
setExtra(extra: any): EnumValue {
|
||||
this.extra = extra;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static of(value: any, label: string): EnumValue {
|
||||
return new EnumValue(value, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据枚举值获取指定枚举值对象
|
||||
*
|
||||
* @param enums 枚举对象
|
||||
* @param value 需要匹配的枚举值
|
||||
* @returns 枚举值对象
|
||||
*/
|
||||
static getEnumByValue(enums: any, value: any): EnumValue | null {
|
||||
const enumValues = Object.values(enums) as any;
|
||||
for (let enumValue of enumValues) {
|
||||
if (enumValue.value == value) {
|
||||
return enumValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据枚举值获取枚举描述
|
||||
*
|
||||
* @param enums 枚举对象
|
||||
* @param value 枚举值
|
||||
* @returns 枚举描述
|
||||
*/
|
||||
static getLabelByValue(enums: any, value: any) {
|
||||
const enumValues = Object.values(enums) as any;
|
||||
for (let enumValue of enumValues) {
|
||||
if (enumValue['value'] == value) {
|
||||
return enumValue['label'];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default EnumValue;
|
||||
43
frontend/src/common/SocketBuilder.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
89
frontend/src/common/assert.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
/**
|
||||
* 不符合业务断言错误
|
||||
*/
|
||||
class AssertError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
// 错误类名
|
||||
this.name = 'AssertError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言表达式为true
|
||||
*
|
||||
* @param condition 条件表达式
|
||||
* @param msg 错误消息
|
||||
*/
|
||||
export function isTrue(condition: boolean, msg: string) {
|
||||
if (!condition) {
|
||||
throw new AssertError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言不能为空值,即null,0,''等
|
||||
*
|
||||
* @param obj 对象1
|
||||
* @param msg 错误消息
|
||||
*/
|
||||
export function notBlank(obj: any, msg: string) {
|
||||
isTrue(obj, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言不能为空值,即null,0,''等
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param field 字段(支持i18n msgKey)
|
||||
*/
|
||||
export function notBlankI18n(obj: any, field: string) {
|
||||
notBlank(obj, i18n.global.t('common.fieldNotEmpty', { field: i18n.global.t(field) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言两对象相等
|
||||
*
|
||||
* @param obj1 对象1
|
||||
* @param obj2 对象2
|
||||
* @param msg 错误消息
|
||||
*/
|
||||
export function isEquals(obj1: any, obj2: any, msg: string) {
|
||||
isTrue(obj1 === obj2, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言对象不为null或undefiend
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param msg 错误提示
|
||||
*/
|
||||
export function notNull(obj: any, msg: string) {
|
||||
if (obj == null || obj == undefined) {
|
||||
throw new AssertError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言字符串不能为空
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param msg 错误提示
|
||||
*/
|
||||
export function notEmpty(str: string, msg: string) {
|
||||
if (str == null || str == undefined || str == '') {
|
||||
throw new AssertError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言字符串不能为空
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param field 字段(支持i18n msgKey)
|
||||
*/
|
||||
export function notEmptyI18n(str: string, field: string) {
|
||||
notEmpty(str, i18n.global.t('common.fieldNotEmpty', { field: i18n.global.t(field) }));
|
||||
}
|
||||
33
frontend/src/common/commonEnum.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import EnumValue from './Enum';
|
||||
// element plus 自带国际化
|
||||
import zhcnLocale from 'element-plus/es/locale/lang/zh-cn';
|
||||
import enLocale from 'element-plus/es/locale/lang/en';
|
||||
|
||||
// i18n
|
||||
export const I18nEnum = {
|
||||
ZhCn: EnumValue.of('zh-cn', '简体中文').setExtra({ icon: 'iconfont icon-fuhao-zhongwen', el: zhcnLocale }),
|
||||
En: EnumValue.of('en', 'English').setExtra({ icon: 'iconfont icon-fuhao-yingwen', el: enLocale }),
|
||||
};
|
||||
|
||||
// 资源类型
|
||||
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(),
|
||||
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
||||
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||
};
|
||||
|
||||
// 标签关联的资源类型
|
||||
export const TagResourceTypeEnum = {
|
||||
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
|
||||
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
|
||||
|
||||
Machine: ResourceTypeEnum.Machine,
|
||||
Db: ResourceTypeEnum.Db,
|
||||
Redis: ResourceTypeEnum.Redis,
|
||||
Mongo: ResourceTypeEnum.Mongo,
|
||||
|
||||
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
DbAuthCert: EnumValue.of(21, '数据库-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
DbName: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
||||
};
|
||||
21
frontend/src/common/config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
function getBaseApiUrl() {
|
||||
let path = window.location.pathname;
|
||||
if (path == '/') {
|
||||
return window.location.host;
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
// 去除最后一个/
|
||||
return window.location.host + path.replace(/\/$/, '');
|
||||
}
|
||||
return window.location.host + path;
|
||||
}
|
||||
|
||||
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.1',
|
||||
};
|
||||
|
||||
export default config;
|
||||
38
frontend/src/common/crypto.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
|
||||
/**
|
||||
* AES 加密数据
|
||||
* @param word
|
||||
* @param key
|
||||
*/
|
||||
export function AesEncrypt(word: string, key?: string) {
|
||||
if (!key) {
|
||||
key = getToken().substring(0, 24);
|
||||
}
|
||||
|
||||
const sKey = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(word, sKey, {
|
||||
iv: sKey,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
|
||||
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
|
||||
export function AesDecrypt(word: string, key?: string): string {
|
||||
if (!key) {
|
||||
key = getToken().substring(0, 24);
|
||||
}
|
||||
|
||||
const sKey = CryptoJS.enc.Utf8.parse(key);
|
||||
// key 和 iv 使用同一个值
|
||||
const decrypted = CryptoJS.AES.decrypt(word, sKey, {
|
||||
iv: sKey,
|
||||
mode: CryptoJS.mode.CBC, // CBC算法
|
||||
padding: CryptoJS.pad.Pkcs7, //使用pkcs7 进行padding 后端需要注意
|
||||
});
|
||||
|
||||
return decrypted.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
19
frontend/src/common/openApi.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import request from './request';
|
||||
|
||||
export default {
|
||||
login: (param: any) => request.post('/auth/accounts/login', param),
|
||||
refreshToken: (param: any) => request.get('/auth/accounts/refreshToken', param),
|
||||
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
||||
getPublicKey: () => request.get('/common/public-key'),
|
||||
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
|
||||
getServerConf: () => request.get('/sys/configs/server'),
|
||||
oauth2LoginConfig: () => request.get('/auth/oauth2-config'),
|
||||
changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
|
||||
captcha: () => request.get('/sys/captcha'),
|
||||
logout: () => request.post('/auth/accounts/logout'),
|
||||
getPermissions: () => request.get('/sys/accounts/permissions'),
|
||||
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
|
||||
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
|
||||
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
|
||||
getFileDetail: (keys: string[]) => request.get(`/sys/files/detail/${keys.join(',')}`),
|
||||
};
|
||||
11
frontend/src/common/pattern.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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'),
|
||||
};
|
||||
260
frontend/src/common/request.ts
Executable file
@@ -0,0 +1,260 @@
|
||||
import router from '../router';
|
||||
import config from './config';
|
||||
import { getClientId, getToken } from './utils/storage';
|
||||
import { templateResolve } from './utils/string';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import axios from 'axios';
|
||||
import { useApiFetch } from '../hooks/useRequest';
|
||||
import Api from './Api';
|
||||
|
||||
export default {
|
||||
request,
|
||||
xhrReq,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del,
|
||||
getApiUrl,
|
||||
};
|
||||
|
||||
export interface Result {
|
||||
/**
|
||||
* 响应码
|
||||
*/
|
||||
code: number;
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
msg: string;
|
||||
/**
|
||||
* 数据
|
||||
*/
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export enum ResultEnum {
|
||||
SUCCESS = 200,
|
||||
ERROR = 400,
|
||||
PARAM_ERROR = 405,
|
||||
SERVER_ERROR = 500,
|
||||
NO_PERMISSION = 501,
|
||||
ACCESS_TOKEN_INVALID = 502, // accessToken失效
|
||||
}
|
||||
|
||||
export const baseUrl: string = config.baseApiUrl;
|
||||
// const baseUrl: string = 'http://localhost:18888/api';
|
||||
// const baseWsUrl: string = config.baseWsUrl;
|
||||
|
||||
/**
|
||||
* 通知错误消息
|
||||
* @param msg 错误消息
|
||||
*/
|
||||
function notifyErrorMsg(msg: string) {
|
||||
// 危险通知
|
||||
ElMessage.error(msg);
|
||||
}
|
||||
|
||||
// create an axios instance
|
||||
const axiosInst = axios.create({
|
||||
baseURL: baseUrl, // url = base url + request url
|
||||
timeout: 60000, // request timeout
|
||||
});
|
||||
|
||||
// request interceptor
|
||||
axiosInst.interceptors.request.use(
|
||||
(config: any) => {
|
||||
// do something before request is sent
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
// 设置token
|
||||
config.headers['Authorization'] = token;
|
||||
config.headers['ClientId'] = getClientId();
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// response interceptor
|
||||
axiosInst.interceptors.response.use(
|
||||
(response) => response,
|
||||
(e: any) => {
|
||||
const rejectPromise = Promise.reject(e);
|
||||
|
||||
if (axios.isCancel(e)) {
|
||||
console.log('请求已取消');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const statusCode = e.response?.status;
|
||||
if (statusCode == 500) {
|
||||
notifyErrorMsg('服务器未知异常');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
if (statusCode == 404) {
|
||||
notifyErrorMsg('请求接口未找到');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
if (e.message) {
|
||||
// 对响应错误做点什么
|
||||
if (e.message.indexOf('timeout') != -1) {
|
||||
notifyErrorMsg('网络请求超时');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
if (e.message == 'Network Error') {
|
||||
notifyErrorMsg('网络连接错误');
|
||||
return rejectPromise;
|
||||
}
|
||||
}
|
||||
|
||||
notifyErrorMsg('网络请求错误');
|
||||
return rejectPromise;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* xhr请求url
|
||||
*
|
||||
* @param method 请求方法
|
||||
* @param url url
|
||||
* @param params 参数
|
||||
* @param options 可选
|
||||
* @returns
|
||||
*/
|
||||
export function xhrReq(method: string, url: string, params: any = null, options: any = {}) {
|
||||
if (!url) {
|
||||
throw new Error('请求url不能为空');
|
||||
}
|
||||
|
||||
// 简单判断该url是否是restful风格
|
||||
if (url.indexOf('{') != -1) {
|
||||
url = templateResolve(url, params);
|
||||
}
|
||||
|
||||
const req: any = {
|
||||
method,
|
||||
url,
|
||||
...options,
|
||||
};
|
||||
|
||||
// post和put使用json格式传参
|
||||
if (method === 'post' || method === 'put') {
|
||||
req.data = params;
|
||||
} else {
|
||||
req.params = params;
|
||||
}
|
||||
|
||||
return axiosInst
|
||||
.request(req)
|
||||
.then((response) => {
|
||||
// 获取请求返回结果
|
||||
const result: Result = response.data;
|
||||
return parseResult(result);
|
||||
})
|
||||
.catch((e) => {
|
||||
return Promise.reject(e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch请求url
|
||||
*
|
||||
* 该方法已处理请求结果中code != 200的message提示,如需其他错误处理(取消加载状态,重置对象状态等等),可catch继续处理
|
||||
*
|
||||
* @param {Object} method 请求方法(GET,POST,PUT,DELTE等)
|
||||
* @param {Object} uri uri
|
||||
* @param {Object} params 参数
|
||||
*/
|
||||
async function request(method: string, url: string, params: any = null, options: any = {}): Promise<any> {
|
||||
const { execute, data } = useApiFetch(Api.create(url, method), params, options);
|
||||
await execute();
|
||||
return data.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* get请求uri
|
||||
* 该方法已处理请求结果中code != 200的message提示,如需其他错误处理(取消加载状态,重置对象状态等等),可catch继续处理
|
||||
*
|
||||
* @param {Object} url uri
|
||||
* @param {Object} params 参数
|
||||
*/
|
||||
function get(url: string, params: any = null, options: any = {}): Promise<any> {
|
||||
return request('get', url, params, options);
|
||||
}
|
||||
|
||||
function post(url: string, params: any = null, options: any = {}): Promise<any> {
|
||||
return request('post', url, params, options);
|
||||
}
|
||||
|
||||
function put(url: string, params: any = null, options: any = {}): Promise<any> {
|
||||
return request('put', url, params, options);
|
||||
}
|
||||
|
||||
function del(url: string, params: any = null, options: any = {}): Promise<any> {
|
||||
return request('delete', url, params, options);
|
||||
}
|
||||
|
||||
function getApiUrl(url: string) {
|
||||
// 只是返回api地址而不做请求,用在上传组件之类的
|
||||
return baseUrl + url + '?' + joinClientParams();
|
||||
}
|
||||
|
||||
// 组装客户端参数,包括 token 和 clientId
|
||||
export function joinClientParams(): string {
|
||||
return `token=${getToken()}&clientId=${getClientId()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件url地址
|
||||
* @param key 文件key
|
||||
* @returns 文件url
|
||||
*/
|
||||
export function getFileUrl(key: string) {
|
||||
return `${baseUrl}/sys/files/${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统文件上传url
|
||||
* @param key 文件key
|
||||
* @returns 文件上传url
|
||||
*/
|
||||
export function getUploadFileUrl(key: string = '') {
|
||||
return `${baseUrl}/sys/files/upload?token=${getToken()}&fileKey=${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param key 文件key
|
||||
*/
|
||||
export function downloadFile(key: string) {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('href', `${getFileUrl(key)}`);
|
||||
a.setAttribute('target', '_blank');
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
function parseResult(result: Result) {
|
||||
if (result.code === ResultEnum.SUCCESS) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// 如果提示没有权限,则移除token,使其重新登录
|
||||
if (result.code === ResultEnum.NO_PERMISSION) {
|
||||
router.push({
|
||||
path: '/401',
|
||||
});
|
||||
}
|
||||
|
||||
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面)
|
||||
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
|
||||
notifyErrorMsg(result.msg);
|
||||
}
|
||||
|
||||
return Promise.reject(result);
|
||||
}
|
||||
36
frontend/src/common/rsa.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
}
|
||||
112
frontend/src/common/sysconfig.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import openApi from './openApi';
|
||||
|
||||
// 登录是否使用验证码配置key
|
||||
const AccountLoginSecurityKey = 'AccountLoginSecurity';
|
||||
const MachineConfigKey = 'MachineConfig';
|
||||
const SysStyleConfigKey = 'SysStyleConfig';
|
||||
|
||||
/**
|
||||
* 获取账号登录安全配置
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export async function getAccountLoginSecurity(): Promise<any> {
|
||||
const value = await getConfigValue(AccountLoginSecurityKey);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const jsonValue = JSON.parse(value);
|
||||
jsonValue.useCaptcha = convertBool(jsonValue.useCaptcha, true);
|
||||
jsonValue.useOtp = convertBool(jsonValue.useOtp, true);
|
||||
return jsonValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局系统样式配置(logo、title等)
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export async function getSysStyleConfig(): Promise<any> {
|
||||
const value = await getConfigValue(SysStyleConfigKey);
|
||||
const defaultValue = {
|
||||
useWatermark: true,
|
||||
};
|
||||
if (!value) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const jsonValue = JSON.parse(value);
|
||||
// 将字符串转为bool
|
||||
jsonValue.useWatermark = convertBool(jsonValue.useWatermark, true);
|
||||
return jsonValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取LDAP登录配置
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export async function getLdapEnabled(): Promise<any> {
|
||||
const value = await openApi.getLdapEnabled();
|
||||
return convertBool(value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器配置
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export async function getMachineConfig(): Promise<any> {
|
||||
const value = await getConfigValue(MachineConfigKey);
|
||||
const defaultValue = {
|
||||
// 默认1gb
|
||||
uploadMaxFileSize: '1GB',
|
||||
};
|
||||
if (!value) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
const jsonValue = JSON.parse(value);
|
||||
return jsonValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统服务启动配置
|
||||
*
|
||||
* @returns 配置信息
|
||||
*/
|
||||
export async function getServerConf(): Promise<any> {
|
||||
return openApi.getServerConf();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置值
|
||||
*
|
||||
* @param key 配置key
|
||||
* @returns 配置值
|
||||
*/
|
||||
export async function getConfigValue(key: string): Promise<string> {
|
||||
return (await openApi.getConfigValue({ key })) as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取bool类型系统配置值
|
||||
*
|
||||
* @param key 配置key
|
||||
* @param defaultValue 默认值
|
||||
* @returns 是否为ture,1: true;其他: false
|
||||
*/
|
||||
export async function getBoolConfigValue(key: string, defaultValue: boolean): Promise<boolean> {
|
||||
const value = await getConfigValue(key);
|
||||
return convertBool(value, defaultValue);
|
||||
}
|
||||
|
||||
function convertBool(value: string, defaultValue: boolean) {
|
||||
if (!value) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value == '1' || value == 'true';
|
||||
}
|
||||
116
frontend/src/common/syssocket.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import Config from './config';
|
||||
import {ElNotification} from 'element-plus';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import {getToken} from '@/common/utils/storage';
|
||||
|
||||
import {joinClientParams} from './request';
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
* socket连接
|
||||
*/
|
||||
socket: any;
|
||||
|
||||
/**
|
||||
* key -> 消息类别,value -> 消息对应的处理器函数
|
||||
*/
|
||||
categoryHandlers: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
messageTypes: any = {
|
||||
0: 'error',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
22: 'info',
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化全局系统消息websocket
|
||||
*/
|
||||
init() {
|
||||
// 存在则不需要重新建立连接
|
||||
if (this.socket) {
|
||||
return;
|
||||
}
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
console.log('init system ws');
|
||||
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
|
||||
this.socket = SocketBuilder.builder(sysMsgUrl)
|
||||
.message((event: { data: string }) => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch (e) {
|
||||
console.error('解析ws消息失败', e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理
|
||||
const handler = this.categoryHandlers.get(message.category);
|
||||
if (handler) {
|
||||
handler(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = this.getMsgType(message.type);
|
||||
let msg = message.msg
|
||||
let duration = 0
|
||||
if (message.type == 22) {
|
||||
let obj = JSON.parse(msg);
|
||||
msg = `文件:${obj['title']} 执行成功: ${obj['executedStatements']} 条`
|
||||
duration = 2000
|
||||
}
|
||||
ElNotification({
|
||||
duration: duration,
|
||||
title: message.title,
|
||||
message: msg,
|
||||
type: type,
|
||||
});
|
||||
console.log(message)
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.close(() => {
|
||||
console.log('close sys socket');
|
||||
this.socket = null;
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
destory() {
|
||||
this.socket?.close();
|
||||
this.socket = null;
|
||||
this.categoryHandlers?.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息处理函数
|
||||
*
|
||||
* @param category 消息类别
|
||||
* @param handlerFunc 消息处理函数
|
||||
*/
|
||||
registerMsgHandler(category: any, handlerFunc: any) {
|
||||
this.init();
|
||||
if (this.categoryHandlers.has(category)) {
|
||||
console.log(`${category}该类别消息处理器已存在...`);
|
||||
return;
|
||||
}
|
||||
if (typeof handlerFunc != 'function') {
|
||||
throw new Error('message handler需为函数');
|
||||
}
|
||||
this.categoryHandlers.set(category, handlerFunc);
|
||||
}
|
||||
|
||||
getMsgType(msgType: any) {
|
||||
return this.messageTypes[msgType];
|
||||
}
|
||||
}
|
||||
|
||||
// 全局系统消息websocket;
|
||||
const sysSocket = new SysSocket();
|
||||
|
||||
export default sysSocket;
|
||||
45
frontend/src/common/utils/export.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
// 二维数组
|
||||
const cvsData = [columns];
|
||||
for (let data of datas) {
|
||||
// 数据值组成的一维数组
|
||||
let dataValueArr: any = [];
|
||||
for (let column of columns) {
|
||||
let val: any = data[column];
|
||||
if (val == null || val == undefined) {
|
||||
val = '';
|
||||
} else if (val && typeof val == 'string') {
|
||||
// 替换换行符
|
||||
val = val.replace(/[\r\n]/g, '\\n');
|
||||
|
||||
// csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了
|
||||
if (val.indexOf(',') != -1) {
|
||||
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
|
||||
if (val.indexOf('"') != -1) {
|
||||
val = val.replace(/"/g, '""');
|
||||
}
|
||||
// 再将逗号转义
|
||||
val = `"${val}"`;
|
||||
}
|
||||
}
|
||||
dataValueArr.push(String(val));
|
||||
}
|
||||
cvsData.push(dataValueArr);
|
||||
}
|
||||
const csvString = cvsData.map((e) => e.join(',')).join('\n');
|
||||
exportFile(`${filename}.csv`, csvString);
|
||||
}
|
||||
|
||||
export function exportFile(filename: string, content: string) {
|
||||
// 导出
|
||||
let link = document.createElement('a');
|
||||
let exportContent = '\uFEFF';
|
||||
let blob = new Blob([exportContent + content], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
link.id = 'download-file';
|
||||
link.setAttribute('href', URL.createObjectURL(blob));
|
||||
link.setAttribute('download', `${filename}`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
116
frontend/src/common/utils/format.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param date 日期 字符串 Date 时间戳等
|
||||
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
|
||||
* @returns 格式化后内容
|
||||
*/
|
||||
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节单位
|
||||
* @param size byte size
|
||||
* @returns
|
||||
*/
|
||||
export function formatByteSize(size: number, fixed = 2) {
|
||||
if (size === 0) {
|
||||
return '0B';
|
||||
}
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const base = 1024;
|
||||
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等
|
||||
* @returns
|
||||
*/
|
||||
export function convertToBytes(sizeStr: string) {
|
||||
sizeStr = sizeStr.trim();
|
||||
const unit = sizeStr.slice(-2);
|
||||
|
||||
const valueStr = sizeStr.slice(0, -2);
|
||||
const value = parseInt(valueStr, 10);
|
||||
|
||||
let bytes = 0;
|
||||
|
||||
switch (unit.toUpperCase()) {
|
||||
case 'KB':
|
||||
bytes = value * 1024;
|
||||
break;
|
||||
case 'MB':
|
||||
bytes = value * 1024 * 1024;
|
||||
break;
|
||||
case 'GB':
|
||||
bytes = value * 1024 * 1024 * 1024;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid size unit');
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
|
||||
*
|
||||
* @param time 时间数
|
||||
* @param unit time对应的单位
|
||||
* @returns
|
||||
*/
|
||||
export function formatTime(time: number, unit: string = 's') {
|
||||
const units: any = {
|
||||
y: 31536000,
|
||||
M: 2592000,
|
||||
d: 86400,
|
||||
h: 3600,
|
||||
m: 60,
|
||||
s: 1,
|
||||
};
|
||||
|
||||
if (!units[unit]) {
|
||||
return 'Invalid unit';
|
||||
}
|
||||
|
||||
let seconds = time * units[unit];
|
||||
let result = '';
|
||||
|
||||
const timeUnits = Object.entries(units).map(([unit, duration]) => {
|
||||
const value = Math.floor(seconds / duration);
|
||||
seconds %= duration;
|
||||
return { value, unit };
|
||||
});
|
||||
|
||||
timeUnits.forEach(({ value, unit }) => {
|
||||
if (value > 0) {
|
||||
result += `${value}${unit} `;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* formatAxis(new Date()) // 上午好
|
||||
*/
|
||||
export function formatAxis(param: any) {
|
||||
let hour: number = new Date(param).getHours();
|
||||
if (hour < 6) return '凌晨好';
|
||||
else if (hour < 9) return '早上好';
|
||||
else if (hour < 12) return '上午好';
|
||||
else if (hour < 14) return '中午好';
|
||||
else if (hour < 17) return '下午好';
|
||||
else if (hour < 19) return '傍晚好';
|
||||
else if (hour < 22) return '晚上好';
|
||||
else return '夜里好';
|
||||
}
|
||||
46
frontend/src/common/utils/loading.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { nextTick } from 'vue';
|
||||
import '@/theme/loading.scss';
|
||||
|
||||
/**
|
||||
* 页面全局 Loading
|
||||
* @method start 创建 loading
|
||||
* @method done 移除 loading
|
||||
*/
|
||||
export const NextLoading = {
|
||||
// 创建 loading
|
||||
start: () => {
|
||||
const bodys: Element = document.body;
|
||||
const div = <HTMLElement>document.createElement('div');
|
||||
div.setAttribute('class', 'loading-next');
|
||||
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.innerHTML = htmls;
|
||||
bodys.insertBefore(div, bodys.childNodes[0]);
|
||||
},
|
||||
// 移除 loading
|
||||
done: (time: number = 1000) => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
const el = <HTMLElement>document.querySelector('.loading-next');
|
||||
el?.parentNode?.removeChild(el);
|
||||
}, time);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
8
frontend/src/common/utils/mitt.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// https://www.npmjs.com/package/mitt
|
||||
import mitt, { Emitter } from 'mitt';
|
||||
|
||||
// 类型
|
||||
const emitter: Emitter<any> = mitt<any>();
|
||||
|
||||
// 导出
|
||||
export default emitter;
|
||||
56
frontend/src/common/utils/object.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 根据对象访问路径,获取对应的值
|
||||
*
|
||||
* @param obj 对象,如 {user: {name: 'xxx'}, orderNo: 1212211, products: [{id: 12}]}
|
||||
* @param path 访问路径,如 orderNo 或者 user.name 或者product[0].id
|
||||
* @returns 路径对应的值
|
||||
*/
|
||||
export function getValueByPath(obj: any, path: string) {
|
||||
const keys = path.split('.');
|
||||
let result = obj;
|
||||
for (let key of keys) {
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
// 如果是字符串,则尝试使用json解析
|
||||
if (typeof result == 'string') {
|
||||
try {
|
||||
result = JSON.parse(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (typeof result !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
// 处理包含数组索引的情况
|
||||
const arrayKey = key.substring(0, key.indexOf('['));
|
||||
const matchIndex = key.match(/\[(.*?)\]/);
|
||||
|
||||
if (!matchIndex) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const index = parseInt(matchIndex[1]);
|
||||
|
||||
let arrValue = result[arrayKey];
|
||||
if (typeof arrValue == 'string') {
|
||||
try {
|
||||
arrValue = JSON.parse(arrValue);
|
||||
} catch (e) {
|
||||
result = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result = Array.isArray(arrValue) ? arrValue[index] : undefined;
|
||||
} else {
|
||||
result = result[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
// 字体图标 url
|
||||
const cssCdnUrlList: Array<string> = [
|
||||
'//at.alicdn.com/t/font_2298093_ysc3z187xhh.css',
|
||||
'//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css',
|
||||
];
|
||||
const cssCdnUrlList: Array<string> = [];
|
||||
// 第三方 js url
|
||||
const jsCdnUrlList: Array<string> = [];
|
||||
|
||||
// 动态设置字体图标
|
||||
// 动态批量设置字体图标
|
||||
export function setCssCdn() {
|
||||
if (cssCdnUrlList.length <= 0) return false;
|
||||
cssCdnUrlList.map((v) => {
|
||||
@@ -18,7 +15,7 @@ export function setCssCdn() {
|
||||
});
|
||||
}
|
||||
|
||||
// 批量设置第三方js
|
||||
// 动态批量设置第三方js
|
||||
export function setJsCdn() {
|
||||
if (jsCdnUrlList.length <= 0) return false;
|
||||
jsCdnUrlList.map((v) => {
|
||||
@@ -28,11 +25,17 @@ export function setJsCdn() {
|
||||
});
|
||||
}
|
||||
|
||||
// 设置执行函数
|
||||
/**
|
||||
* 批量设置字体图标、动态js
|
||||
* @method cssCdn 动态批量设置字体图标
|
||||
* @method jsCdn 动态批量设置第三方js
|
||||
*/
|
||||
const setIntroduction = {
|
||||
// 设置css
|
||||
cssCdn: () => {
|
||||
setCssCdn();
|
||||
},
|
||||
// 设置js
|
||||
jsCdn: () => {
|
||||
setJsCdn();
|
||||
},
|
||||
133
frontend/src/common/utils/storage.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { randomUuid } from './string';
|
||||
|
||||
const TokenKey = 'm-token';
|
||||
const RefreshTokenKey = 'm-refresh-token';
|
||||
const UserKey = 'm-user';
|
||||
const TagViewsKey = 'm-tagViews';
|
||||
const ClientIdKey = 'm-clientId';
|
||||
|
||||
// 获取请求token
|
||||
export function getToken(): string {
|
||||
return getLocal(TokenKey);
|
||||
}
|
||||
|
||||
// 保存用户访问token
|
||||
export function saveToken(token: string) {
|
||||
setLocal(TokenKey, token);
|
||||
}
|
||||
|
||||
export function getRefreshToken(): string {
|
||||
return getLocal(RefreshTokenKey);
|
||||
}
|
||||
|
||||
export function saveRefreshToken(refreshToken: string) {
|
||||
return setLocal(RefreshTokenKey, refreshToken);
|
||||
}
|
||||
|
||||
// 获取登录用户基础信息
|
||||
export function getUser() {
|
||||
return getLocal(UserKey);
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
export function saveUser(userinfo: any) {
|
||||
setLocal(UserKey, userinfo);
|
||||
}
|
||||
|
||||
export function saveThemeConfig(themeConfig: any) {
|
||||
setLocal('themeConfig', themeConfig);
|
||||
}
|
||||
|
||||
export function getThemeConfig() {
|
||||
return getLocal('themeConfig');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前登录用户相关信息
|
||||
*/
|
||||
export function clearUser() {
|
||||
removeLocal(TokenKey);
|
||||
removeLocal(UserKey);
|
||||
removeLocal(RefreshTokenKey);
|
||||
}
|
||||
|
||||
export function getTagViews() {
|
||||
return getSession(TagViewsKey);
|
||||
}
|
||||
|
||||
export function setTagViews(tagViews: Array<object>) {
|
||||
setSession(TagViewsKey, tagViews);
|
||||
}
|
||||
|
||||
export function removeTagViews() {
|
||||
removeSession(TagViewsKey);
|
||||
}
|
||||
|
||||
// 获取客户端UUID
|
||||
export function getClientId(): string {
|
||||
let uuid = getSession(ClientIdKey);
|
||||
if (uuid == null) {
|
||||
uuid = randomUuid();
|
||||
setSession(ClientIdKey, uuid);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// 1. localStorage
|
||||
// 设置永久缓存
|
||||
export function setLocal(key: string, val: any) {
|
||||
if (typeof val == 'object') {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
// 获取永久缓存
|
||||
export function getLocal(key: string) {
|
||||
let val: any = window.localStorage.getItem(key);
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除永久缓存
|
||||
export function removeLocal(key: string) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
// 移除全部永久缓存
|
||||
export function clearLocal() {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
// 2. sessionStorage
|
||||
// 设置临时缓存
|
||||
export function setSession(key: string, val: any) {
|
||||
if (typeof val == 'object') {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
window.sessionStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
// 获取临时缓存
|
||||
export function getSession(key: string) {
|
||||
let val: any = window.sessionStorage.getItem(key);
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除临时缓存
|
||||
export function removeSession(key: string) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
// 移除全部临时缓存
|
||||
export function clearSession() {
|
||||
clearUser();
|
||||
window.sessionStorage.clear();
|
||||
}
|
||||
216
frontend/src/common/utils/string.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import Clipboard from 'clipboard';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 模板字符串解析,如:template = 'hahaha{name}_{id}' ,param = {name: 'hh', id: 1}
|
||||
* 解析后为 hahahahh_1
|
||||
* @param template 模板字符串
|
||||
* @param param 参数占位符
|
||||
* @returns
|
||||
*/
|
||||
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];
|
||||
if (value != null || value != undefined) {
|
||||
return value;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
// 首字符头像
|
||||
export function letterAvatar(name: string, size = 60, color = '') {
|
||||
name = name || '';
|
||||
size = size || 60;
|
||||
var colours = [
|
||||
'#1abc9c',
|
||||
'#2ecc71',
|
||||
'#3498db',
|
||||
'#9b59b6',
|
||||
'#34495e',
|
||||
'#16a085',
|
||||
'#27ae60',
|
||||
'#2980b9',
|
||||
'#8e44ad',
|
||||
'#2c3e50',
|
||||
'#f1c40f',
|
||||
'#e67e22',
|
||||
'#e74c3c',
|
||||
'#00bcd4',
|
||||
'#95a5a6',
|
||||
'#f39c12',
|
||||
'#d35400',
|
||||
'#c0392b',
|
||||
'#bdc3c7',
|
||||
'#7f8c8d',
|
||||
],
|
||||
nameSplit = String(name).split(' '),
|
||||
initials,
|
||||
charIndex,
|
||||
colourIndex,
|
||||
canvas,
|
||||
context,
|
||||
dataURI;
|
||||
|
||||
if (nameSplit.length == 1) {
|
||||
initials = nameSplit[0] ? nameSplit[0].charAt(0) : '?';
|
||||
} else {
|
||||
initials = nameSplit[0].charAt(0) + nameSplit[1].charAt(0);
|
||||
}
|
||||
if (window.devicePixelRatio) {
|
||||
size = size * window.devicePixelRatio;
|
||||
}
|
||||
initials = initials.toLocaleUpperCase();
|
||||
charIndex = (initials == '?' ? 72 : initials.charCodeAt(0)) - 64;
|
||||
colourIndex = charIndex % 20;
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
context = canvas.getContext('2d') as any;
|
||||
|
||||
context.fillStyle = color ? color : colours[colourIndex - 1];
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.font = Math.round(canvas.width / 2) + "px 'Microsoft Yahei'";
|
||||
context.textAlign = 'center';
|
||||
context.fillStyle = '#FFF';
|
||||
context.fillText(initials, size / 2, size / 1.5);
|
||||
dataURI = canvas.toDataURL();
|
||||
canvas = null;
|
||||
return dataURI;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文本所占用的宽度(px) -> 该种方式较为准确
|
||||
* 使用span标签包裹内容,然后计算span的宽度 width: px
|
||||
* @param str
|
||||
*/
|
||||
export function getTextWidth(str: string) {
|
||||
let width = 0;
|
||||
let html = document.createElement('span');
|
||||
html.innerText = str;
|
||||
html.className = 'getTextWidth';
|
||||
document?.querySelector('body')?.appendChild(html);
|
||||
width = (document?.querySelector('.getTextWidth') as any).offsetWidth;
|
||||
document?.querySelector('.getTextWidth')?.remove();
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns uuid
|
||||
*/
|
||||
export function randomUuid() {
|
||||
return uuidv1();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝文本至剪贴板
|
||||
* @param txt 需要拷贝到剪贴板的文本
|
||||
* @param selector click事件对应的元素selector,默认为 #copyValue
|
||||
* @returns
|
||||
*/
|
||||
export async function copyToClipboard(txt: string, selector: string = '#copyValue') {
|
||||
// navigator clipboard 需要https等安全上下文
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard 向剪贴板写文本
|
||||
try {
|
||||
// 拷贝单元格数据
|
||||
await navigator.clipboard.writeText(txt);
|
||||
ElMessage.success('复制成功');
|
||||
} catch (e: any) {
|
||||
ElMessage.error('复制失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboard = new Clipboard(selector, {
|
||||
text: function () {
|
||||
return txt;
|
||||
},
|
||||
});
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
clipboard.on('error', () => {
|
||||
// 不支持复制
|
||||
ElMessage.error('该浏览器不支持自动复制');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
|
||||
keyword = keyword?.toLowerCase();
|
||||
return fields.filter((field) => {
|
||||
for (let valueExtractFunc of valueExtractFuncs) {
|
||||
const value = valueExtractFunc(field)?.toLowerCase();
|
||||
if (isPrefixSubsequence(keyword, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false
|
||||
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
|
||||
* @param targetTemplate 目标模板
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
|
||||
let i = 0; // 指向prefix的索引
|
||||
let j = 0; // 指向targetTemplate的索引
|
||||
|
||||
while (i < prefix.length && j < targetTemplate.length) {
|
||||
if (prefix[i] === targetTemplate[j]) {
|
||||
// 字符匹配,两个指针都向前移动
|
||||
i++;
|
||||
}
|
||||
j++; // 目标字符串指针始终向前移动
|
||||
}
|
||||
|
||||
// 如果prefix的所有字符都被找到,返回true
|
||||
return i === prefix.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机密码
|
||||
* @param length 密码长度
|
||||
*/
|
||||
export function randomPassword(length = 10) {
|
||||
const lowerCase = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const upperCase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const numbers = '0123456789';
|
||||
const specialChars = '!@#$%^&*()-_=+[]{}|;:,.<>?';
|
||||
|
||||
// 确保每个类别至少包含一个字符
|
||||
let password = [getRandomChar(lowerCase), getRandomChar(upperCase), getRandomChar(numbers), getRandomChar(specialChars)];
|
||||
|
||||
// 剩余字符从所有字符集中随机选择
|
||||
const allChars = lowerCase + upperCase + numbers + specialChars;
|
||||
for (let i = 4; i < length; i++) {
|
||||
password.push(getRandomChar(allChars));
|
||||
}
|
||||
|
||||
// 打乱数组顺序以增加随机性
|
||||
shuffleArray(password);
|
||||
|
||||
return password.join('');
|
||||
}
|
||||
|
||||
function getRandomChar(charSet: string) {
|
||||
const randomIndex = Math.floor(Math.random() * charSet.length);
|
||||
return charSet[randomIndex];
|
||||
}
|
||||
|
||||
function shuffleArray(array: string[]) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
import { nextTick } from 'vue';
|
||||
import * as svg from '@element-plus/icons-vue';
|
||||
import iconfontJson from '@/assets/iconfont/iconfont.json';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
/**
|
||||
* 导出全局注册 element plus svg 图标
|
||||
* @param app vue 实例
|
||||
* @description 使用:https://element-plus.gitee.io/zh-CN/component/icon.html
|
||||
*/
|
||||
export function registElSvgIcon(app: any) {
|
||||
const icons = svg as any;
|
||||
for (const i in icons) {
|
||||
app.component(`${icons[i].name}`, icons[i]);
|
||||
}
|
||||
app.component('SvgIcon', SvgIcon);
|
||||
}
|
||||
|
||||
// 获取阿里字体图标
|
||||
const getAlicdnIconfont = () => {
|
||||
@@ -9,7 +24,8 @@ const getAlicdnIconfont = () => {
|
||||
let sheetsList = [];
|
||||
let sheetsIconList = [];
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
if (styles[i].href && styles[i].href.indexOf('at.alicdn.com') > -1) {
|
||||
console.log(styles[i]);
|
||||
if (styles[i].href && styles[i].href.indexOf('iconfont') > -1) {
|
||||
sheetsList.push(styles[i]);
|
||||
}
|
||||
}
|
||||
@@ -28,19 +44,29 @@ const getAlicdnIconfont = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 获取本地阿里icons
|
||||
const getLocalAliIconfont = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
nextTick(() => {
|
||||
const prefix = iconfontJson.css_prefix_text;
|
||||
resolve(iconfontJson.glyphs.map((x: any) => prefix + x.font_class));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化获取 css 样式,获取 element plus 自带图标
|
||||
const elementPlusIconfont = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
nextTick(() => {
|
||||
const icons = svg as any;
|
||||
const sheetsIconList = [];
|
||||
for (const i in icons) {
|
||||
sheetsIconList.push(`${icons[i].name}`);
|
||||
}
|
||||
if (sheetsIconList.length > 0) resolve(sheetsIconList);
|
||||
else reject('未获取到值,请刷新重试');
|
||||
});
|
||||
});
|
||||
nextTick(() => {
|
||||
const icons = svg as any;
|
||||
const sheetsIconList = [];
|
||||
for (const i in icons) {
|
||||
sheetsIconList.push(`${icons[i].name}`);
|
||||
}
|
||||
if (sheetsIconList.length > 0) resolve(sheetsIconList);
|
||||
else reject('未获取到值,请刷新重试');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化获取 css 样式,这里使用 fontawesome 的图标
|
||||
@@ -76,9 +102,9 @@ const awesomeIconfont = () => {
|
||||
|
||||
// 定义导出方法集合
|
||||
const initIconfont = {
|
||||
// ali: () => {
|
||||
// return getAlicdnIconfont();
|
||||
// },
|
||||
ali: () => {
|
||||
return getLocalAliIconfont();
|
||||
},
|
||||
ele: () => {
|
||||
return elementPlusIconfont();
|
||||
},
|
||||
@@ -163,8 +163,7 @@ export function verifyPasswordStrength(val: string) {
|
||||
// 中:字母+数字,字母+特殊字符,数字+特殊字符
|
||||
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 = '强';
|
||||
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '强';
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
@@ -172,11 +171,7 @@ export function verifyPasswordStrength(val: string) {
|
||||
// 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
|
||||
)
|
||||
)
|
||||
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;
|
||||
13
frontend/src/common/utils/url.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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];
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export interface ViteEnv {
|
||||
VITE_PORT: number;
|
||||
VITE_OPEN: boolean;
|
||||
VITE_PUBLIC_PATH: string;
|
||||
VITE_EDITOR: string;
|
||||
}
|
||||
|
||||
export function loadEnv(): ViteEnv {
|
||||
66
frontend/src/components/Grid/components/GridItem.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div v-show="isShow" :style="style">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="GridItem">
|
||||
import { computed, inject, Ref, ref, useAttrs, watch } from 'vue';
|
||||
import { BreakPoint, Responsive } from '../interface/index';
|
||||
|
||||
type Props = {
|
||||
offset?: number;
|
||||
span?: number;
|
||||
suffix?: boolean;
|
||||
xs?: Responsive;
|
||||
sm?: Responsive;
|
||||
md?: Responsive;
|
||||
lg?: Responsive;
|
||||
xl?: Responsive;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
offset: 0,
|
||||
span: 1,
|
||||
suffix: false,
|
||||
xs: undefined,
|
||||
sm: undefined,
|
||||
md: undefined,
|
||||
lg: undefined,
|
||||
xl: undefined,
|
||||
});
|
||||
|
||||
const attrs = useAttrs() as { index: string };
|
||||
const isShow = ref(true);
|
||||
|
||||
// 注入断点
|
||||
const breakPoint = inject<Ref<BreakPoint>>('breakPoint', ref('xl'));
|
||||
const shouldHiddenIndex = inject<Ref<number>>('shouldHiddenIndex', ref(-1));
|
||||
watch(
|
||||
() => [shouldHiddenIndex.value, breakPoint.value],
|
||||
(n) => {
|
||||
if (attrs.index) {
|
||||
isShow.value = !(n[0] !== -1 && parseInt(attrs.index) >= Number(n[0]));
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const gap = inject('gap', 0);
|
||||
const cols = inject('cols', ref(4));
|
||||
const style = computed(() => {
|
||||
let span = props[breakPoint.value]?.span ?? props.span;
|
||||
let offset = props[breakPoint.value]?.offset ?? props.offset;
|
||||
if (props.suffix) {
|
||||
return {
|
||||
gridColumnStart: cols.value - span - offset + 1,
|
||||
gridColumnEnd: `span ${span + offset}`,
|
||||
marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
gridColumn: `span ${span + offset > cols.value ? cols.value : span + offset}/span ${span + offset > cols.value ? cols.value : span + offset}`,
|
||||
marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset',
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
159
frontend/src/components/Grid/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div :style="style">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Grid">
|
||||
import { ref, watch, useSlots, computed, provide, onBeforeMount, onMounted, onUnmounted, onDeactivated, onActivated, VNodeArrayChildren, VNode } from 'vue';
|
||||
import type { BreakPoint } from './interface/index';
|
||||
|
||||
type Props = {
|
||||
cols?: number | Record<BreakPoint, number>;
|
||||
collapsed?: boolean;
|
||||
collapsedRows?: number;
|
||||
gap?: [number, number] | number;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
cols: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),
|
||||
collapsed: false,
|
||||
collapsedRows: 1,
|
||||
gap: 0,
|
||||
});
|
||||
|
||||
onBeforeMount(() => props.collapsed && findIndex());
|
||||
onMounted(() => {
|
||||
resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
onActivated(() => {
|
||||
resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent);
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
});
|
||||
onDeactivated(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
});
|
||||
|
||||
// 监听屏幕变化
|
||||
const resize = (e: UIEvent) => {
|
||||
let width = (e.target as Window).innerWidth;
|
||||
switch (!!width) {
|
||||
case width < 768:
|
||||
breakPoint.value = 'xs';
|
||||
break;
|
||||
case width >= 768 && width < 992:
|
||||
breakPoint.value = 'sm';
|
||||
break;
|
||||
case width >= 992 && width < 1200:
|
||||
breakPoint.value = 'md';
|
||||
break;
|
||||
case width >= 1200 && width < 1920:
|
||||
breakPoint.value = 'lg';
|
||||
break;
|
||||
case width >= 1920:
|
||||
breakPoint.value = 'xl';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 注入 gap 间距
|
||||
provide('gap', Array.isArray(props.gap) ? props.gap[0] : props.gap);
|
||||
|
||||
// 注入响应式断点
|
||||
let breakPoint = ref<BreakPoint>('xl');
|
||||
provide('breakPoint', breakPoint);
|
||||
|
||||
// 注入要开始折叠的 index
|
||||
const hiddenIndex = ref(-1);
|
||||
provide('shouldHiddenIndex', hiddenIndex);
|
||||
|
||||
// 注入 cols
|
||||
const gridCols = computed(() => {
|
||||
if (typeof props.cols === 'object') return props.cols[breakPoint.value] ?? props.cols;
|
||||
return props.cols;
|
||||
});
|
||||
provide('cols', gridCols);
|
||||
|
||||
// 寻找需要开始折叠的字段 index
|
||||
const slots = useSlots().default!();
|
||||
|
||||
const findIndex = () => {
|
||||
let fields: VNodeArrayChildren = [];
|
||||
let suffix: VNode | null = null;
|
||||
slots.forEach((slot: any) => {
|
||||
// suffix
|
||||
if (typeof slot.type === 'object' && slot.type.__name === 'GridItem' && slot.props?.suffix !== undefined) {
|
||||
suffix = slot;
|
||||
}
|
||||
// slot children
|
||||
if (typeof slot.type === 'symbol' && Array.isArray(slot.children)) {
|
||||
fields.push(...slot.children);
|
||||
}
|
||||
});
|
||||
|
||||
// 计算 suffix 所占用的列
|
||||
let suffixCols = 0;
|
||||
if (suffix) {
|
||||
suffixCols =
|
||||
((suffix as VNode).props![breakPoint.value]?.span ?? (suffix as VNode).props?.span ?? 1) +
|
||||
((suffix as VNode).props![breakPoint.value]?.offset ?? (suffix as VNode).props?.offset ?? 0);
|
||||
}
|
||||
|
||||
try {
|
||||
let find = false;
|
||||
fields.reduce((prev = 0, current, index) => {
|
||||
prev +=
|
||||
((current as VNode)!.props![breakPoint.value]?.span ?? (current as VNode)!.props?.span ?? 1) +
|
||||
((current as VNode)!.props![breakPoint.value]?.offset ?? (current as VNode)!.props?.offset ?? 0);
|
||||
if (Number(prev) > props.collapsedRows * gridCols.value - suffixCols) {
|
||||
hiddenIndex.value = index;
|
||||
find = true;
|
||||
throw 'find it';
|
||||
}
|
||||
return prev;
|
||||
}, 0);
|
||||
if (!find) hiddenIndex.value = -1;
|
||||
} catch (e) {
|
||||
// console.warn(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 断点变化时执行 findIndex
|
||||
watch(
|
||||
() => breakPoint.value,
|
||||
() => {
|
||||
if (props.collapsed) findIndex();
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 collapsed
|
||||
watch(
|
||||
() => props.collapsed,
|
||||
(value) => {
|
||||
if (value) return findIndex();
|
||||
hiddenIndex.value = -1;
|
||||
}
|
||||
);
|
||||
|
||||
// 设置间距
|
||||
const gridGap = computed(() => {
|
||||
if (typeof props.gap === 'number') return `${props.gap}px`;
|
||||
if (Array.isArray(props.gap)) return `${props.gap[1]}px ${props.gap[0]}px`;
|
||||
return 'unset';
|
||||
});
|
||||
|
||||
// 设置 style
|
||||
const style = computed(() => {
|
||||
return {
|
||||
display: 'grid',
|
||||
gridGap: gridGap.value,
|
||||
gridTemplateColumns: `repeat(${gridCols.value}, minmax(0, 1fr))`,
|
||||
};
|
||||
});
|
||||
|
||||
defineExpose({ breakPoint });
|
||||
</script>
|
||||
6
frontend/src/components/Grid/interface/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type BreakPoint = "xs" | "sm" | "md" | "lg" | "xl";
|
||||
|
||||
export type Responsive = {
|
||||
span?: number;
|
||||
offset?: number;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<component
|
||||
:is="item?.render ?? `el-${item.type}`"
|
||||
v-bind="{ ...handleSearchProps, ...placeholder, clearable: true }"
|
||||
v-on="{ ...handleEvents }"
|
||||
v-model.trim="itemValue"
|
||||
:data="item.type === 'tree-select' ? item.options : []"
|
||||
:options="['cascader', 'select-v2'].includes(item.type!) ? item.options : []"
|
||||
>
|
||||
<template v-if="item.type === 'cascader'" #default="{ data }">
|
||||
<span>{{ data[fieldNames.label] }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'select'">
|
||||
<component
|
||||
:is="`el-option`"
|
||||
v-for="(col, index) in item.options"
|
||||
:key="index"
|
||||
:label="$t(col[fieldNames.label])"
|
||||
:value="col[fieldNames.value]"
|
||||
></component>
|
||||
</template>
|
||||
|
||||
<slot v-else></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="SearchFormItem">
|
||||
import { computed } from 'vue';
|
||||
import { SearchItem } from '../index';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface SearchFormItemProps {
|
||||
item: SearchItem;
|
||||
}
|
||||
const props = defineProps<SearchFormItemProps>();
|
||||
|
||||
const itemValue = defineModel('modelValue');
|
||||
|
||||
// 判断 fieldNames 设置 label && value && children 的 key 值
|
||||
const fieldNames = computed(() => {
|
||||
return {
|
||||
label: props.item?.fieldNames?.label ?? 'label',
|
||||
value: props.item?.fieldNames?.value ?? 'value',
|
||||
children: props.item.fieldNames?.children ?? 'children',
|
||||
};
|
||||
});
|
||||
|
||||
// 处理透传的 searchProps (type 为 tree-select、cascader 的时候需要给下默认 label && value && children)
|
||||
const handleSearchProps = computed(() => {
|
||||
const label = fieldNames.value.label;
|
||||
const value = fieldNames.value.value;
|
||||
const children = fieldNames.value.children;
|
||||
const searchEl = props.item?.type;
|
||||
let searchProps = props.item?.props ?? {};
|
||||
if (searchEl === 'tree-select') {
|
||||
searchProps = { ...searchProps, props: { ...searchProps.props, label, children }, nodeKey: value };
|
||||
}
|
||||
if (searchEl === 'cascader') {
|
||||
searchProps = { ...searchProps, props: { ...searchProps.props, label, value, children } };
|
||||
}
|
||||
return searchProps;
|
||||
});
|
||||
|
||||
// 处理透传的 事件
|
||||
const handleEvents = computed(() => {
|
||||
let itemEvents = props.item?.events ?? {};
|
||||
return itemEvents;
|
||||
});
|
||||
|
||||
// 处理默认 placeholder
|
||||
const placeholder = computed(() => {
|
||||
const search = props.item;
|
||||
const label = t(search.label);
|
||||
if (['datetimerange', 'daterange', 'monthrange'].includes(search?.props?.type) || search?.props?.isRange) {
|
||||
return {
|
||||
rangeSeparator: search?.props?.rangeSeparator ?? '至',
|
||||
startPlaceholder: search?.props?.startPlaceholder ?? '开始时间',
|
||||
endPlaceholder: search?.props?.endPlaceholder ?? '结束时间',
|
||||
};
|
||||
}
|
||||
|
||||
const placeholder =
|
||||
search?.props?.placeholder ?? (search?.type?.includes('input') ? t('common.pleaseInput', { label }) : t('common.pleaseSelect', { label }));
|
||||
return { placeholder: t(placeholder) };
|
||||
});
|
||||
</script>
|
||||
318
frontend/src/components/SearchForm/index.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import Api from '@/common/Api';
|
||||
import { VNode, ref, toValue } from 'vue';
|
||||
|
||||
export type FieldNamesProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: string;
|
||||
};
|
||||
|
||||
export type SearchItemType =
|
||||
| 'input'
|
||||
| 'input-number'
|
||||
| 'select'
|
||||
| 'select-v2'
|
||||
| 'tree-select'
|
||||
| 'cascader'
|
||||
| 'date-picker'
|
||||
| 'time-picker'
|
||||
| 'time-select'
|
||||
| 'switch'
|
||||
| 'slider';
|
||||
|
||||
/**
|
||||
* 表单组件可选项的api信息
|
||||
*/
|
||||
export class OptionsApi {
|
||||
/**
|
||||
* 请求获取options的api
|
||||
*/
|
||||
api: Api;
|
||||
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
params: any;
|
||||
|
||||
/**
|
||||
* 是否立即执行,否则在组件focus事件中获取
|
||||
*/
|
||||
immediate: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否只获取一次,即若以获取则不继续调用该api
|
||||
*/
|
||||
once: boolean = true;
|
||||
|
||||
/**
|
||||
* 转换函数,主要用于将响应的api结果转换为满足组件options的结构
|
||||
*/
|
||||
convertFn: (apiResp: any) => any;
|
||||
|
||||
// remote: boolean = false;
|
||||
|
||||
/**
|
||||
* 远程方法参数属性字段,存在该值,则说明使用remote-method进行远程搜索
|
||||
*/
|
||||
remoteMethodParamProp: string;
|
||||
|
||||
withConvertFn(fn: (apiResp: any) => any) {
|
||||
this.convertFn = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即获取该可选值
|
||||
* @returns
|
||||
*/
|
||||
withImmediate() {
|
||||
this.immediate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为非一次性api,即每次组件focus获取的时候都允许重新获取options
|
||||
* @returns this
|
||||
*/
|
||||
withNoOnce() {
|
||||
this.once = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否使用select的remote方式远程搜索调用
|
||||
* @param remoteReqParamKey remote请求参数对应的prop,需要将输入的value赋值给params[paramProp]进行远程搜索
|
||||
*/
|
||||
isRemote(paramProp: string) {
|
||||
this.remoteMethodParamProp = paramProp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用api获取组件可选项
|
||||
* @returns 组件可选项信息
|
||||
*/
|
||||
async getOptions() {
|
||||
let res = await this.api.request(toValue(this.params));
|
||||
if (this.convertFn) {
|
||||
res = this.convertFn(res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static new(api: Api, params: any): OptionsApi {
|
||||
const oa = new OptionsApi();
|
||||
oa.api = api;
|
||||
oa.params = params;
|
||||
return oa;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索项
|
||||
*/
|
||||
export class SearchItem {
|
||||
/**
|
||||
* 属性字段
|
||||
*/
|
||||
prop: string;
|
||||
|
||||
/**
|
||||
* 当前项搜索框的 label
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* 表单项类型,input、select、date等
|
||||
*/
|
||||
type: SearchItemType;
|
||||
|
||||
/**
|
||||
* select等组件的可选值
|
||||
*/
|
||||
options: any;
|
||||
|
||||
/**
|
||||
* 获取可选项的api信息
|
||||
*/
|
||||
optionsApi: OptionsApi;
|
||||
|
||||
/**
|
||||
* 插槽名
|
||||
*/
|
||||
slot: string;
|
||||
|
||||
/**
|
||||
* 搜索项参数,根据 element plus 官方文档来传递,该属性所有值会透传到组件
|
||||
*/
|
||||
props?: any;
|
||||
|
||||
/**
|
||||
* 搜索项事件,根据 element plus 官方文档来传递,该属性所有值会透传到组件
|
||||
*/
|
||||
events?: any;
|
||||
|
||||
/**
|
||||
* 搜索提示
|
||||
*/
|
||||
tooltip?: string;
|
||||
|
||||
/**
|
||||
* 搜索项所占用的列数,默认为 1 列
|
||||
*/
|
||||
span?: number;
|
||||
|
||||
/**
|
||||
* 搜索字段左侧偏移列数
|
||||
*/
|
||||
offset?: number;
|
||||
|
||||
/**
|
||||
* 指定 label && value && children 的 key 值,用于select等类型组件
|
||||
*/
|
||||
fieldNames: FieldNamesProps;
|
||||
|
||||
/**
|
||||
* 自定义搜索内容渲染(tsx语法)
|
||||
*/
|
||||
render?: (scope: any) => VNode;
|
||||
|
||||
constructor(prop: string, label: string) {
|
||||
this.prop = prop;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
static new(prop: string, label: string): SearchItem {
|
||||
return new SearchItem(prop, label);
|
||||
}
|
||||
|
||||
static input(prop: string, label: string): SearchItem {
|
||||
const tq = new SearchItem(prop, label);
|
||||
tq.type = 'input';
|
||||
return tq;
|
||||
}
|
||||
|
||||
static select(prop: string, label: string): SearchItem {
|
||||
const tq = new SearchItem(prop, label);
|
||||
tq.type = 'select';
|
||||
tq.withOneProps('filterable', true);
|
||||
return tq;
|
||||
}
|
||||
|
||||
static datePicker(prop: string, label: string): SearchItem {
|
||||
const tq = new SearchItem(prop, label);
|
||||
tq.type = 'date-picker';
|
||||
return tq;
|
||||
}
|
||||
|
||||
static slot(prop: string, label: string, slotName: string): SearchItem {
|
||||
const tq = new SearchItem(prop, label);
|
||||
tq.slot = slotName;
|
||||
return tq;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为组件设置一个props属性
|
||||
* @param propsKey 属性key
|
||||
* @param propsValue 属性value
|
||||
* @returns
|
||||
*/
|
||||
withOneProps(propsKey: string, propsValue: any): SearchItem {
|
||||
if (!this.props) {
|
||||
this.props = {};
|
||||
}
|
||||
this.props[propsKey] = propsValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为组件传递组件自身的props属性 (根据 element plus 官方文档来传递,该属性所有值会透传到组件)
|
||||
* @returns this
|
||||
*/
|
||||
withProps(props: any = {}): SearchItem {
|
||||
this.props = props;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为组件传递组件自身事件函数
|
||||
* @param event 事件名称
|
||||
* @param fn 事件处理函数
|
||||
* @returns
|
||||
*/
|
||||
bindEvent(event: string, eventFn: any): SearchItem {
|
||||
if (!this.events) {
|
||||
this.events = {};
|
||||
}
|
||||
this.events[event] = eventFn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置枚举值用于选择等
|
||||
* @param enumValues 枚举值对象
|
||||
* @returns
|
||||
*/
|
||||
withEnum(enumValues: any): SearchItem {
|
||||
this.options = Object.values(enumValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置获取组件options可选项值的api配置
|
||||
* @param optionsApi 可选项api配置
|
||||
* @returns this
|
||||
*/
|
||||
withOptionsApi(optionsApi: OptionsApi): SearchItem {
|
||||
this.optionsApi = optionsApi;
|
||||
// 使用api获取组件可选项需要将options转为响应式,否则组件无法响应式获取组件可选项
|
||||
this.options = ref(null);
|
||||
|
||||
// 存在远程搜索请求参数prop,则为使用远程搜索可选项
|
||||
if (optionsApi.remoteMethodParamProp) {
|
||||
return this.withOneProps('remote', true).withOneProps('remote-method', async (value: any) => {
|
||||
if (!value) {
|
||||
this.options.value = [];
|
||||
return;
|
||||
}
|
||||
// 将输入的内容赋值为真实api请求参数中指定的属性字段
|
||||
optionsApi.params[optionsApi.remoteMethodParamProp] = value;
|
||||
this.options.value = await this.optionsApi.getOptions();
|
||||
});
|
||||
}
|
||||
|
||||
// 立即执行,则直接调用api获取并赋值options
|
||||
if (this.optionsApi.immediate) {
|
||||
this.optionsApi.getOptions().then((res) => {
|
||||
this.options.value = res;
|
||||
});
|
||||
} else {
|
||||
// 注册focus事件,在触发focus时赋值options
|
||||
this.bindEvent('focus', async () => {
|
||||
if (!toValue(this.options) || !optionsApi.once) {
|
||||
this.options.value = await this.optionsApi.getOptions();
|
||||
}
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
withSpan(span: number): SearchItem {
|
||||
this.span = span;
|
||||
return this;
|
||||
}
|
||||
|
||||
withOptions(options: any): SearchItem {
|
||||
this.options = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值placeholder
|
||||
* @param val placeholder
|
||||
* @returns
|
||||
*/
|
||||
withPlaceholder(val: string): SearchItem {
|
||||
return this.withOneProps('placeholder', val);
|
||||
}
|
||||
}
|
||||
138
frontend/src/components/SearchForm/index.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div v-if="items.length" class="search-form">
|
||||
<el-form ref="formRef" :model="searchParam" label-width="auto">
|
||||
<Grid ref="gridRef" :collapsed="collapsed" :gap="[20, 0]" :cols="searchCol">
|
||||
<GridItem v-for="(item, index) in items" :key="item.prop" v-bind="getResponsive(item)" :index="index">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-space :size="4">
|
||||
<span>{{ `${item?.label}` }}</span>
|
||||
<el-tooltip v-if="item.tooltip" :content="item?.tooltip" placement="top">
|
||||
<SvgIcon name="QuestionFilled" />
|
||||
</el-tooltip>
|
||||
</el-space>
|
||||
<span>:</span>
|
||||
</template>
|
||||
|
||||
<SearchFormItem @keyup.enter="handleItemKeyupEnter(item)" v-if="!item.slot" :item="item" v-model="searchParam[item.prop]" />
|
||||
|
||||
<slot v-else :name="item.slot"></slot>
|
||||
</el-form-item>
|
||||
</GridItem>
|
||||
<GridItem suffix>
|
||||
<div class="operation">
|
||||
<el-button type="primary" :icon="Search" @click="search" plain> 搜索 </el-button>
|
||||
<el-button :icon="Delete" @click="reset"> 重置 </el-button>
|
||||
<el-button v-if="showCollapse" type="primary" link class="search-isOpen" @click="collapsed = !collapsed">
|
||||
{{ collapsed ? '展开' : '合并' }}
|
||||
<el-icon class="el-icon--right">
|
||||
<component :is="collapsed ? ArrowDown : ArrowUp"></component>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="SearchForm">
|
||||
import { computed, ref } from 'vue';
|
||||
import { BreakPoint } from '@/components/Grid/interface/index';
|
||||
import { Delete, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import SearchFormItem from './components/SearchFormItem.vue';
|
||||
import Grid from '@/components/Grid/index.vue';
|
||||
import GridItem from '@/components/Grid/components/GridItem.vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { SearchItem } from './index';
|
||||
|
||||
interface ProTableProps {
|
||||
items: SearchItem[]; // 搜索配置项
|
||||
searchCol: number | Record<BreakPoint, number>;
|
||||
search: (params: any) => void; // 搜索方法
|
||||
reset: (params: any) => void; // 重置方法
|
||||
}
|
||||
|
||||
// 默认值
|
||||
const props = withDefaults(defineProps<ProTableProps>(), {
|
||||
items: () => [],
|
||||
modelValue: () => ({}),
|
||||
});
|
||||
|
||||
const searchParam: any = defineModel('modelValue');
|
||||
|
||||
// 获取响应式设置
|
||||
const getResponsive = (item: SearchItem) => {
|
||||
return {
|
||||
span: item?.span,
|
||||
offset: item.offset ?? 0,
|
||||
// xs: item.search?.xs,
|
||||
// sm: item.search?.sm,
|
||||
// md: item.search?.md,
|
||||
// lg: item.search?.lg,
|
||||
// xl: item.search?.xl,
|
||||
};
|
||||
};
|
||||
|
||||
// 是否默认折叠搜索项
|
||||
const collapsed = ref(true);
|
||||
|
||||
// 获取响应式断点
|
||||
const gridRef = ref();
|
||||
const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint);
|
||||
|
||||
// 判断是否显示 展开/合并 按钮
|
||||
const showCollapse = computed(() => {
|
||||
let show = false;
|
||||
props.items.reduce((prev, current: any) => {
|
||||
prev += (current![breakPoint.value]?.span ?? current?.span ?? 1) + (current![breakPoint.value]?.offset ?? current?.offset ?? 0);
|
||||
if (typeof props.searchCol !== 'number') {
|
||||
if (prev >= props.searchCol[breakPoint.value]) show = true;
|
||||
} else {
|
||||
if (prev >= props.searchCol) show = true;
|
||||
}
|
||||
return prev;
|
||||
}, 0);
|
||||
return show;
|
||||
});
|
||||
|
||||
const handleItemKeyupEnter = (item: SearchItem) => {
|
||||
if (item.type == 'input') {
|
||||
props.search(searchParam);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.search-form {
|
||||
padding: 18px 18px 0;
|
||||
margin-bottom: 10px;
|
||||
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 12px rgb(0 0 0 / 5%);
|
||||
|
||||
.el-form {
|
||||
.el-form-item__content > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 去除时间选择器上下 padding
|
||||
.el-range-editor.el-input__wrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.operation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
frontend/src/components/auth/auth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
|
||||
/**
|
||||
* 判断当前用户是否拥有指定权限
|
||||
* @param code 权限code
|
||||
* @returns
|
||||
*/
|
||||
export function hasPerm(code: string) {
|
||||
if (!code) {
|
||||
return true;
|
||||
}
|
||||
return useUserInfo().userInfo.permissions.some((v: any) => v === code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否拥有权限对象里对应的code
|
||||
* @returns {"xxx:save": true} key->permission code
|
||||
* @param permCodes
|
||||
*/
|
||||
export function hasPerms(permCodes: any[]) {
|
||||
const res = {} as { [key: string]: boolean };
|
||||
for (let permCode of permCodes) {
|
||||
if (hasPerm(permCode)) {
|
||||
res[permCode] = true;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from '/@/store/index.ts';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
export default {
|
||||
name: 'auth',
|
||||
props: {
|
||||
@@ -16,10 +16,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
// 获取 vuex 中的用户权限
|
||||
const getUserAuthBtnList = computed(() => {
|
||||
return store.state.userInfos.userInfos.authBtnList.some((v: any) => v === props.value);
|
||||
return useUserInfo().userInfo.authBtnList.some((v: any) => v === props.value);
|
||||
});
|
||||
return {
|
||||
getUserAuthBtnList,
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from '/@/store/index.ts';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import { judementSameArr } from '/@/utils/arrayOperation.ts';
|
||||
export default {
|
||||
name: 'authAll',
|
||||
@@ -17,10 +17,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
// 获取 vuex 中的用户权限
|
||||
const getUserAuthBtnList = computed(() => {
|
||||
return judementSameArr(props.value, store.state.userInfos.userInfos.authBtnList);
|
||||
return judementSameArr(props.value, useUserInfo().userInfo.authBtnList);
|
||||
});
|
||||
return {
|
||||
getUserAuthBtnList,
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from '/@/store/index.ts';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
export default {
|
||||
name: 'auths',
|
||||
props: {
|
||||
@@ -16,11 +16,10 @@ export default {
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
// 获取 vuex 中的用户权限
|
||||
const getUserAuthBtnList = computed(() => {
|
||||
let flag = false;
|
||||
store.state.userInfos.userInfos.authBtnList.map((val: any) => {
|
||||
useUserInfo().userInfo.authBtnList.map((val: any) => {
|
||||
props.value.map((v) => {
|
||||
if (val === v) flag = true;
|
||||
});
|
||||
59
frontend/src/components/contextmenu/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import Contextmenu from './index.vue';
|
||||
|
||||
class ContextmenuItem {
|
||||
clickId: any;
|
||||
|
||||
txt: string;
|
||||
|
||||
icon: string;
|
||||
|
||||
affix: boolean;
|
||||
|
||||
permission: string;
|
||||
|
||||
/**
|
||||
* 是否隐藏回调函数
|
||||
*/
|
||||
hideFunc: (data: any) => boolean;
|
||||
|
||||
onClickFunc: (data: any) => void;
|
||||
|
||||
constructor(clickId: any, txt: string) {
|
||||
this.clickId = clickId;
|
||||
this.txt = txt;
|
||||
}
|
||||
|
||||
withIcon(icon: string) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
withPermission(permission: string) {
|
||||
this.permission = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
withHideFunc(func: (data: any) => boolean) {
|
||||
this.hideFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
withOnClick(func: (data: any) => void) {
|
||||
this.onClickFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否隐藏
|
||||
* @param data 点击数据项
|
||||
* @returns
|
||||
*/
|
||||
isHide(data: any) {
|
||||
if (this.hideFunc) {
|
||||
return this.hideFunc(data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { Contextmenu, ContextmenuItem };
|
||||
201
frontend/src/components/contextmenu/index.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<transition @enter="onEnter" name="el-zoom-in-center">
|
||||
<div
|
||||
aria-hidden="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"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
<li
|
||||
:id="v.clickId"
|
||||
v-auth="v.permission"
|
||||
class="el-dropdown-menu__item"
|
||||
aria-disabled="false"
|
||||
tabindex="-1"
|
||||
:key="k"
|
||||
v-if="!v.affix && !v.isHide(state.item)"
|
||||
@click="onCurrentContextmenuClick(v)"
|
||||
>
|
||||
<SvgIcon :name="v.icon" />
|
||||
<span>{{ $t(v.txt) }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div v-if="state.arrowLeft > 0" class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutTagsViewContextmenu">
|
||||
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { ContextmenuItem } from './index';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
dropdown: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
items: {
|
||||
type: Array<ContextmenuItem>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
const emit = defineEmits(['currentContextmenuClick']);
|
||||
|
||||
const { width: vw, height: vh } = useWindowSize();
|
||||
|
||||
// 定义变量内容
|
||||
const state = reactive({
|
||||
isShow: false,
|
||||
dropdownList: [] as ContextmenuItem[],
|
||||
item: {} as any,
|
||||
arrowLeft: 10,
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 下拉菜单宽高
|
||||
let contextmenuWidth = 117;
|
||||
let contextmenuHeight = 117;
|
||||
// 下拉菜单元素
|
||||
let ele = null as any;
|
||||
|
||||
const onEnter = (el: any) => {
|
||||
if (ele || el.offsetHeight == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ele = el;
|
||||
contextmenuHeight = el.offsetHeight;
|
||||
contextmenuWidth = el.offsetWidth;
|
||||
setDropdowns(props.dropdown);
|
||||
};
|
||||
|
||||
const setDropdowns = (dropdown: any) => {
|
||||
let { x, y } = dropdown;
|
||||
|
||||
state.arrowLeft = 10;
|
||||
|
||||
// `Dropdown 下拉菜单` 的宽度
|
||||
if (x + contextmenuWidth > vw.value) {
|
||||
state.arrowLeft = contextmenuWidth - (vw.value - x);
|
||||
x = vw.value - contextmenuWidth - 5;
|
||||
}
|
||||
if (y + contextmenuHeight > vh.value) {
|
||||
y = vh.value - contextmenuHeight - 5;
|
||||
state.arrowLeft = 0;
|
||||
}
|
||||
|
||||
state.dropdown.x = x;
|
||||
state.dropdown.y = y;
|
||||
};
|
||||
|
||||
const allHide = computed(() => {
|
||||
for (let item of state.dropdownList) {
|
||||
if (!item.isHide(state.item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 当前项菜单点击
|
||||
const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
||||
// 存在点击事件,则触发该事件函数
|
||||
if (ci.onClickFunc) {
|
||||
ci.onClickFunc(state.item);
|
||||
}
|
||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||
};
|
||||
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
closeContextmenu();
|
||||
setTimeout(() => {
|
||||
state.isShow = true;
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeContextmenu = () => {
|
||||
state.isShow = false;
|
||||
};
|
||||
|
||||
// 监听页面监听进行右键菜单的关闭
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('click', closeContextmenu);
|
||||
state.dropdownList = props.items;
|
||||
});
|
||||
|
||||
// 页面卸载时,移除右键菜单监听事件
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('click', closeContextmenu);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.dropdown,
|
||||
() => {
|
||||
// 元素置为空,重新在onEnter赋值元素,否则会造成堆栈溢出
|
||||
ele = null;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
(x: any) => {
|
||||
state.dropdownList = x;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openContextmenu,
|
||||
closeContextmenu,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-contextmenu {
|
||||
transform-origin: center top;
|
||||
z-index: 2190;
|
||||
position: fixed;
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
|
||||
i {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.
|
||||
298
frontend/src/components/crontab/Crontab.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="crontab">
|
||||
<el-tabs v-model="state.activeName" @tab-change="changeTab(state.activeName)" type="border-card">
|
||||
<el-tab-pane :label="$t('components.crontab.second')" name="second" v-if="shouldHide('second')">
|
||||
<CrontabSecond :cron="crontabValueObj" ref="secondRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('components.crontab.minute')" name="min" v-if="shouldHide('min')">
|
||||
<CrontabMin :cron="crontabValueObj" ref="minRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('components.crontab.hour')" name="hour" v-if="shouldHide('hour')">
|
||||
<CrontabHour :cron="crontabValueObj" ref="hourRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('components.crontab.day')" name="day" v-if="shouldHide('day')">
|
||||
<CrontabDay :cron="crontabValueObj" ref="dayRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('components.crontab.month')" name="mouth" v-if="shouldHide('mouth')">
|
||||
<CrontabMouth :cron="crontabValueObj" ref="mouthRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('components.crontab.week')" name="week" v-if="shouldHide('week')">
|
||||
<CrontabWeek :cron="crontabValueObj" ref="weekRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('components.crontab.year')" name="year" v-if="shouldHide('year')">
|
||||
<CrontabYear :cron="crontabValueObj" ref="yearRef" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="popup-main">
|
||||
<div class="popup-result">
|
||||
<p class="title">{{ $t('components.crontab.timeExpression') }}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
|
||||
<th>{{ $t('components.crontab.crontabCompleteExpression') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.second }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.min }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.hour }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.day }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.mouth }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.week }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.year }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueString }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CrontabResult :ex="crontabValueString"></CrontabResult>
|
||||
|
||||
<div class="pop_btn">
|
||||
<el-button size="small" @click="hidePopup">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button size="small" type="warning" @click="clearCron">{{ $t('common.reset') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="submitFill">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, onMounted, reactive, ref, nextTick, watch } from 'vue';
|
||||
import CrontabSecond from './CrontabSecond.vue';
|
||||
import CrontabMin from './CrontabMin.vue';
|
||||
import CrontabHour from './CrontabHour.vue';
|
||||
import CrontabDay from './CrontabDay.vue';
|
||||
import CrontabMouth from './CrontabMouth.vue';
|
||||
import CrontabWeek from './CrontabWeek.vue';
|
||||
import CrontabYear from './CrontabYear.vue';
|
||||
import CrontabResult from './CrontabResult.vue';
|
||||
|
||||
const secondRef: any = ref(null);
|
||||
const minRef: any = ref(null);
|
||||
const hourRef: any = ref(null);
|
||||
const dayRef: any = ref(null);
|
||||
const mouthRef: any = ref(null);
|
||||
const weekRef: any = ref(null);
|
||||
const yearRef: any = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
expression: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hideComponent: {
|
||||
type: Array,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['hide', 'fill']);
|
||||
|
||||
const state = reactive({
|
||||
tabTitles: ['秒', '分钟', '小时', '日', '月', '周', '年'],
|
||||
tabActive: 0,
|
||||
activeName: 'second',
|
||||
myindex: 0,
|
||||
crontabValueObj: {
|
||||
second: '*',
|
||||
min: '*',
|
||||
hour: '*',
|
||||
day: '*',
|
||||
mouth: '*',
|
||||
week: '?',
|
||||
year: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { tabTitles, crontabValueObj } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
resolveExp();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.expression,
|
||||
() => {
|
||||
resolveExp();
|
||||
}
|
||||
);
|
||||
|
||||
function shouldHide(key: string) {
|
||||
if (props.hideComponent && props.hideComponent.includes(key)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveExp() {
|
||||
//反解析 表达式
|
||||
if (props.expression) {
|
||||
let arr = props.expression.split(' ');
|
||||
if (arr.length >= 6) {
|
||||
//6 位以上是合法表达式
|
||||
let obj = {
|
||||
second: arr[0],
|
||||
min: arr[1],
|
||||
hour: arr[2],
|
||||
day: arr[3],
|
||||
mouth: arr[4],
|
||||
week: arr[5],
|
||||
year: arr[6] ? arr[6] : '',
|
||||
};
|
||||
state.crontabValueObj = {
|
||||
...obj,
|
||||
};
|
||||
}
|
||||
changeTab(state.activeName);
|
||||
} else {
|
||||
//没有传入的表达式 则还原
|
||||
clearCron();
|
||||
}
|
||||
}
|
||||
|
||||
// 改变tab
|
||||
const changeTab = (name: string) => {
|
||||
nextTick(() => {
|
||||
getRefByName(name).value?.parse();
|
||||
});
|
||||
};
|
||||
|
||||
const getRefByName = (name: string) => {
|
||||
switch (name) {
|
||||
case 'second':
|
||||
return secondRef;
|
||||
case 'min':
|
||||
return minRef;
|
||||
case 'hour':
|
||||
return hourRef;
|
||||
case 'day':
|
||||
return dayRef;
|
||||
case 'mouth':
|
||||
return mouthRef;
|
||||
case 'week':
|
||||
return weekRef;
|
||||
case 'year':
|
||||
return yearRef;
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏弹窗
|
||||
function hidePopup() {
|
||||
emit('hide');
|
||||
}
|
||||
|
||||
// 填充表达式
|
||||
const submitFill = () => {
|
||||
emit('fill', crontabValueString.value);
|
||||
hidePopup();
|
||||
};
|
||||
|
||||
const clearCron = () => {
|
||||
// 还原选择项
|
||||
state.crontabValueObj = {
|
||||
second: '*',
|
||||
min: '*',
|
||||
hour: '*',
|
||||
day: '*',
|
||||
mouth: '*',
|
||||
week: '?',
|
||||
year: '',
|
||||
};
|
||||
changeTab(state.activeName);
|
||||
};
|
||||
|
||||
const crontabValueString = computed(() => {
|
||||
let obj = state.crontabValueObj;
|
||||
let str = obj.second + ' ' + obj.min + ' ' + obj.hour + ' ' + obj.day + ' ' + obj.mouth + ' ' + obj.week + (obj.year == '' ? '' : ' ' + obj.year);
|
||||
return str;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pop_btn {
|
||||
text-align: right;
|
||||
}
|
||||
.popup-main {
|
||||
position: relative;
|
||||
margin: 10px auto;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.popup-title {
|
||||
overflow: hidden;
|
||||
line-height: 34px;
|
||||
padding-top: 6px;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
.popup-result {
|
||||
box-sizing: border-box;
|
||||
line-height: 24px;
|
||||
margin: 15px auto;
|
||||
padding: 15px 20px 10px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
position: relative;
|
||||
}
|
||||
.popup-result .title {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
left: 50%;
|
||||
width: 140px;
|
||||
font-size: 14px;
|
||||
margin-left: -70px;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
}
|
||||
.popup-result table {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.popup-result table span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-family: arial;
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
.popup-result-scroll {
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
height: 10em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.crontab {
|
||||
::v-deep(.el-form-item) {
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
219
frontend/src/components/crontab/CrontabDay.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.day') }},{{ $t('components.crontab.dayCrontype1') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2"> {{ $t('components.crontab.crontype2') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ $t('components.crontab.crontype3') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="31" /> - <el-input-number v-model="cycle02" :min="0" :max="31" />
|
||||
{{ $t('components.crontab.day') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
{{ $t('components.crontab.crontypeFrom') }}
|
||||
<el-input-number v-model="average01" :min="0" :max="31" /> {{ $t('components.crontab.crontypeStartDay') }},
|
||||
{{ $t('components.crontab.crontypeEvery') }} <el-input-number v-model="average02" :min="0" :max="31" />
|
||||
{{ $t('components.crontab.crontypeExecDay') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item>
|
||||
<el-radio v-model="radioValue" :label="5">
|
||||
每月
|
||||
<el-input-number v-model="workday" :min="0" :max="31" /> 号最近的那个工作日
|
||||
</el-radio>
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="6"> {{ $t('components.crontab.monthLastDay') }} </el-radio>
|
||||
</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>
|
||||
<el-option v-for="item in 31" :key="item" :value="`${item}`">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watch, reactive } from 'vue';
|
||||
import { checkNumber, CrontabValueObj } from './index';
|
||||
|
||||
const cron = defineModel<CrontabValueObj>('cron', { required: true });
|
||||
|
||||
const state = reactive({
|
||||
radioValue: 1,
|
||||
workday: 1,
|
||||
cycle01: 1,
|
||||
cycle02: 2,
|
||||
average01: 1,
|
||||
average02: 1,
|
||||
checkboxList: [] as any,
|
||||
});
|
||||
|
||||
const { radioValue, workday, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
|
||||
|
||||
// 单选按钮值变化时
|
||||
function radioChange() {
|
||||
if (state.radioValue === 1) {
|
||||
cron.value.day = '*';
|
||||
cron.value.week = '?';
|
||||
cron.value.mouth = '*';
|
||||
} else {
|
||||
if (cron.value.hour === '*') {
|
||||
cron.value.hour = '0';
|
||||
}
|
||||
if (cron.value.min === '*') {
|
||||
cron.value.min = '0';
|
||||
}
|
||||
if (cron.value.second === '*') {
|
||||
cron.value.second = '0';
|
||||
}
|
||||
}
|
||||
|
||||
switch (state.radioValue) {
|
||||
case 2:
|
||||
cron.value.day = '?';
|
||||
break;
|
||||
case 3:
|
||||
cron.value.day = state.cycle01 + '-' + state.cycle02;
|
||||
break;
|
||||
case 4:
|
||||
cron.value.day = state.average01 + '/' + state.average02;
|
||||
break;
|
||||
case 5:
|
||||
cron.value.day = state.workday + 'W';
|
||||
break;
|
||||
case 6:
|
||||
cron.value.day = 'L';
|
||||
break;
|
||||
case 7:
|
||||
cron.value.day = checkboxString.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 周期两个值变化时
|
||||
function cycleChange() {
|
||||
if (state.radioValue == 3) {
|
||||
cron.value.day = cycleTotal.value;
|
||||
}
|
||||
}
|
||||
// 平均两个值变化时
|
||||
function averageChange() {
|
||||
state.average01 = checkNumber(state.average01, 1, 31);
|
||||
state.average02 = checkNumber(state.average02, 1, 31);
|
||||
if (state.radioValue == 4) {
|
||||
cron.value.day = averageTotal.value;
|
||||
}
|
||||
}
|
||||
// 最近工作日值变化时
|
||||
function workdayChange() {
|
||||
state.workday = checkNumber(state.workday, 1, 31);
|
||||
if (state.radioValue == 5) {
|
||||
cron.value.day = state.workday + 'W';
|
||||
}
|
||||
}
|
||||
// checkbox值变化时
|
||||
function checkboxChange() {
|
||||
if (state.radioValue == 7) {
|
||||
cron.value.day = checkboxString.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 父组件传递的week发生变化触发
|
||||
// function weekChange() {
|
||||
// //判断week值与day不能同时为“?”
|
||||
// if (cron.value.week == '?' && state.radioValue == 2) {
|
||||
// state.radioValue = 1;
|
||||
// } else if (cron.value.week !== '?' && state.radioValue != 2) {
|
||||
// state.radioValue = 2;
|
||||
// }
|
||||
// }
|
||||
|
||||
// 计算两个周期值
|
||||
const cycleTotal = computed(() => {
|
||||
return state.cycle01 + '-' + state.cycle02;
|
||||
});
|
||||
|
||||
// 计算平均用到的值
|
||||
const averageTotal = computed(() => {
|
||||
return state.average01 + '/' + state.average02;
|
||||
});
|
||||
|
||||
// 计算工作日格式
|
||||
const workdayCheck = computed(() => {
|
||||
return state.workday;
|
||||
});
|
||||
|
||||
// 计算勾选的checkbox值合集
|
||||
const checkboxString = computed(() => {
|
||||
let str = state.checkboxList.join();
|
||||
return str == '' ? '*' : str;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.radioValue,
|
||||
() => {
|
||||
radioChange();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
cycleChange();
|
||||
});
|
||||
|
||||
watch(averageTotal, () => {
|
||||
averageChange();
|
||||
});
|
||||
|
||||
watch(workdayCheck, () => {
|
||||
workdayChange();
|
||||
});
|
||||
|
||||
watch(checkboxString, () => {
|
||||
checkboxChange();
|
||||
});
|
||||
|
||||
const parse = () => {
|
||||
//反解析
|
||||
let value = cron.value.day;
|
||||
if (value === '*') {
|
||||
state.radioValue = 1;
|
||||
} else if (value == '?') {
|
||||
state.radioValue = 2;
|
||||
} else if (value.indexOf('-') > -1) {
|
||||
state.radioValue = 3;
|
||||
let indexArr = value.split('-') as any;
|
||||
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
|
||||
state.cycle02 = indexArr[1];
|
||||
} else if (value.indexOf('/') > -1) {
|
||||
state.radioValue = 4;
|
||||
let indexArr = value.split('/') as any;
|
||||
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
|
||||
state.average02 = indexArr[1];
|
||||
} else if (value.indexOf('W') > -1) {
|
||||
state.radioValue = 5;
|
||||
let indexArr = value.split('W') as any;
|
||||
isNaN(indexArr[0]) ? (state.workday = 0) : (state.workday = indexArr[0]);
|
||||
} else if (value === 'L') {
|
||||
state.radioValue = 6;
|
||||
} else {
|
||||
state.checkboxList = value.split(',');
|
||||
state.radioValue = 7;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ parse });
|
||||
</script>
|
||||
159
frontend/src/components/crontab/CrontabHour.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.hour') }},{{ $t('components.crontab.hourCronType1') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ $t('components.crontab.crontype3') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" />
|
||||
{{ $t('components.crontab.hour') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ $t('components.crontab.crontypeFrom') }}
|
||||
<el-input-number v-model="average01" :min="0" :max="60" /> {{ $t('components.crontab.crontypeStartHour') }},
|
||||
{{ $t('components.crontab.crontypeEvery') }} <el-input-number v-model="average02" :min="0" :max="60" />
|
||||
{{ $t('components.crontab.crontypeExecHour') }}
|
||||
</el-radio>
|
||||
</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>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watch, reactive } from 'vue';
|
||||
import { checkNumber, CrontabValueObj } from './index';
|
||||
|
||||
const cron = defineModel<CrontabValueObj>('cron', { required: true });
|
||||
|
||||
const state = reactive({
|
||||
radioValue: 1,
|
||||
cycle01: 0,
|
||||
cycle02: 1,
|
||||
average01: 0,
|
||||
average02: 1,
|
||||
checkboxList: [] as any,
|
||||
});
|
||||
|
||||
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
|
||||
|
||||
// 单选按钮值变化时
|
||||
function radioChange() {
|
||||
if (state.radioValue === 1) {
|
||||
cron.value.hour = '*';
|
||||
cron.value.day = '*';
|
||||
} else {
|
||||
if (cron.value.min === '*') {
|
||||
cron.value.min = '0';
|
||||
}
|
||||
if (cron.value.second === '*') {
|
||||
cron.value.second = '0';
|
||||
}
|
||||
}
|
||||
switch (state.radioValue) {
|
||||
case 2:
|
||||
cron.value.hour = state.cycle01 + '-' + state.cycle02;
|
||||
break;
|
||||
case 3:
|
||||
cron.value.hour = state.average01 + '/' + state.average02;
|
||||
break;
|
||||
case 4:
|
||||
cron.value.hour = checkboxString.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 周期两个值变化时
|
||||
function cycleChange() {
|
||||
state.cycle01 = checkNumber(state.cycle01, 0, 23);
|
||||
state.cycle02 = checkNumber(state.cycle02, 0, 23);
|
||||
if (state.radioValue == 2) {
|
||||
cron.value.hour = cycleTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 平均两个值变化时
|
||||
function averageChange() {
|
||||
state.average01 = checkNumber(state.average01, 0, 23);
|
||||
state.average02 = checkNumber(state.average02, 0, 23);
|
||||
if (state.radioValue == 3) {
|
||||
cron.value.hour = averageTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// checkbox值变化时
|
||||
function checkboxChange() {
|
||||
if (state.radioValue == 4) {
|
||||
cron.value.hour = checkboxString.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算两个周期值
|
||||
const cycleTotal = computed(() => {
|
||||
return state.cycle01 + '-' + state.cycle02;
|
||||
});
|
||||
|
||||
// 计算平均用到的值
|
||||
const averageTotal = computed(() => {
|
||||
return state.average01 + '/' + state.average02;
|
||||
});
|
||||
|
||||
// 计算勾选的checkbox值合集
|
||||
const checkboxString = computed(() => {
|
||||
let str = state.checkboxList.join();
|
||||
return str == '' ? '*' : str;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.radioValue,
|
||||
() => {
|
||||
radioChange();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
cycleChange();
|
||||
});
|
||||
|
||||
watch(averageTotal, () => {
|
||||
averageChange();
|
||||
});
|
||||
|
||||
watch(checkboxString, () => {
|
||||
checkboxChange();
|
||||
});
|
||||
|
||||
const parse = () => {
|
||||
//反解析
|
||||
let ins = cron.value.hour;
|
||||
if (ins === '*') {
|
||||
state.radioValue = 1;
|
||||
} else if (ins.indexOf('-') > -1) {
|
||||
state.radioValue = 2;
|
||||
let indexArr = ins.split('-') as any;
|
||||
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
|
||||
state.cycle02 = indexArr[1];
|
||||
} else if (ins.indexOf('/') > -1) {
|
||||
state.radioValue = 3;
|
||||
let indexArr = ins.split('/') as any;
|
||||
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
|
||||
state.average02 = indexArr[1];
|
||||
} else {
|
||||
state.radioValue = 4;
|
||||
state.checkboxList = ins.split(',');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ parse });
|
||||
</script>
|
||||
19
frontend/src/components/crontab/CrontabInput.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<el-input v-model="cron" :placeholder="$t('components.crontab.crontabInputPlaceholder')">
|
||||
<template #prepend>
|
||||
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-dialog :title="$t('components.crontab.crontabTitle')" v-model="showCron" width="660px">
|
||||
<Crontab :expression="cron" @hide="showCron = false" @fill="(ex: any) => (cron = ex)" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import Crontab from '@/components/crontab/Crontab.vue';
|
||||
|
||||
const cron = defineModel<string>('modelValue', { required: true });
|
||||
|
||||
const showCron = ref(false);
|
||||
</script>
|
||||
155
frontend/src/components/crontab/CrontabMin.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.minute') }},{{ $t('components.crontab.hourCronType1') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ $t('components.crontab.crontype3') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" />
|
||||
{{ $t('components.crontab.minute') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ $t('components.crontab.crontypeFrom') }}
|
||||
<el-input-number v-model="average01" :min="0" :max="60" /> {{ $t('components.crontab.crontypeStartMin') }},
|
||||
{{ $t('components.crontab.crontypeEvery') }} <el-input-number v-model="average02" :min="0" :max="60" />
|
||||
{{ $t('components.crontab.crontypeExecMin') }}
|
||||
</el-radio>
|
||||
</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>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watch, reactive } from 'vue';
|
||||
import { checkNumber, CrontabValueObj } from './index';
|
||||
|
||||
const cron = defineModel<CrontabValueObj>('cron', { required: true });
|
||||
|
||||
const state = reactive({
|
||||
radioValue: 1,
|
||||
cycle01: 0,
|
||||
cycle02: 1,
|
||||
average01: 0,
|
||||
average02: 1,
|
||||
checkboxList: [] as any,
|
||||
});
|
||||
|
||||
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
|
||||
|
||||
// 单选按钮值变化时
|
||||
function radioChange() {
|
||||
if (state.radioValue !== 1 && cron.value.second === '*') {
|
||||
cron.value.second = '0';
|
||||
}
|
||||
switch (state.radioValue) {
|
||||
case 1:
|
||||
cron.value.min = '*';
|
||||
cron.value.hour = '*';
|
||||
break;
|
||||
case 2:
|
||||
cron.value.min = state.cycle01 + '-' + state.cycle02;
|
||||
break;
|
||||
case 3:
|
||||
cron.value.min = state.average01 + '/' + state.average02;
|
||||
break;
|
||||
case 4:
|
||||
cron.value.min = checkboxString.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 周期两个值变化时
|
||||
function cycleChange() {
|
||||
state.cycle01 = checkNumber(state.cycle01, 0, 59);
|
||||
state.cycle02 = checkNumber(state.cycle02, 0, 59);
|
||||
if (state.radioValue == 2) {
|
||||
cron.value.min = cycleTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 平均两个值变化时
|
||||
function averageChange() {
|
||||
state.average01 = checkNumber(state.average01, 0, 59);
|
||||
state.average02 = checkNumber(state.average02, 1, 59);
|
||||
if (state.radioValue == 3) {
|
||||
cron.value.min = averageTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// checkbox值变化时
|
||||
function checkboxChange() {
|
||||
if (state.radioValue == 4) {
|
||||
cron.value.min = checkboxString.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算两个周期值
|
||||
const cycleTotal = computed(() => {
|
||||
return state.cycle01 + '-' + state.cycle02;
|
||||
});
|
||||
|
||||
// 计算平均用到的值
|
||||
const averageTotal = computed(() => {
|
||||
return state.average01 + '/' + state.average02;
|
||||
});
|
||||
|
||||
// 计算勾选的checkbox值合集
|
||||
const checkboxString = computed(() => {
|
||||
let str = state.checkboxList.join();
|
||||
return str == '' ? '*' : str;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.radioValue,
|
||||
() => {
|
||||
radioChange();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
cycleChange();
|
||||
});
|
||||
|
||||
watch(averageTotal, () => {
|
||||
averageChange();
|
||||
});
|
||||
|
||||
watch(checkboxString, () => {
|
||||
checkboxChange();
|
||||
});
|
||||
|
||||
const parse = () => {
|
||||
//反解析
|
||||
let ins = cron.value.min;
|
||||
if (ins === '*') {
|
||||
state.radioValue = 1;
|
||||
} else if (ins.indexOf('-') > -1) {
|
||||
state.radioValue = 2;
|
||||
let indexArr = ins.split('-') as any;
|
||||
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
|
||||
state.cycle02 = indexArr[1];
|
||||
} else if (ins.indexOf('/') > -1) {
|
||||
state.radioValue = 3;
|
||||
let indexArr = ins.split('/') as any;
|
||||
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
|
||||
state.average02 = indexArr[1];
|
||||
} else {
|
||||
state.radioValue = 4;
|
||||
state.checkboxList = ins.split(',');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ parse });
|
||||
</script>
|
||||
166
frontend/src/components/crontab/CrontabMouth.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.month') }},{{ $t('components.crontab.hourCronType1') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ $t('components.crontab.crontype3') }}
|
||||
<el-input-number v-model="cycle01" :min="1" :max="12" /> - <el-input-number v-model="cycle02" :min="1" :max="12" />
|
||||
{{ $t('components.crontab.month') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ $t('components.crontab.crontypeFrom') }}
|
||||
<el-input-number v-model="average01" :min="1" :max="12" /> {{ $t('components.crontab.crontypeStartMonth') }},
|
||||
{{ $t('components.crontab.crontypeEvery') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="12" /> {{ $t('components.crontab.crontypeExecMonth') }}
|
||||
</el-radio>
|
||||
</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>
|
||||
<el-option v-for="item in 12" :key="item" :value="`${item}`">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watch, reactive } from 'vue';
|
||||
import { checkNumber, CrontabValueObj } from './index';
|
||||
|
||||
const cron = defineModel<CrontabValueObj>('cron', { required: true });
|
||||
|
||||
const state = reactive({
|
||||
radioValue: 1,
|
||||
cycle01: 1,
|
||||
cycle02: 2,
|
||||
average01: 1,
|
||||
average02: 1,
|
||||
checkboxList: [] as any,
|
||||
});
|
||||
|
||||
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
|
||||
|
||||
// 单选按钮值变化时
|
||||
function radioChange() {
|
||||
if (state.radioValue === 1) {
|
||||
cron.value.mouth = '*';
|
||||
cron.value.year = '*';
|
||||
} else {
|
||||
if (cron.value.day === '*') {
|
||||
cron.value.day = '0';
|
||||
}
|
||||
if (cron.value.hour === '*') {
|
||||
cron.value.hour = '0';
|
||||
}
|
||||
if (cron.value.min === '*') {
|
||||
cron.value.min = '0';
|
||||
}
|
||||
if (cron.value.second === '*') {
|
||||
cron.value.second = '0';
|
||||
}
|
||||
}
|
||||
switch (state.radioValue) {
|
||||
case 2:
|
||||
cron.value.mouth = state.cycle01 + '-' + state.cycle02;
|
||||
break;
|
||||
case 3:
|
||||
cron.value.mouth = state.average01 + '/' + state.average02;
|
||||
break;
|
||||
case 4:
|
||||
cron.value.mouth = checkboxString.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 周期两个值变化时
|
||||
function cycleChange() {
|
||||
state.cycle01 = checkNumber(state.cycle01, 1, 12);
|
||||
state.cycle02 = checkNumber(state.cycle02, 1, 12);
|
||||
if (state.radioValue == 2) {
|
||||
cron.value.mouth = cycleTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 平均两个值变化时
|
||||
function averageChange() {
|
||||
state.average01 = checkNumber(state.average01, 1, 12);
|
||||
state.average02 = checkNumber(state.average02, 1, 12);
|
||||
if (state.radioValue == 3) {
|
||||
cron.value.mouth = averageTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// checkbox值变化时
|
||||
function checkboxChange() {
|
||||
if (state.radioValue == 4) {
|
||||
cron.value.mouth = checkboxString.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算两个周期值
|
||||
const cycleTotal = computed(() => {
|
||||
return state.cycle01 + '-' + state.cycle02;
|
||||
});
|
||||
|
||||
// 计算平均用到的值
|
||||
const averageTotal = computed(() => {
|
||||
return state.average01 + '/' + state.average02;
|
||||
});
|
||||
|
||||
// 计算勾选的checkbox值合集
|
||||
const checkboxString = computed(() => {
|
||||
let str = state.checkboxList.join();
|
||||
return str == '' ? '*' : str;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.radioValue,
|
||||
() => {
|
||||
radioChange();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
cycleChange();
|
||||
});
|
||||
|
||||
watch(averageTotal, () => {
|
||||
averageChange();
|
||||
});
|
||||
|
||||
watch(checkboxString, () => {
|
||||
checkboxChange();
|
||||
});
|
||||
|
||||
const parse = () => {
|
||||
//反解析
|
||||
let ins = cron.value.mouth;
|
||||
if (ins === '*') {
|
||||
state.radioValue = 1;
|
||||
} else if (ins.indexOf('-') > -1) {
|
||||
state.radioValue = 2;
|
||||
let indexArr = ins.split('-') as any;
|
||||
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
|
||||
state.cycle02 = indexArr[1];
|
||||
} else if (ins.indexOf('/') > -1) {
|
||||
state.radioValue = 3;
|
||||
let indexArr = ins.split('/') as any;
|
||||
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
|
||||
state.average02 = indexArr[1];
|
||||
} else {
|
||||
state.radioValue = 4;
|
||||
state.checkboxList = ins.split(',');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ parse });
|
||||
</script>
|
||||
588
frontend/src/components/crontab/CrontabResult.vue
Normal file
@@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<div class="popup-result">
|
||||
<p class="title">{{ $t('components.crontab.last5runTimes') }}</p>
|
||||
<ul class="popup-result-scroll">
|
||||
<template v-if="isShow">
|
||||
<li v-for="item in resultList" :key="item">{{ item }}</li>
|
||||
</template>
|
||||
<li v-else>{{ $t('components.crontab.calculationing') }}...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js" setup>
|
||||
import { toRefs, watch, onMounted, reactive } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
ex: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
dayRule: '',
|
||||
dayRuleSup: '',
|
||||
dateArr: [],
|
||||
resultList: [],
|
||||
isShow: false,
|
||||
});
|
||||
|
||||
const { resultList, isShow } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.ex,
|
||||
() => {
|
||||
expressionChange();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
expressionChange();
|
||||
});
|
||||
|
||||
// 表达式值变化时,开始去计算结果
|
||||
function expressionChange() {
|
||||
// 计算开始-隐藏结果
|
||||
state.isShow = false;
|
||||
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
|
||||
let ruleArr = props.ex.split(' ');
|
||||
// 用于记录进入循环的次数
|
||||
let nums = 0;
|
||||
// 用于暂时存符号时间规则结果的数组
|
||||
let resultArr = [];
|
||||
// 获取当前时间精确至[年、月、日、时、分、秒]
|
||||
let nTime = new Date();
|
||||
let nYear = nTime.getFullYear();
|
||||
let nMouth = nTime.getMonth() + 1;
|
||||
let nDay = nTime.getDate();
|
||||
let nHour = nTime.getHours();
|
||||
let nMin = nTime.getMinutes();
|
||||
let nSecond = nTime.getSeconds();
|
||||
// 根据规则获取到近100年可能年数组、月数组等等
|
||||
getSecondArr(ruleArr[0]);
|
||||
getMinArr(ruleArr[1]);
|
||||
getHourArr(ruleArr[2]);
|
||||
getDayArr(ruleArr[3]);
|
||||
getMouthArr(ruleArr[4]);
|
||||
getWeekArr(ruleArr[5]);
|
||||
getYearArr(ruleArr[6], nYear);
|
||||
// 将获取到的数组赋值-方便使用
|
||||
let sDate = state.dateArr[0];
|
||||
let mDate = state.dateArr[1];
|
||||
let hDate = state.dateArr[2];
|
||||
let DDate = state.dateArr[3];
|
||||
let MDate = state.dateArr[4];
|
||||
let YDate = state.dateArr[5];
|
||||
// 获取当前时间在数组中的索引
|
||||
let sIdx = getIndex(sDate, nSecond);
|
||||
let mIdx = getIndex(mDate, nMin);
|
||||
let hIdx = getIndex(hDate, nHour);
|
||||
let DIdx = getIndex(DDate, nDay);
|
||||
let MIdx = getIndex(MDate, nMouth);
|
||||
let YIdx = getIndex(YDate, nYear);
|
||||
// 重置月日时分秒的函数(后面用的比较多)
|
||||
const resetSecond = function () {
|
||||
sIdx = 0;
|
||||
nSecond = sDate[sIdx];
|
||||
};
|
||||
const resetMin = function () {
|
||||
mIdx = 0;
|
||||
nMin = mDate[mIdx];
|
||||
resetSecond();
|
||||
};
|
||||
const resetHour = function () {
|
||||
hIdx = 0;
|
||||
nHour = hDate[hIdx];
|
||||
resetMin();
|
||||
};
|
||||
const resetDay = function () {
|
||||
DIdx = 0;
|
||||
nDay = DDate[DIdx];
|
||||
resetHour();
|
||||
};
|
||||
const resetMouth = function () {
|
||||
MIdx = 0;
|
||||
nMouth = MDate[MIdx];
|
||||
resetDay();
|
||||
};
|
||||
// 如果当前年份不为数组中当前值
|
||||
if (nYear !== YDate[YIdx]) {
|
||||
resetMouth();
|
||||
}
|
||||
// 如果当前月份不为数组中当前值
|
||||
if (nMouth !== MDate[MIdx]) {
|
||||
resetDay();
|
||||
}
|
||||
// 如果当前“日”不为数组中当前值
|
||||
if (nDay !== DDate[DIdx]) {
|
||||
resetHour();
|
||||
}
|
||||
// 如果当前“时”不为数组中当前值
|
||||
if (nHour !== hDate[hIdx]) {
|
||||
resetMin();
|
||||
}
|
||||
// 如果当前“分”不为数组中当前值
|
||||
if (nMin !== mDate[mIdx]) {
|
||||
resetSecond();
|
||||
}
|
||||
|
||||
// 循环年份数组
|
||||
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
|
||||
let YY = YDate[Yi];
|
||||
// 如果到达最大值时
|
||||
if (nMouth > MDate[MDate.length - 1]) {
|
||||
resetMouth();
|
||||
continue;
|
||||
}
|
||||
// 循环月份数组
|
||||
goMouth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
|
||||
// 赋值、方便后面运算
|
||||
let MM = MDate[Mi];
|
||||
MM = MM < 10 ? '0' + MM : MM;
|
||||
// 如果到达最大值时
|
||||
if (nDay > DDate[DDate.length - 1]) {
|
||||
resetDay();
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMouth();
|
||||
continue goYear;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 循环日期数组
|
||||
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
|
||||
// 赋值、方便后面运算
|
||||
let DD = DDate[Di];
|
||||
let thisDD = DD < 10 ? '0' + DD : DD;
|
||||
|
||||
// 如果到达最大值时
|
||||
if (nHour > hDate[hDate.length - 1]) {
|
||||
resetHour();
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay();
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMouth();
|
||||
continue goYear;
|
||||
}
|
||||
continue goMouth;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断日期的合法性,不合法的话也是跳出当前循环
|
||||
if (
|
||||
checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true &&
|
||||
state.dayRule !== 'workDay' &&
|
||||
state.dayRule !== 'lastWeek' &&
|
||||
state.dayRule !== 'lastDay'
|
||||
) {
|
||||
resetDay();
|
||||
continue goMouth;
|
||||
}
|
||||
// 如果日期规则中有值时
|
||||
if (state.dayRule == 'lastDay') {
|
||||
//如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
|
||||
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--;
|
||||
|
||||
thisDD = DD < 10 ? '0' + DD : DD;
|
||||
}
|
||||
}
|
||||
} else if (state.dayRule == 'workDay') {
|
||||
//校验并调整如果是2月30号这种日期传进来时需调整至正常月底
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--;
|
||||
thisDD = DD < 10 ? '0' + DD : DD;
|
||||
}
|
||||
}
|
||||
// 获取达到条件的日期是星期X
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
|
||||
// 当星期日时
|
||||
if (thisWeek == 0) {
|
||||
//先找下一个日,并判断是否为月底
|
||||
DD++;
|
||||
thisDD = DD < 10 ? '0' + DD : DD;
|
||||
//判断下一日已经不是合法日期
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD -= 3;
|
||||
}
|
||||
} else if (thisWeek == 6) {
|
||||
//当星期6时只需判断不是1号就可进行操作
|
||||
if (state.dayRuleSup !== 1) {
|
||||
DD--;
|
||||
} else {
|
||||
DD += 2;
|
||||
}
|
||||
}
|
||||
} else if (state.dayRule == 'weekDay') {
|
||||
//如果指定了是星期几
|
||||
//获取当前日期是属于星期几
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
|
||||
//校验当前星期是否在星期池(state.dayRuleSup)中
|
||||
if (Array.indexOf(state.dayRuleSup, thisWeek) < 0) {
|
||||
// 如果到达最大值时
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay();
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMouth();
|
||||
continue goYear;
|
||||
}
|
||||
continue goMouth;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else if (state.dayRule == 'assWeek') {
|
||||
//如果指定了是第几周的星期几
|
||||
//获取每月1号是属于星期几
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
|
||||
if (state.dayRuleSup[1] >= thisWeek) {
|
||||
DD = (state.dayRuleSup[0] - 1) * 7 + state.dayRuleSup[1] - thisWeek + 1;
|
||||
} else {
|
||||
DD = state.dayRuleSup[0] * 7 + state.dayRuleSup[1] - thisWeek + 1;
|
||||
}
|
||||
} else if (state.dayRule == 'lastWeek') {
|
||||
//如果指定了每月最后一个星期几
|
||||
//校验并调整如果是2月30号这种日期传进来时需调整至正常月底
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--;
|
||||
thisDD = DD < 10 ? '0' + DD : DD;
|
||||
}
|
||||
}
|
||||
//获取月末最后一天是星期几
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
|
||||
//找到要求中最近的那个星期几
|
||||
if (state.dayRuleSup < thisWeek) {
|
||||
DD -= thisWeek - state.dayRuleSup;
|
||||
} else if (state.dayRuleSup > thisWeek) {
|
||||
DD -= 7 - (state.dayRuleSup - thisWeek);
|
||||
}
|
||||
}
|
||||
// 判断时间值是否小于10置换成“05”这种格式
|
||||
DD = DD < 10 ? '0' + DD : DD;
|
||||
|
||||
// 循环“时”数组
|
||||
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
|
||||
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi];
|
||||
|
||||
// 如果到达最大值时
|
||||
if (nMin > mDate[mDate.length - 1]) {
|
||||
resetMin();
|
||||
if (hi == hDate.length - 1) {
|
||||
resetHour();
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay();
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMouth();
|
||||
continue goYear;
|
||||
}
|
||||
continue goMouth;
|
||||
}
|
||||
continue goDay;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 循环"分"数组
|
||||
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
|
||||
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi];
|
||||
|
||||
// 如果到达最大值时
|
||||
if (nSecond > sDate[sDate.length - 1]) {
|
||||
resetSecond();
|
||||
if (mi == mDate.length - 1) {
|
||||
resetMin();
|
||||
if (hi == hDate.length - 1) {
|
||||
resetHour();
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay();
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMouth();
|
||||
continue goYear;
|
||||
}
|
||||
continue goMouth;
|
||||
}
|
||||
continue goDay;
|
||||
}
|
||||
continue goHour;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 循环"秒"数组
|
||||
// eslint-disable-next-line no-unused-labels
|
||||
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
|
||||
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si];
|
||||
// 添加当前时间(时间合法性在日期循环时已经判断)
|
||||
if (MM !== '00' && DD !== '00') {
|
||||
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss);
|
||||
nums++;
|
||||
}
|
||||
//如果条数满了就退出循环
|
||||
if (nums == 5) break goYear;
|
||||
//如果到达最大值时
|
||||
if (si == sDate.length - 1) {
|
||||
resetSecond();
|
||||
if (mi == mDate.length - 1) {
|
||||
resetMin();
|
||||
if (hi == hDate.length - 1) {
|
||||
resetHour();
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay();
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMouth();
|
||||
continue goYear;
|
||||
}
|
||||
continue goMouth;
|
||||
}
|
||||
continue goDay;
|
||||
}
|
||||
continue goHour;
|
||||
}
|
||||
continue goMin;
|
||||
}
|
||||
} //goSecond
|
||||
} //goMin
|
||||
} //goHour
|
||||
} //goDay
|
||||
} //goMouth
|
||||
}
|
||||
// 判断100年内的结果条数
|
||||
if (resultArr.length == 0) {
|
||||
state.resultList = ['没有达到条件的结果!'];
|
||||
} else {
|
||||
state.resultList = resultArr;
|
||||
if (resultArr.length !== 5) {
|
||||
state.resultList.push('最近100年内只有上面' + resultArr.length + '条结果!');
|
||||
}
|
||||
}
|
||||
// 计算完成-显示结果
|
||||
state.isShow = true;
|
||||
}
|
||||
//用于计算某位数字在数组中的索引
|
||||
function getIndex(arr, value) {
|
||||
if (value <= arr[0] || value > arr[arr.length - 1]) {
|
||||
return 0;
|
||||
} else {
|
||||
for (let i = 0; i < arr.length - 1; i++) {
|
||||
if (value > arr[i] && value <= arr[i + 1]) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取"年"数组
|
||||
function getYearArr(rule, year) {
|
||||
state.dateArr[5] = getOrderArr(year, year + 100);
|
||||
if (rule !== undefined) {
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
state.dateArr[5] = getCycleArr(rule, year + 100, false);
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
state.dateArr[5] = getAverageArr(rule, year + 100);
|
||||
} else if (rule !== '*') {
|
||||
state.dateArr[5] = getAssignArr(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取"月"数组
|
||||
function getMouthArr(rule) {
|
||||
state.dateArr[4] = getOrderArr(1, 12);
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
state.dateArr[4] = getCycleArr(rule, 12, false);
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
state.dateArr[4] = getAverageArr(rule, 12);
|
||||
} else if (rule !== '*') {
|
||||
state.dateArr[4] = getAssignArr(rule);
|
||||
}
|
||||
}
|
||||
// 获取"日"数组-主要为日期规则
|
||||
function getWeekArr(rule) {
|
||||
//只有当日期规则的两个值均为“”时则表达日期是有选项的
|
||||
if (state.dayRule == '' && state.dayRuleSup == '') {
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
state.dayRule = 'weekDay';
|
||||
state.dayRuleSup = getCycleArr(rule, 7, false);
|
||||
} else if (rule.indexOf('#') >= 0) {
|
||||
state.dayRule = 'assWeek';
|
||||
let matchRule = rule.match(/[0-9]{1}/g);
|
||||
state.dayRuleSup = [Number(matchRule[0]), Number(matchRule[1])];
|
||||
state.dateArr[3] = [1];
|
||||
if (state.dayRuleSup[1] == 7) {
|
||||
state.dayRuleSup[1] = 0;
|
||||
}
|
||||
} else if (rule.indexOf('L') >= 0) {
|
||||
state.dayRule = 'lastWeek';
|
||||
state.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0]);
|
||||
state.dateArr[3] = [31];
|
||||
if (state.dayRuleSup == 7) {
|
||||
state.dayRuleSup = 0;
|
||||
}
|
||||
} else if (rule !== '*' && rule !== '?') {
|
||||
state.dayRule = 'weekDay';
|
||||
state.dayRuleSup = getAssignArr(rule);
|
||||
}
|
||||
//如果weekDay时将7调整为0【week值0即是星期日】
|
||||
if (state.dayRule == 'weekDay') {
|
||||
for (let i = 0; i < state.dayRuleSup.length; i++) {
|
||||
if (state.dayRuleSup[i] == 7) {
|
||||
state.dayRuleSup[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取"日"数组-少量为日期规则
|
||||
function getDayArr(rule) {
|
||||
state.dateArr[3] = getOrderArr(1, 31);
|
||||
state.dayRule = '';
|
||||
state.dayRuleSup = '';
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
state.dateArr[3] = getCycleArr(rule, 31, false);
|
||||
state.dayRuleSup = 'null';
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
state.dateArr[3] = getAverageArr(rule, 31);
|
||||
state.dayRuleSup = 'null';
|
||||
} else if (rule.indexOf('W') >= 0) {
|
||||
state.dayRule = 'workDay';
|
||||
state.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0]);
|
||||
state.dateArr[3] = [state.dayRuleSup];
|
||||
} else if (rule.indexOf('L') >= 0) {
|
||||
state.dayRule = 'lastDay';
|
||||
state.dayRuleSup = 'null';
|
||||
state.dateArr[3] = [31];
|
||||
} else if (rule !== '*' && rule !== '?') {
|
||||
state.dateArr[3] = getAssignArr(rule);
|
||||
state.dayRuleSup = 'null';
|
||||
} else if (rule == '*') {
|
||||
state.dayRuleSup = 'null';
|
||||
}
|
||||
}
|
||||
// 获取"时"数组
|
||||
function getHourArr(rule) {
|
||||
state.dateArr[2] = getOrderArr(0, 23);
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
state.dateArr[2] = getCycleArr(rule, 24, true);
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
state.dateArr[2] = getAverageArr(rule, 23);
|
||||
} else if (rule !== '*') {
|
||||
state.dateArr[2] = getAssignArr(rule);
|
||||
}
|
||||
}
|
||||
// 获取"分"数组
|
||||
function getMinArr(rule) {
|
||||
state.dateArr[1] = getOrderArr(0, 59);
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
state.dateArr[1] = getCycleArr(rule, 60, true);
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
state.dateArr[1] = getAverageArr(rule, 59);
|
||||
} else if (rule !== '*') {
|
||||
state.dateArr[1] = getAssignArr(rule);
|
||||
}
|
||||
}
|
||||
// 获取"秒"数组
|
||||
function getSecondArr(rule) {
|
||||
state.dateArr[0] = getOrderArr(0, 59);
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
state.dateArr[0] = getCycleArr(rule, 60, true);
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
state.dateArr[0] = getAverageArr(rule, 59);
|
||||
} else if (rule !== '*') {
|
||||
state.dateArr[0] = getAssignArr(rule);
|
||||
}
|
||||
}
|
||||
// 根据传进来的min-max返回一个顺序的数组
|
||||
function getOrderArr(min, max) {
|
||||
let arr = [];
|
||||
for (let i = min; i <= max; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
// 根据规则中指定的零散值返回一个数组
|
||||
function getAssignArr(rule) {
|
||||
let arr = [];
|
||||
let assiginArr = rule.split(',');
|
||||
for (let i = 0; i < assiginArr.length; i++) {
|
||||
arr[i] = Number(assiginArr[i]);
|
||||
}
|
||||
arr.sort(compare);
|
||||
return arr;
|
||||
}
|
||||
// 根据一定算术规则计算返回一个数组
|
||||
function getAverageArr(rule, limit) {
|
||||
let arr = [];
|
||||
let agArr = rule.split('/');
|
||||
let min = Number(agArr[0]);
|
||||
let step = Number(agArr[1]);
|
||||
while (min <= limit) {
|
||||
arr.push(min);
|
||||
min += step;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
// 根据规则返回一个具有周期性的数组
|
||||
function getCycleArr(rule, limit, status) {
|
||||
//status--表示是否从0开始(则从1开始)
|
||||
let arr = [];
|
||||
let cycleArr = rule.split('-');
|
||||
let min = Number(cycleArr[0]);
|
||||
let max = Number(cycleArr[1]);
|
||||
if (min > max) {
|
||||
max += limit;
|
||||
}
|
||||
for (let i = min; i <= max; i++) {
|
||||
let add = 0;
|
||||
if (status == false && i % limit == 0) {
|
||||
add = limit;
|
||||
}
|
||||
arr.push(Math.round((i % limit) + add));
|
||||
}
|
||||
arr.sort(compare);
|
||||
return arr;
|
||||
}
|
||||
//比较数字大小(用于Array.sort)
|
||||
function compare(value1, value2) {
|
||||
if (value2 - value1 > 0) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// 格式化日期格式如:2017-9-19 18:04:33
|
||||
function formatDate(value, type) {
|
||||
// 计算日期相关值
|
||||
let time = typeof value == 'number' ? new Date(value) : value;
|
||||
let Y = time.getFullYear();
|
||||
let M = time.getMonth() + 1;
|
||||
let D = time.getDate();
|
||||
let h = time.getHours();
|
||||
let m = time.getMinutes();
|
||||
let s = time.getSeconds();
|
||||
let week = time.getDay();
|
||||
// 如果传递了type的话
|
||||
if (type == undefined) {
|
||||
return (
|
||||
Y +
|
||||
'-' +
|
||||
(M < 10 ? '0' + M : M) +
|
||||
'-' +
|
||||
(D < 10 ? '0' + D : D) +
|
||||
' ' +
|
||||
(h < 10 ? '0' + h : h) +
|
||||
':' +
|
||||
(m < 10 ? '0' + m : m) +
|
||||
':' +
|
||||
(s < 10 ? '0' + s : s)
|
||||
);
|
||||
} else if (type == 'week') {
|
||||
return week;
|
||||
}
|
||||
}
|
||||
// 检查日期是否存在
|
||||
function checkDate(value) {
|
||||
let time = new Date(value);
|
||||
let format = formatDate(time);
|
||||
return value == format ? true : false;
|
||||
}
|
||||
</script>
|
||||
152
frontend/src/components/crontab/CrontabSecond.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.second') }},{{ $t('components.crontab.hourCronType1') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ $t('components.crontab.crontype3') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="60" /> - <el-input-number v-model="cycle02" :min="0" :max="60" />
|
||||
{{ $t('components.crontab.second') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ $t('components.crontab.crontypeFrom') }}
|
||||
<el-input-number v-model="average01" :min="0" :max="60" /> {{ $t('components.crontab.crontypeStartSecond') }},
|
||||
{{ $t('components.crontab.crontypeEvery') }}<el-input-number v-model="average02" :min="0" :max="60" />
|
||||
{{ $t('components.crontab.crontypeExecSecond') }}
|
||||
</el-radio>
|
||||
</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>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watch, reactive } from 'vue';
|
||||
import { checkNumber, CrontabValueObj } from './index';
|
||||
|
||||
const cron = defineModel<CrontabValueObj>('cron', { required: true });
|
||||
|
||||
const state = reactive({
|
||||
radioValue: 1,
|
||||
cycle01: 1,
|
||||
cycle02: 2,
|
||||
average01: 0,
|
||||
average02: 1,
|
||||
checkboxList: [] as any,
|
||||
});
|
||||
|
||||
const { radioValue, cycle01, cycle02, average01, average02, checkboxList } = toRefs(state);
|
||||
|
||||
// 单选按钮值变化时
|
||||
function radioChange() {
|
||||
switch (state.radioValue) {
|
||||
case 1:
|
||||
cron.value.second = '*';
|
||||
cron.value.min = '*';
|
||||
break;
|
||||
case 2:
|
||||
cron.value.second = state.cycle01 + '-' + state.cycle02;
|
||||
break;
|
||||
case 3:
|
||||
cron.value.second = state.average01 + '/' + state.average02;
|
||||
break;
|
||||
case 4:
|
||||
cron.value.second = checkboxString.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 周期两个值变化时
|
||||
function cycleChange() {
|
||||
state.cycle01 = checkNumber(state.cycle01, 0, 59);
|
||||
state.cycle02 = checkNumber(state.cycle02, 0, 59);
|
||||
if (state.radioValue == 2) {
|
||||
cron.value.second = cycleTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 平均两个值变化时
|
||||
function averageChange() {
|
||||
state.average01 = checkNumber(state.average01, 0, 59);
|
||||
state.average02 = checkNumber(state.average02, 1, 59);
|
||||
if (state.radioValue == 3) {
|
||||
cron.value.second = averageTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// checkbox值变化时
|
||||
function checkboxChange() {
|
||||
if (state.radioValue == 4) {
|
||||
cron.value.second = checkboxString.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算两个周期值
|
||||
const cycleTotal = computed(() => {
|
||||
return state.cycle01 + '-' + state.cycle02;
|
||||
});
|
||||
|
||||
// 计算平均用到的值
|
||||
const averageTotal = computed(() => {
|
||||
return state.average01 + '/' + state.average02;
|
||||
});
|
||||
|
||||
// 计算勾选的checkbox值合集
|
||||
const checkboxString = computed(() => {
|
||||
let str = state.checkboxList.join();
|
||||
return str == '' ? '*' : str;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.radioValue,
|
||||
() => {
|
||||
radioChange();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
cycleChange();
|
||||
});
|
||||
|
||||
watch(averageTotal, () => {
|
||||
averageChange();
|
||||
});
|
||||
|
||||
watch(checkboxString, () => {
|
||||
checkboxChange();
|
||||
});
|
||||
|
||||
const parse = () => {
|
||||
//反解析
|
||||
let ins = cron.value.second;
|
||||
if (ins === '*') {
|
||||
state.radioValue = 1;
|
||||
} else if (ins.indexOf('-') > -1) {
|
||||
state.radioValue = 2;
|
||||
let indexArr = ins.split('-') as any;
|
||||
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
|
||||
state.cycle02 = indexArr[1];
|
||||
} else if (ins.indexOf('/') > -1) {
|
||||
state.radioValue = 3;
|
||||
let indexArr = ins.split('/') as any;
|
||||
isNaN(indexArr[0]) ? (state.average01 = 0) : (state.average01 = indexArr[0]);
|
||||
state.average02 = indexArr[1];
|
||||
} else {
|
||||
state.radioValue = 4;
|
||||
state.checkboxList = ins.split(',');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ parse });
|
||||
</script>
|
||||
213
frontend/src/components/crontab/CrontabWeek.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> {{ $t('components.crontab.weekCronType1') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2"> {{ $t('components.crontab.crontype2') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ $t('components.crontab.crontype3') }}
|
||||
<el-input-number v-model="cycle01" :min="1" :max="7" /> -
|
||||
<el-input-number v-model="cycle02" :min="1" :max="7" />
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
第
|
||||
<el-input-number v-model="average01" :min="1" :max="4" /> 周的星期
|
||||
<el-input-number v-model="average02" :min="1" :max="7" />
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="5">
|
||||
本月最后一个星期
|
||||
<el-input-number v-model="weekday" :min="1" :max="7" />
|
||||
</el-radio>
|
||||
</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>
|
||||
<el-option v-for="(item, index) of weekList" :label="item" :key="index" :value="`${index + 1}`">{{ $t(item) }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watch, reactive } from 'vue';
|
||||
import { checkNumber, CrontabValueObj } from './index';
|
||||
|
||||
const cron = defineModel<CrontabValueObj>('cron', { required: true });
|
||||
|
||||
const state = reactive({
|
||||
radioValue: 2,
|
||||
weekday: 1,
|
||||
cycle01: 1,
|
||||
cycle02: 2,
|
||||
average01: 1,
|
||||
average02: 1,
|
||||
checkboxList: [] as any,
|
||||
weekList: [
|
||||
'components.crontab.monday',
|
||||
'components.crontab.tuesday',
|
||||
'components.crontab.wednesday',
|
||||
'components.crontab.thursday',
|
||||
'components.crontab.friday',
|
||||
'components.crontab.saturday',
|
||||
'components.crontab.sunday',
|
||||
],
|
||||
});
|
||||
|
||||
const { radioValue, cycle01, cycle02, average01, average02, checkboxList, weekday, weekList } = toRefs(state);
|
||||
|
||||
// 单选按钮值变化时
|
||||
function radioChange() {
|
||||
if (state.radioValue === 1) {
|
||||
cron.value.week = '*';
|
||||
cron.value.year = '*';
|
||||
} else {
|
||||
if (cron.value.mouth === '*') {
|
||||
cron.value.mouth = '0';
|
||||
}
|
||||
if (cron.value.day === '*') {
|
||||
cron.value.day = '0';
|
||||
}
|
||||
if (cron.value.hour === '*') {
|
||||
cron.value.hour = '0';
|
||||
}
|
||||
if (cron.value.min === '*') {
|
||||
cron.value.min = '0';
|
||||
}
|
||||
if (cron.value.second === '*') {
|
||||
cron.value.second = '0';
|
||||
}
|
||||
}
|
||||
switch (state.radioValue) {
|
||||
case 2:
|
||||
cron.value.week = '?';
|
||||
break;
|
||||
case 3:
|
||||
cron.value.week = state.cycle01 + '-' + state.cycle02;
|
||||
break;
|
||||
case 4:
|
||||
cron.value.week = state.average01 + '#' + state.average02;
|
||||
break;
|
||||
case 5:
|
||||
cron.value.week = state.weekday + 'L';
|
||||
break;
|
||||
case 6:
|
||||
cron.value.week = checkboxString.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 周期两个值变化时
|
||||
function cycleChange() {
|
||||
state.cycle01 = checkNumber(state.cycle01, 1, 7);
|
||||
state.cycle02 = checkNumber(state.cycle02, 1, 7);
|
||||
if (state.radioValue == 3) {
|
||||
cron.value.week = cycleTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 平均两个值变化时
|
||||
function averageChange() {
|
||||
state.average01 = checkNumber(state.average01, 1, 4);
|
||||
state.average02 = checkNumber(state.average02, 1, 7);
|
||||
if (state.radioValue == 4) {
|
||||
cron.value.week = averageTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// checkbox值变化时
|
||||
function checkboxChange() {
|
||||
if (state.radioValue == 6) {
|
||||
cron.value.week = checkboxString.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算两个周期值
|
||||
const cycleTotal = computed(() => {
|
||||
return state.cycle01 + '-' + state.cycle02;
|
||||
});
|
||||
|
||||
// 计算平均用到的值
|
||||
const averageTotal = computed(() => {
|
||||
return state.average01 + '#' + state.average02;
|
||||
});
|
||||
|
||||
// 计算勾选的checkbox值合集
|
||||
const checkboxString = computed(() => {
|
||||
let str = state.checkboxList.join();
|
||||
return str == '' ? '*' : str;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.radioValue,
|
||||
() => {
|
||||
radioChange();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
cycleChange();
|
||||
});
|
||||
|
||||
watch(averageTotal, () => {
|
||||
averageChange();
|
||||
});
|
||||
|
||||
watch(checkboxString, () => {
|
||||
checkboxChange();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.weekday,
|
||||
() => {
|
||||
state.weekday = checkNumber(state.weekday, 1, 7);
|
||||
if (state.radioValue == 5) {
|
||||
cron.value.week = state.weekday + 'L';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const parse = () => {
|
||||
//反解析
|
||||
let value = cron.value.week;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (value === '*') {
|
||||
state.radioValue = 1;
|
||||
} else if (value == '?') {
|
||||
state.radioValue = 2;
|
||||
} else if (value.indexOf('-') > -1) {
|
||||
let indexArr = value.split('-') as any;
|
||||
isNaN(indexArr[0]) ? (state.cycle01 = 0) : (state.cycle01 = indexArr[0]);
|
||||
state.cycle02 = indexArr[1];
|
||||
state.radioValue = 3;
|
||||
} else if (value.indexOf('#') > -1) {
|
||||
let indexArr = value.split('#') as any;
|
||||
isNaN(indexArr[0]) ? (state.average01 = 1) : (state.average01 = indexArr[0]);
|
||||
state.average02 = indexArr[1];
|
||||
state.radioValue = 4;
|
||||
} else if (value.indexOf('L') > -1) {
|
||||
let indexArr = value.split('L') as any;
|
||||
isNaN(indexArr[0]) ? (state.weekday = 1) : (state.weekday = indexArr[0]);
|
||||
state.radioValue = 5;
|
||||
} else {
|
||||
state.checkboxList = value.split(',');
|
||||
state.radioValue = 6;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ parse });
|
||||
</script>
|
||||
161
frontend/src/components/crontab/CrontabYear.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio :label="1" v-model="radioValue"> {{ $t('components.crontab.hourCronType1') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio :label="2" v-model="radioValue"> {{ $t('components.crontab.yearly') }} </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio :label="3" v-model="radioValue">
|
||||
{{ $t('components.crontab.crontype3') }}
|
||||
<el-input-number v-model="cycle01" :min="fullYear" /> -
|
||||
<el-input-number v-model="cycle02" :min="fullYear" />
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio :label="4" v-model="radioValue">
|
||||
{{ $t('components.crontab.crontypeFrom') }}
|
||||
<el-input-number v-model="average01" :min="fullYear" /> {{ $t('components.crontab.crontypeStartYear') }},
|
||||
{{ $t('components.crontab.crontypeEvery') }}
|
||||
<el-input-number v-model="average02" :min="fullYear" /> {{ $t('components.crontab.crontypeExecYear') }}
|
||||
</el-radio>
|
||||
</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>
|
||||
<el-option v-for="item in 9" :key="item" :value="`${item - 1 + fullYear}`" :label="item - 1 + fullYear" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watch, onMounted, reactive } from 'vue';
|
||||
import { checkNumber, CrontabValueObj } from './index';
|
||||
|
||||
const cron = defineModel<CrontabValueObj>('cron', { required: true });
|
||||
|
||||
const state = reactive({
|
||||
fullYear: 0,
|
||||
radioValue: 1,
|
||||
cycle01: 0,
|
||||
cycle02: 0,
|
||||
average01: 0,
|
||||
average02: 1,
|
||||
checkboxList: [] as any,
|
||||
});
|
||||
|
||||
const { radioValue, cycle01, cycle02, average01, average02, checkboxList, fullYear } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
// 仅获取当前年份
|
||||
state.fullYear = Number(new Date().getFullYear());
|
||||
});
|
||||
|
||||
// 单选按钮值变化时
|
||||
function radioChange() {
|
||||
switch (state.radioValue) {
|
||||
case 1:
|
||||
cron.value.year = '';
|
||||
break;
|
||||
case 2:
|
||||
cron.value.year = '*';
|
||||
break;
|
||||
case 3:
|
||||
cron.value.year = state.cycle01 + '-' + state.cycle02;
|
||||
break;
|
||||
case 4:
|
||||
cron.value.year = state.average01 + '/' + state.average02;
|
||||
break;
|
||||
case 5:
|
||||
cron.value.year = checkboxString.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 周期两个值变化时
|
||||
function cycleChange() {
|
||||
state.cycle01 = checkNumber(state.cycle01, state.fullYear, state.fullYear + 100);
|
||||
state.cycle02 = checkNumber(state.cycle02, state.fullYear + 1, state.fullYear + 101);
|
||||
if (state.radioValue == 3) {
|
||||
cron.value.year = cycleTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 平均两个值变化时
|
||||
function averageChange() {
|
||||
state.average01 = checkNumber(state.average01, state.fullYear, state.fullYear + 100);
|
||||
state.average02 = checkNumber(state.average02, 1, 10);
|
||||
if (state.radioValue == 4) {
|
||||
cron.value.year = averageTotal.value;
|
||||
}
|
||||
}
|
||||
|
||||
// checkbox值变化时
|
||||
function checkboxChange() {
|
||||
if (state.radioValue == 5) {
|
||||
cron.value.year = checkboxString.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算两个周期值
|
||||
const cycleTotal = computed(() => {
|
||||
return state.cycle01 + '-' + state.cycle02;
|
||||
});
|
||||
|
||||
// 计算平均用到的值
|
||||
const averageTotal = computed(() => {
|
||||
return state.average01 + '/' + state.average02;
|
||||
});
|
||||
|
||||
// 计算勾选的checkbox值合集
|
||||
const checkboxString = computed(() => {
|
||||
let str = state.checkboxList.join();
|
||||
return str == '' ? '*' : str;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.radioValue,
|
||||
() => {
|
||||
radioChange();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
cycleChange();
|
||||
});
|
||||
|
||||
watch(averageTotal, () => {
|
||||
averageChange();
|
||||
});
|
||||
|
||||
watch(checkboxString, () => {
|
||||
checkboxChange();
|
||||
});
|
||||
|
||||
const parse = () => {
|
||||
//反解析
|
||||
let value = cron.value.year;
|
||||
if (value == '') {
|
||||
state.radioValue = 1;
|
||||
} else if (value == '*') {
|
||||
state.radioValue = 2;
|
||||
} else if (value.indexOf('-') > -1) {
|
||||
state.radioValue = 3;
|
||||
} else if (value.indexOf('/') > -1) {
|
||||
state.radioValue = 4;
|
||||
} else {
|
||||
state.checkboxList = value.split(',');
|
||||
state.radioValue = 5;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ parse });
|
||||
</script>
|
||||
21
frontend/src/components/crontab/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 表单选项的子组件校验数字格式
|
||||
export function checkNumber(value: any, minLimit: number, maxLimit: number) {
|
||||
//检查必须为整数
|
||||
value = Math.floor(value);
|
||||
if (value < minLimit) {
|
||||
value = minLimit;
|
||||
} else if (value > maxLimit) {
|
||||
value = maxLimit;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export interface CrontabValueObj {
|
||||
second: string;
|
||||
min: string;
|
||||
hour: string;
|
||||
day: string;
|
||||
mouth: string;
|
||||
week: string;
|
||||
year: string;
|
||||
}
|
||||
33
frontend/src/components/drawer-header/DrawerHeader.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-page-header @back="props.back">
|
||||
<template #content>
|
||||
<span>{{ header }}</span>
|
||||
<span v-if="resource && !hideResource">
|
||||
-
|
||||
<el-tooltip v-if="resource.length > 25" :content="resource" placement="bottom">
|
||||
<el-tag effect="dark" type="success">{{ resource.substring(0, 23) + '...' }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else effect="dark" type="success">{{ resource }}</el-tag>
|
||||
</span>
|
||||
<el-divider v-if="slots.buttons" direction="vertical" />
|
||||
<slot v-if="slots.buttons" name="buttons"></slot>
|
||||
</template>
|
||||
<template #extra>
|
||||
<slot v-if="slots.extra" name="extra"></slot>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSlots } from 'vue';
|
||||
const slots = useSlots();
|
||||
|
||||
defineOptions({ name: 'DrawerHeader' });
|
||||
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
back: Function,
|
||||
resource: String,
|
||||
hideResource: Boolean,
|
||||
});
|
||||
</script>
|
||||
47
frontend/src/components/dynamic-form/DynamicForm.vue
Executable file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="dynamic-form">
|
||||
<el-form v-bind="$attrs" ref="formRef" :model="modelValue" label-width="auto">
|
||||
<el-form-item v-for="item in props.formItems as any" :key="item.name" :prop="item.model" :label="$t(item.name)" :required="item.required ?? true">
|
||||
<el-input v-if="!item.options" v-model="modelValue[item.model]" :placeholder="$t(item.placeholder)" autocomplete="off" clearable></el-input>
|
||||
|
||||
<el-select
|
||||
v-else
|
||||
v-model="modelValue[item.model]"
|
||||
:placeholder="$t(item.placeholder)"
|
||||
filterable
|
||||
autocomplete="off"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
formItems: { type: Array },
|
||||
});
|
||||
|
||||
const formRef: any = ref();
|
||||
|
||||
const modelValue: any = defineModel();
|
||||
|
||||
const validate = async (func: any) => {
|
||||
await formRef.value.validate(func);
|
||||
};
|
||||
|
||||
const resetFields = () => {
|
||||
formRef.value.resetFields();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
validate,
|
||||
resetFields,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
52
frontend/src/components/dynamic-form/DynamicFormDialog.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="form-dialog">
|
||||
<el-dialog @close="close" v-bind="$attrs" :title="title" v-model="dialogVisible" :width="width">
|
||||
<dynamic-form ref="df" :form-items="props.formItems" v-model="formData" />
|
||||
|
||||
<template #footer>
|
||||
<span>
|
||||
<slot name="btns">
|
||||
<el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirm">{{ $t('common.confirm') }}</el-button>
|
||||
</slot>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import DynamicForm from './DynamicForm.vue';
|
||||
|
||||
const emit = defineEmits(['close', 'confirm']);
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String },
|
||||
width: { type: [String, Number], default: '500px' },
|
||||
formItems: { type: Array },
|
||||
});
|
||||
|
||||
const df: any = ref();
|
||||
|
||||
const formData: any = defineModel('modelValue');
|
||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
// 取消动态表单的校验
|
||||
setTimeout(() => {
|
||||
formData.value = {};
|
||||
df.value.resetFields();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
df.value.validate((valid: any) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
emit('confirm', formData.value);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
58
frontend/src/components/dynamic-form/DynamicFormEdit.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="dynamic-form-edit w100">
|
||||
<el-table :data="formItems" stripe class="w100">
|
||||
<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>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row['model']" :placeholder="$t('components.df.fieldModelPlaceholder')" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" :label="$t('components.df.fieldLabel')" min-width="100px">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row['name']" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="placeholder" :label="$t('components.df.fieldPlaceholder')" min-width="140px">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row['placeholder']" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="options" :label="$t('components.df.optionalValues')" min-width="140px">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row['options']" :placeholder="$t('components.df.optionalValuesPlaceholder')" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="required" :label="$t('components.df.required')" min-width="65px">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row['required']" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('common.operation')" wdith="20px">
|
||||
<template #default="scope">
|
||||
<el-button type="danger" @click="deleteItem(scope.$index)" icon="delete" plain></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const formItems: any = defineModel('modelValue');
|
||||
|
||||
const addItem = () => {
|
||||
formItems.value.push({});
|
||||
};
|
||||
|
||||
const deleteItem = (index: any) => {
|
||||
formItems.value.splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as DynamicForm } from './DynamicForm.vue';
|
||||
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';
|
||||
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';
|
||||
export { default as DynamicFormEdit } from './DynamicFormEdit.vue';
|
||||
86
frontend/src/components/echarts/ECharts.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div id="echarts" ref="chartRef" :style="echartsStyle" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ECharts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch, computed, markRaw, nextTick } from 'vue';
|
||||
import { EChartsType, ECElementEvent } from 'echarts/core';
|
||||
import echarts, { ECOption } from './config';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
import { light } from './config/theme';
|
||||
// import { useThemeConfig } from '@/store/themeConfig';
|
||||
// import { storeToRefs } from 'pinia';
|
||||
|
||||
interface Props {
|
||||
option: ECOption;
|
||||
renderer?: 'canvas' | 'svg';
|
||||
resize?: boolean;
|
||||
theme?: Object | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
onClick?: (event: ECElementEvent) => any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
renderer: 'canvas',
|
||||
theme: light as any,
|
||||
resize: true,
|
||||
});
|
||||
|
||||
const echartsStyle = computed(() => {
|
||||
return props.width || props.height ? { height: props.height + 'px', width: props.width + 'px' } : { height: '100%', width: '100%' };
|
||||
});
|
||||
|
||||
const chartRef = ref<HTMLDivElement | HTMLCanvasElement>();
|
||||
const chartInstance = ref<EChartsType>();
|
||||
|
||||
const draw = () => {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.setOption(props.option, { notMerge: true });
|
||||
}
|
||||
};
|
||||
|
||||
watch(props, () => {
|
||||
draw();
|
||||
});
|
||||
|
||||
const handleClick = (event: ECElementEvent) => props.onClick && props.onClick(event);
|
||||
|
||||
const init = () => {
|
||||
if (!chartRef.value) return;
|
||||
chartInstance.value = echarts.getInstanceByDom(chartRef.value);
|
||||
|
||||
if (!chartInstance.value) {
|
||||
chartInstance.value = markRaw(
|
||||
echarts.init(chartRef.value, props.theme, {
|
||||
renderer: props.renderer,
|
||||
})
|
||||
);
|
||||
chartInstance.value.on('click', handleClick);
|
||||
draw();
|
||||
}
|
||||
};
|
||||
|
||||
const resize = () => {
|
||||
if (chartInstance.value && props.resize) {
|
||||
chartInstance.value.resize({ animation: { duration: 300 } });
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedResize = useDebounceFn(resize, 300, { maxWait: 800 });
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => init());
|
||||
useEventListener('resize', debouncedResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
chartInstance.value?.dispose();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
getInstance: () => chartInstance.value,
|
||||
resize,
|
||||
draw,
|
||||
});
|
||||
</script>
|
||||
67
frontend/src/components/echarts/config/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as echarts from 'echarts/core';
|
||||
import { BarChart, LineChart, LinesChart, PieChart, ScatterChart, RadarChart, GaugeChart } from 'echarts/charts';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
PolarComponent,
|
||||
GeoComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components';
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
LineSeriesOption,
|
||||
LinesSeriesOption,
|
||||
PieSeriesOption,
|
||||
ScatterSeriesOption,
|
||||
RadarSeriesOption,
|
||||
GaugeSeriesOption,
|
||||
} from 'echarts/charts';
|
||||
import type { TitleComponentOption, TooltipComponentOption, GridComponentOption, DatasetComponentOption } from 'echarts/components';
|
||||
import type { ComposeOption } from 'echarts/core';
|
||||
// import 'echarts-liquidfill';
|
||||
|
||||
export type ECOption = ComposeOption<
|
||||
| BarSeriesOption
|
||||
| LineSeriesOption
|
||||
| LinesSeriesOption
|
||||
| PieSeriesOption
|
||||
| RadarSeriesOption
|
||||
| GaugeSeriesOption
|
||||
| TitleComponentOption
|
||||
| TooltipComponentOption
|
||||
| GridComponentOption
|
||||
| DatasetComponentOption
|
||||
| ScatterSeriesOption
|
||||
>;
|
||||
|
||||
echarts.use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
PolarComponent,
|
||||
GeoComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
LinesChart,
|
||||
PieChart,
|
||||
ScatterChart,
|
||||
RadarChart,
|
||||
GaugeChart,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
export default echarts;
|
||||
151
frontend/src/components/echarts/config/theme.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const light = {
|
||||
seriesCnt: '4',
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
titleColor: '#008acd',
|
||||
subtitleColor: '#aaaaaa',
|
||||
textColorShow: false,
|
||||
textColor: '#333',
|
||||
markTextColor: '#eeeeee',
|
||||
color: [
|
||||
'#2ec7c9',
|
||||
'#b6a2de',
|
||||
'#5ab1ef',
|
||||
'#ffb980',
|
||||
'#d87a80',
|
||||
'#8d98b3',
|
||||
'#e5cf0d',
|
||||
'#97b552',
|
||||
'#95706d',
|
||||
'#dc69aa',
|
||||
'#07a2a4',
|
||||
'#9a7fd1',
|
||||
'#588dd5',
|
||||
'#f5994e',
|
||||
'#c05050',
|
||||
'#59678c',
|
||||
'#c9ab00',
|
||||
'#7eb00a',
|
||||
'#6f5553',
|
||||
'#c14089',
|
||||
],
|
||||
borderColor: '#ccc',
|
||||
borderWidth: 0,
|
||||
visualMapColor: ['#5ab1ef', '#e0ffff'],
|
||||
legendTextColor: '#333333',
|
||||
kColor: '#d87a80',
|
||||
kColor0: '#2ec7c9',
|
||||
kBorderColor: '#d87a80',
|
||||
kBorderColor0: '#2ec7c9',
|
||||
kBorderWidth: 1,
|
||||
lineWidth: 2,
|
||||
symbolSize: 3,
|
||||
symbol: 'emptyCircle',
|
||||
symbolBorderWidth: 1,
|
||||
lineSmooth: true,
|
||||
graphLineWidth: 1,
|
||||
graphLineColor: '#aaaaaa',
|
||||
mapLabelColor: '#d87a80',
|
||||
mapLabelColorE: 'rgb(100,0,0)',
|
||||
mapBorderColor: '#eeeeee',
|
||||
mapBorderColorE: '#444',
|
||||
mapBorderWidth: 0.5,
|
||||
mapBorderWidthE: 1,
|
||||
mapAreaColor: '#dddddd',
|
||||
mapAreaColorE: 'rgba(254,153,78,1)',
|
||||
axes: [
|
||||
{
|
||||
type: 'all',
|
||||
name: '通用坐标轴',
|
||||
axisLineShow: true,
|
||||
axisLineColor: '#eeeeee',
|
||||
axisTickShow: true,
|
||||
axisTickColor: '#eeeeee',
|
||||
axisLabelShow: true,
|
||||
axisLabelColor: '#eeeeee',
|
||||
splitLineShow: true,
|
||||
splitLineColor: ['#aaaaaa'],
|
||||
splitAreaShow: false,
|
||||
splitAreaColor: ['#eeeeee'],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
name: '类目坐标轴',
|
||||
axisLineShow: true,
|
||||
axisLineColor: '#008acd',
|
||||
axisTickShow: true,
|
||||
axisTickColor: '#333',
|
||||
axisLabelShow: true,
|
||||
axisLabelColor: '#333',
|
||||
splitLineShow: false,
|
||||
splitLineColor: ['#eee'],
|
||||
splitAreaShow: false,
|
||||
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '数值坐标轴',
|
||||
axisLineShow: true,
|
||||
axisLineColor: '#008acd',
|
||||
axisTickShow: true,
|
||||
axisTickColor: '#333',
|
||||
axisLabelShow: true,
|
||||
axisLabelColor: '#333',
|
||||
splitLineShow: true,
|
||||
splitLineColor: ['#eee'],
|
||||
splitAreaShow: true,
|
||||
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||
},
|
||||
{
|
||||
type: 'log',
|
||||
name: '对数坐标轴',
|
||||
axisLineShow: true,
|
||||
axisLineColor: '#008acd',
|
||||
axisTickShow: true,
|
||||
axisTickColor: '#333',
|
||||
axisLabelShow: true,
|
||||
axisLabelColor: '#333',
|
||||
splitLineShow: true,
|
||||
splitLineColor: ['#eee'],
|
||||
splitAreaShow: true,
|
||||
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||
},
|
||||
{
|
||||
type: 'time',
|
||||
name: '时间坐标轴',
|
||||
axisLineShow: true,
|
||||
axisLineColor: '#008acd',
|
||||
axisTickShow: true,
|
||||
axisTickColor: '#333',
|
||||
axisLabelShow: true,
|
||||
axisLabelColor: '#333',
|
||||
splitLineShow: true,
|
||||
splitLineColor: ['#eee'],
|
||||
splitAreaShow: false,
|
||||
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||
},
|
||||
],
|
||||
axisSeperateSetting: true,
|
||||
toolboxColor: '#2ec7c9',
|
||||
toolboxEmphasisColor: '#18a4a6',
|
||||
tooltipAxisColor: '#008acd',
|
||||
tooltipAxisWidth: '1',
|
||||
timelineLineColor: '#008acd',
|
||||
timelineLineWidth: 1,
|
||||
timelineItemColor: '#008acd',
|
||||
timelineItemColorE: '#a9334c',
|
||||
timelineCheckColor: '#2ec7c9',
|
||||
timelineCheckBorderColor: '#2ec7c9',
|
||||
timelineItemBorderWidth: 1,
|
||||
timelineControlColor: '#008acd',
|
||||
timelineControlBorderColor: '#008acd',
|
||||
timelineControlBorderWidth: 0.5,
|
||||
timelineLabelColor: '#008acd',
|
||||
datazoomBackgroundColor: 'rgba(47,69,84,0)',
|
||||
datazoomDataColor: '#efefff',
|
||||
datazoomFillColor: 'rgba(182,162,222,0.2)',
|
||||
datazoomHandleColor: '#008acd',
|
||||
datazoomHandleWidth: '100',
|
||||
datazoomLabelColor: '#333333',
|
||||
};
|
||||
|
||||
export { light };
|
||||
17
frontend/src/components/enumselect/EnumSelect.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<el-select v-bind="$attrs" v-model="modelValue">
|
||||
<el-option v-for="item in props.enums" :key="item.value" :label="$t(item.label)" :value="item.value"> </el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
enums: {
|
||||
type: Object, // 需要为EnumValue类型
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue: any = defineModel('modelValue');
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
60
frontend/src/components/enumtag/EnumTag.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<el-tag :disable-transitions="true" v-bind="$attrs" :type="type" :color="color" effect="plain">{{ $t(enumLabel) }}</el-tag>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, reactive, onMounted } from 'vue';
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
const props = defineProps({
|
||||
enums: {
|
||||
type: Object, // 需要为EnumValue类型
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [Object, String, Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultType = 'primary';
|
||||
|
||||
const state = reactive({
|
||||
type: defaultType,
|
||||
color: '',
|
||||
enumLabel: '',
|
||||
});
|
||||
|
||||
const { type, color, enumLabel } = toRefs(state);
|
||||
|
||||
// 监听该值是否改变,改变则需要将其枚举值与标签进行调整
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue: any) => {
|
||||
convert(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
convert(props.value);
|
||||
});
|
||||
|
||||
const convert = (value: any) => {
|
||||
const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
|
||||
if (!enumValue) {
|
||||
state.enumLabel = '-';
|
||||
state.type = 'danger';
|
||||
state.color = '';
|
||||
return;
|
||||
}
|
||||
|
||||
state.enumLabel = enumValue?.label || '';
|
||||
if (enumValue.tag) {
|
||||
state.color = enumValue.tag.color;
|
||||
state.type = enumValue.tag.type;
|
||||
} else {
|
||||
state.type = defaultType;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
60
frontend/src/components/file/FileInfo.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<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>
|
||||
|
||||
{{ fileDetail?.filename }}
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, 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,
|
||||
required: true,
|
||||
},
|
||||
files: {
|
||||
type: [Array],
|
||||
},
|
||||
canDownload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
setFileInfo();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.fileKey,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
setFileInfo();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const files = await openApi.getFileDetail([props.fileKey]);
|
||||
fileDetail.value = files?.[0];
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
239
frontend/src/components/iconSelector/index.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="icon-selector w100 h100">
|
||||
<el-input
|
||||
v-model="state.fontIconSearch"
|
||||
:placeholder="state.fontIconPlaceholder"
|
||||
:clearable="clearable"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
ref="inputWidthRef"
|
||||
@clear="onClearFontIcon"
|
||||
@focus="onIconFocus"
|
||||
@blur="onIconBlur"
|
||||
>
|
||||
<template #prepend>
|
||||
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
:width="state.fontIconWidth"
|
||||
transition="el-zoom-in-top"
|
||||
popper-class="icon-selector-popper"
|
||||
trigger="click"
|
||||
:virtual-ref="inputWidthRef"
|
||||
virtual-triggering
|
||||
>
|
||||
<template #default>
|
||||
<div class="icon-selector-warp">
|
||||
<div class="ml10 mt10">{{ title }}</div>
|
||||
<el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
|
||||
<el-tab-pane lazy label="ele" name="ele">
|
||||
<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy label="ali" name="ali">
|
||||
<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
|
||||
</el-tab-pane>
|
||||
<!-- <el-tab-pane lazy label="awe" name="awe">
|
||||
<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="iconSelector">
|
||||
import { defineAsyncComponent, ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
|
||||
import type { TabsPaneContext } from 'element-plus';
|
||||
import initIconfont from '@/common/utils/svgIcons';
|
||||
import '@/theme/iconSelector.scss';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
// 输入框前置内容
|
||||
prepend: {
|
||||
type: String,
|
||||
default: () => 'Pointer',
|
||||
},
|
||||
// 输入框占位文本
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: () => '请输入内容搜索图标或者选择图标',
|
||||
},
|
||||
// 输入框占位文本
|
||||
size: {
|
||||
type: String,
|
||||
default: () => 'default',
|
||||
},
|
||||
// 弹窗标题
|
||||
title: {
|
||||
type: String,
|
||||
default: () => '请选择图标',
|
||||
},
|
||||
// 禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
// 是否可清空
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: () => true,
|
||||
},
|
||||
// 自定义空状态描述文字
|
||||
emptyDescription: {
|
||||
type: String,
|
||||
default: () => '无相关图标',
|
||||
},
|
||||
// 双向绑定值,默认为 modelValue,
|
||||
// 参考:https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
|
||||
// 参考:https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
|
||||
modelValue: String,
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
const emit = defineEmits(['update:modelValue', 'get', 'clear']);
|
||||
|
||||
// 引入组件
|
||||
const IconList = defineAsyncComponent(() => import('@/components/iconSelector/list.vue'));
|
||||
|
||||
// 定义变量内容
|
||||
const inputWidthRef = ref();
|
||||
const state = reactive({
|
||||
fontIconPrefix: '',
|
||||
fontIconWidth: 0,
|
||||
fontIconSearch: '',
|
||||
fontIconPlaceholder: '',
|
||||
fontIconTabActive: 'ele',
|
||||
fontIconList: {
|
||||
ali: [],
|
||||
ele: [],
|
||||
awe: [],
|
||||
},
|
||||
});
|
||||
|
||||
// 处理 input 获取焦点时,modelValue 有值时,改变 input 的 placeholder 值
|
||||
const onIconFocus = () => {
|
||||
if (!props.modelValue) return false;
|
||||
state.fontIconSearch = '';
|
||||
state.fontIconPlaceholder = props.modelValue;
|
||||
};
|
||||
// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
|
||||
const onIconBlur = () => {
|
||||
const list = fontIconTabNameList();
|
||||
setTimeout(() => {
|
||||
const icon = list.filter((icon: string) => icon === state.fontIconSearch);
|
||||
if (icon.length <= 0) state.fontIconSearch = '';
|
||||
}, 300);
|
||||
};
|
||||
// 图标搜索及图标数据显示
|
||||
const fontIconSheetsFilterList = computed(() => {
|
||||
const list = fontIconTabNameList();
|
||||
if (!state.fontIconSearch) return list;
|
||||
let search = state.fontIconSearch.trim().toLowerCase();
|
||||
return list.filter((item: string) => {
|
||||
if (item.toLowerCase().indexOf(search) !== -1) return item;
|
||||
});
|
||||
});
|
||||
// 根据 tab name 类型设置图标
|
||||
const fontIconTabNameList = () => {
|
||||
let iconList: any = [];
|
||||
if (state.fontIconTabActive === 'ali') iconList = state.fontIconList.ali;
|
||||
else if (state.fontIconTabActive === 'ele') iconList = state.fontIconList.ele;
|
||||
else if (state.fontIconTabActive === 'awe') iconList = state.fontIconList.awe;
|
||||
return iconList;
|
||||
};
|
||||
// 处理 icon 双向绑定数值回显
|
||||
const initModeValueEcho = () => {
|
||||
if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder);
|
||||
(<string | undefined>state.fontIconPlaceholder) = props.modelValue;
|
||||
(<string | undefined>state.fontIconPrefix) = props.modelValue;
|
||||
};
|
||||
// 处理 icon 类型,用于回显时,tab 高亮与初始化数据
|
||||
const initFontIconName = () => {
|
||||
let name = 'ele';
|
||||
if (props.modelValue!.indexOf('iconfont') > -1) {
|
||||
name = 'ali';
|
||||
} else {
|
||||
name = 'ele';
|
||||
}
|
||||
// else if (props.modelValue!.indexOf('ele-') > -1) name = 'ele';
|
||||
// else if (props.modelValue!.indexOf('fa') > -1) name = 'awe';
|
||||
// 初始化 tab 高亮回显
|
||||
state.fontIconTabActive = name;
|
||||
return name;
|
||||
};
|
||||
// 初始化数据
|
||||
const initFontIconData = async (name: string) => {
|
||||
if (name === 'ali') {
|
||||
// 阿里字体图标使用 `iconfont xxx`
|
||||
if (state.fontIconList.ali.length > 0) return;
|
||||
const res: any = await initIconfont.ali();
|
||||
state.fontIconList.ali = res.map((i: string) => `iconfont ${i}`);
|
||||
} else if (name === 'ele') {
|
||||
// element plus 图标
|
||||
if (state.fontIconList.ele.length > 0) return;
|
||||
await initIconfont.ele().then((res: any) => {
|
||||
state.fontIconList.ele = res;
|
||||
});
|
||||
} else if (name === 'awe') {
|
||||
// fontawesome字体图标使用 `fa xxx`
|
||||
// if (state.fontIconList.awe.length > 0) return;
|
||||
// await initIconfont.awe().then((res: any) => {
|
||||
// state.fontIconList.awe = res.map((i: string) => `fa ${i}`);
|
||||
// });
|
||||
}
|
||||
// 初始化 input 的 placeholder
|
||||
// 参考(单项数据流):https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
|
||||
state.fontIconPlaceholder = props.placeholder;
|
||||
// 初始化双向绑定回显
|
||||
initModeValueEcho();
|
||||
};
|
||||
// 图标点击切换
|
||||
const onIconClick = (pane: TabsPaneContext) => {
|
||||
initFontIconData(pane.paneName as string);
|
||||
inputWidthRef.value.focus();
|
||||
};
|
||||
// 获取当前点击的 icon 图标
|
||||
const onColClick = (v: string) => {
|
||||
state.fontIconPlaceholder = v;
|
||||
state.fontIconPrefix = v;
|
||||
emit('get', state.fontIconPrefix);
|
||||
emit('update:modelValue', state.fontIconPrefix);
|
||||
inputWidthRef.value.focus();
|
||||
};
|
||||
// 清空当前点击的 icon 图标
|
||||
const onClearFontIcon = () => {
|
||||
state.fontIconPrefix = '';
|
||||
emit('clear', state.fontIconPrefix);
|
||||
emit('update:modelValue', state.fontIconPrefix);
|
||||
};
|
||||
// 获取 input 的宽度
|
||||
const getInputWidth = () => {
|
||||
nextTick(() => {
|
||||
state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
|
||||
});
|
||||
};
|
||||
// 监听页面宽度改变
|
||||
const initResize = () => {
|
||||
window.addEventListener('resize', () => {
|
||||
getInputWidth();
|
||||
});
|
||||
};
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
initFontIconData(initFontIconName());
|
||||
initResize();
|
||||
getInputWidth();
|
||||
});
|
||||
// 监听双向绑定 modelValue 的变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
initModeValueEcho();
|
||||
initFontIconName();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
84
frontend/src/components/iconSelector/list.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="icon-selector-warp-row">
|
||||
<el-scrollbar ref="selectorScrollbarRef">
|
||||
<el-row :gutter="10" v-if="props.list.length > 0">
|
||||
<el-col :xs="6" :sm="4" :md="4" :lg="4" :xl="4" v-for="(v, k) in list" :key="k" @click="onColClick(v)">
|
||||
<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': prefix === v }">
|
||||
<SvgIcon :name="v" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty :image-size="100" v-if="list.length <= 0" :description="empty"></el-empty>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="iconSelectorList">
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
// 图标列表数据
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 自定义空状态描述文字
|
||||
empty: {
|
||||
type: String,
|
||||
default: () => '无相关图标',
|
||||
},
|
||||
// 高亮当前选中图标
|
||||
prefix: {
|
||||
type: String,
|
||||
default: () => '',
|
||||
},
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
const emit = defineEmits(['get-icon']);
|
||||
|
||||
// 当前 icon 图标点击时
|
||||
const onColClick = (v: unknown | string) => {
|
||||
emit('get-icon', v);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-selector-warp-row {
|
||||
height: 230px;
|
||||
overflow: hidden;
|
||||
.el-row {
|
||||
padding: 15px;
|
||||
}
|
||||
.el-scrollbar__bar.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
.icon-selector-warp-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
height: 30px;
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-5);
|
||||
i {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon-selector-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-5);
|
||||
i {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,40 @@
|
||||
<template>
|
||||
<div class="monaco-editor" style="border: 1px solid #ccc;">
|
||||
<div ref="monacoTextarea" :style="{ height: height }"></div>
|
||||
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
|
||||
<el-option v-for="mode in languages" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
|
||||
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5); height: 100%">
|
||||
<div class="monaco-editor-content" ref="monacoTextarea" :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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, toRefs, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
// 相关语言
|
||||
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/dockerfile/dockerfile.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/html/html.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/css/css.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/python/python.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/java/java.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js';
|
||||
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
|
||||
// 右键菜单
|
||||
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/caretOperations.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/clipboard//browser/clipboard.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController.js';
|
||||
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 JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
||||
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'
|
||||
@@ -21,10 +43,15 @@ import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
||||
// 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 SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
|
||||
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
||||
import { ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
@@ -49,12 +76,12 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const languages = [
|
||||
const languageArr = [
|
||||
{
|
||||
value: 'shell',
|
||||
label: 'Shell',
|
||||
@@ -105,15 +132,15 @@ const languages = [
|
||||
},
|
||||
];
|
||||
|
||||
const options = {
|
||||
const defaultOptions = {
|
||||
language: 'shell',
|
||||
theme: 'SolarizedLight',
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
foldingStrategy: 'indentation',//代码可分小段折叠
|
||||
foldingStrategy: 'indentation', //代码可分小段折叠
|
||||
roundedSelection: false, // 禁用选择文本背景的圆角
|
||||
matchBrackets: 'near',
|
||||
linkedEditing: true,
|
||||
cursorBlinking: 'smooth',// 光标闪烁样式
|
||||
cursorBlinking: 'smooth', // 光标闪烁样式
|
||||
mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时,在编辑器中缩放字体
|
||||
overviewRulerBorder: false, // 不要滚动条的边框
|
||||
tabSize: 4, // tab 缩进长度
|
||||
@@ -125,15 +152,27 @@ const options = {
|
||||
minimap: {
|
||||
enabled: false, // 不要小地图
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const monacoTextarea: any = ref();
|
||||
|
||||
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
|
||||
let completionItemProvider: any = null;
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_: any, label: string) {
|
||||
if (label === 'json') {
|
||||
return new JsonWorker();
|
||||
}
|
||||
return new EditorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
languageMode: 'shell',
|
||||
})
|
||||
});
|
||||
|
||||
const {
|
||||
languageMode,
|
||||
} = toRefs(state)
|
||||
const { languageMode } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
state.languageMode = props.language;
|
||||
@@ -147,124 +186,129 @@ onBeforeUnmount(() => {
|
||||
monacoEditorIns.dispose();
|
||||
}
|
||||
if (completionItemProvider) {
|
||||
console.log('unmount=> dispose completion item provider');
|
||||
completionItemProvider.dispose();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (newValue: any) => {
|
||||
if (!monacoEditorIns.hasTextFocus()) {
|
||||
state.languageMode = props.language;
|
||||
monacoEditorIns?.setValue(newValue);
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.language, (newValue: any) => {
|
||||
changeLanguage(newValue);
|
||||
})
|
||||
|
||||
|
||||
const monacoTextarea: any = ref(null);
|
||||
|
||||
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
|
||||
let completionItemProvider: any = null;
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_: any, label: string) {
|
||||
if (label === 'json') {
|
||||
return new JsonWorker()
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue: any) => {
|
||||
if (!monacoEditorIns.hasTextFocus()) {
|
||||
state.languageMode = props.language;
|
||||
monacoEditorIns?.setValue(newValue);
|
||||
}
|
||||
return new EditorWorker();
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.language,
|
||||
(newValue: any) => {
|
||||
changeLanguage(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 themeConfig editorTheme配置文件的变化
|
||||
watch(
|
||||
() => themeConfig.value.editorTheme,
|
||||
(val) => {
|
||||
console.log('monaco editor theme change: ', val);
|
||||
monaco?.editor?.setTheme(val);
|
||||
}
|
||||
);
|
||||
|
||||
const initMonacoEditorIns = () => {
|
||||
console.log('初始化monaco编辑器')
|
||||
console.log('初始化monaco编辑器');
|
||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||
// 初始化一些主题
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
options.language = state.languageMode;
|
||||
// 从localStorage中获取,通过store可能存在父子组件都使用store报错
|
||||
options.theme = JSON.parse(localStorage.getItem('themeConfig') as string).editorTheme || 'vs';
|
||||
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(options, props.options as any));
|
||||
defaultOptions.language = state.languageMode;
|
||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(defaultOptions, props.options as any));
|
||||
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
emit('update:modelValue', monacoEditorIns.getModel()?.getValue());
|
||||
})
|
||||
|
||||
// 动态设置主题
|
||||
// monaco.editor.setTheme('hc-black');
|
||||
});
|
||||
};
|
||||
|
||||
const changeLanguage = (value: any) => {
|
||||
console.log('change lan');
|
||||
// 获取当前的文档模型
|
||||
let oldModel = monacoEditorIns.getModel()
|
||||
let oldModel = monacoEditorIns.getModel();
|
||||
if (!oldModel) {
|
||||
return;
|
||||
}
|
||||
// 创建一个新的文档模型
|
||||
let newModel = monaco.editor.createModel(oldModel.getValue(), value)
|
||||
let newModel = monaco.editor.createModel(oldModel.getValue(), value);
|
||||
// 设置成新的
|
||||
monacoEditorIns.setModel(newModel)
|
||||
monacoEditorIns.setModel(newModel);
|
||||
// 销毁旧的模型
|
||||
if (oldModel) {
|
||||
oldModel.dispose()
|
||||
oldModel.dispose();
|
||||
}
|
||||
|
||||
registerCompletionItemProvider();
|
||||
}
|
||||
};
|
||||
|
||||
const setEditorValue = (value: any) => {
|
||||
monacoEditorIns.getModel()?.setValue(value)
|
||||
}
|
||||
if (value) {
|
||||
monacoEditorIns.getModel()?.setValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册联想补全提示
|
||||
*/
|
||||
const registerCompletionItemProvider = () => {
|
||||
if (completionItemProvider) {
|
||||
console.log('exist competion item provider, dispose now');
|
||||
completionItemProvider.dispose();
|
||||
}
|
||||
if (state.languageMode == 'shell') {
|
||||
registeShell()
|
||||
registeShell();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const registeShell = () => {
|
||||
completionItemProvider = monaco.languages.registerCompletionItemProvider('shell', {
|
||||
provideCompletionItems: async () => {
|
||||
let suggestions: languages.CompletionItem[] = []
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
shellLan.keywords.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: item,
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: item,
|
||||
} as any);
|
||||
})
|
||||
});
|
||||
shellLan.builtins.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: item,
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
insertText: item,
|
||||
} as any);
|
||||
})
|
||||
});
|
||||
return {
|
||||
suggestions: suggestions
|
||||
suggestions: suggestions,
|
||||
};
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const format = () => {
|
||||
/*
|
||||
触发自动格式化;
|
||||
*/
|
||||
monacoEditorIns.trigger('', 'editor.action.formatDocument', '')
|
||||
}
|
||||
// 触发自动格式化;
|
||||
monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
|
||||
};
|
||||
|
||||
defineExpose({ format })
|
||||
const focus = () => {
|
||||
monacoEditorIns.focus();
|
||||
};
|
||||
|
||||
const getEditor = () => {
|
||||
return monacoEditorIns;
|
||||
};
|
||||
|
||||
defineExpose({ getEditor, format, focus });
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
37
frontend/src/components/monaco/MonacoEditorDialog.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { VNode, h, render } from 'vue';
|
||||
import MonacoEditorDialogComp from './MonacoEditorDialogComp.vue';
|
||||
|
||||
export type MonacoEditorDialogProps = {
|
||||
content: string;
|
||||
title: string;
|
||||
language: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
confirmFn?: Function; // 点击确认的回调函数,入参editor value
|
||||
cancelFn?: Function; // 点击取消 或 关闭弹窗的回调函数
|
||||
};
|
||||
|
||||
const boxId = 'monaco-editor-dialog-id';
|
||||
|
||||
let boxInstance: VNode;
|
||||
|
||||
const MonacoEditorDialog = (props: MonacoEditorDialogProps): void => {
|
||||
if (!boxInstance) {
|
||||
const container = document.createElement('div');
|
||||
container.id = boxId;
|
||||
// 创建 虚拟dom
|
||||
boxInstance = h(MonacoEditorDialogComp);
|
||||
// 将虚拟dom渲染到 container dom 上
|
||||
render(boxInstance, container);
|
||||
// 最后将 container 追加到 body 上
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const boxVue = boxInstance.component;
|
||||
if (boxVue) {
|
||||
// 调用open方法显示弹框,注意不能使用boxVue.ctx来调用组件函数(build打包后ctx会获取不到)
|
||||
boxVue.exposed?.open(props);
|
||||
}
|
||||
};
|
||||
|
||||
export default MonacoEditorDialog;
|
||||
136
frontend/src/components/monaco/MonacoEditorDialogComp.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :title="state.title" v-model="state.dialogVisible" :width="state.width" @close="cancel">
|
||||
<monaco-editor ref="editorRef" :height="state.height" class="editor" :language="state.language" v-model="contentValue" can-change-mode />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="cancel">取消</el-button>
|
||||
<el-button @click="confirm" type="primary">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, reactive } from 'vue';
|
||||
import { ElDialog, ElButton, ElMessage } from 'element-plus';
|
||||
// import base style
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { MonacoEditorDialogProps } from './MonacoEditorDialog';
|
||||
|
||||
const editorRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
height: '450px',
|
||||
width: '800px',
|
||||
contentValue: '',
|
||||
title: '',
|
||||
language: '',
|
||||
});
|
||||
|
||||
let confirmFn: any;
|
||||
let cancelFn: any;
|
||||
|
||||
const { contentValue } = toRefs(state);
|
||||
|
||||
function compressHTML(html: string) {
|
||||
return (
|
||||
html
|
||||
.replace(/[\r\n\t]+/g, ' ') // 移除换行符和制表符
|
||||
// .replace(/<!--[\s\S]*?-->/g, '') // 移除注释
|
||||
.replace(/\s{2,}/g, ' ') // 合并多个空格为一个空格
|
||||
.replace(/>\s+</g, '><')
|
||||
); // 移除标签之间的空格
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认按钮
|
||||
*/
|
||||
const confirm = async () => {
|
||||
if (confirmFn) {
|
||||
if (state.language === 'json') {
|
||||
let val;
|
||||
try {
|
||||
val = JSON.parse(contentValue.value);
|
||||
if (typeof val !== 'object') {
|
||||
ElMessage.error('请输入正确的json');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('请输入正确的json');
|
||||
return;
|
||||
}
|
||||
|
||||
// 压缩json字符串
|
||||
confirmFn(JSON.stringify(val));
|
||||
} else if (state.language === 'html') {
|
||||
// 压缩html字符串
|
||||
confirmFn(compressHTML(contentValue.value));
|
||||
} else {
|
||||
confirmFn(contentValue.value);
|
||||
}
|
||||
}
|
||||
state.dialogVisible = false;
|
||||
setTimeout(() => {
|
||||
state.contentValue = '';
|
||||
state.title = '';
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
state.dialogVisible = false;
|
||||
// 没有执行成功,并且取消回调函数存在,则执行
|
||||
cancelFn && cancelFn();
|
||||
setTimeout(() => {
|
||||
state.contentValue = '';
|
||||
state.title = '';
|
||||
}, 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);
|
||||
};
|
||||
|
||||
const open = (optionProps: MonacoEditorDialogProps) => {
|
||||
confirmFn = optionProps.confirmFn;
|
||||
cancelFn = optionProps.cancelFn;
|
||||
|
||||
const language = optionProps.language;
|
||||
state.language = language;
|
||||
state.title = optionProps.title;
|
||||
if (optionProps.height) {
|
||||
state.height = optionProps.height;
|
||||
}
|
||||
|
||||
state.contentValue = optionProps.content;
|
||||
// 格式化输出html;
|
||||
if (language === 'html' || language == 'xml') {
|
||||
state.contentValue = formatXML(optionProps.content);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
editorRef.value?.focus();
|
||||
editorRef.value?.format();
|
||||
}, 300);
|
||||
|
||||
state.dialogVisible = true;
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.editor {
|
||||
font-size: 9pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
25
frontend/src/components/monaco/completionItemProvider.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
/**
|
||||
* key: language, value: CompletionItemProvider
|
||||
*/
|
||||
const completionItemProviders: Map<string, any> = new Map();
|
||||
|
||||
export function registerCompletionItemProvider(language: string, completionItemProvider: any, replace: boolean = true) {
|
||||
const exist = completionItemProviders.get(language);
|
||||
if (exist) {
|
||||
if (!replace) {
|
||||
return;
|
||||
}
|
||||
exist.dispose();
|
||||
}
|
||||
completionItemProviders.set(language, monaco.languages.registerCompletionItemProvider(language, completionItemProvider));
|
||||
}
|
||||
|
||||
export function dispposeCompletionItemProvider(language: string) {
|
||||
const exist = completionItemProviders.get(language);
|
||||
if (exist) {
|
||||
exist.dispose();
|
||||
completionItemProviders.delete(language);
|
||||
}
|
||||
}
|
||||
439
frontend/src/components/pagetable/PageTable.vue
Normal file
@@ -0,0 +1,439 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="el-zoom-in-top">
|
||||
<!-- 查询表单 -->
|
||||
<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>
|
||||
</template>
|
||||
</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>
|
||||
</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">
|
||||
<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">
|
||||
<!-- 插槽:预留功能 -->
|
||||
<template #default="scope" v-if="item.slot">
|
||||
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
|
||||
</template>
|
||||
|
||||
<!-- 枚举类型使用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>
|
||||
</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">
|
||||
<template #default>
|
||||
<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>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<span>{{ item.getValueByData(scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue } 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 SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { usePageTable } from '@/hooks/usePageTable';
|
||||
import { ElTable } from 'element-plus';
|
||||
|
||||
|
||||
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
|
||||
|
||||
export interface PageTableProps {
|
||||
size?: string;
|
||||
pageApi?: Api; // 请求表格数据的 api
|
||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||
showSelection?: boolean;
|
||||
selectable?: (row: any) => boolean; // 是否可选
|
||||
pageable?: boolean;
|
||||
showSearch?: boolean; // 是否显示搜索表单
|
||||
data?: any[]; // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ==> 非必传
|
||||
lazy?: boolean; // 是否自动执行请求 api ==> 非必传(默认为false)
|
||||
beforeQueryFn?: (params: any) => any; // 执行查询时对查询参数进行处理,调整等
|
||||
dataHandlerFn?: (data: any) => any; // 数据处理回调函数,用于将请求回来的数据二次加工处理等
|
||||
searchItems?: SearchItem[];
|
||||
border?: boolean; // 是否带有纵向边框 ==> 非必传(默认为false)
|
||||
toolButton?: ('setting' | 'search')[] | boolean; // 是否显示表格功能按钮 ==> 非必传(默认为true)
|
||||
searchCol?: any; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 } | number 如 3
|
||||
}
|
||||
|
||||
// 接受父组件参数,配置默认值
|
||||
const props = withDefaults(defineProps<PageTableProps>(), {
|
||||
columns: () => [],
|
||||
pageable: true,
|
||||
showSelection: false,
|
||||
lazy: false,
|
||||
border: false,
|
||||
toolButton: true,
|
||||
showSearch: false,
|
||||
searchItems: () => [],
|
||||
searchCol: () => ({ xs: 1, sm: 3, md: 3, lg: 4, xl: 5 }),
|
||||
});
|
||||
|
||||
// 查询表单参数 ==> 非必传(默认为{pageNum:1, pageSize: 10})
|
||||
const queryForm: Ref<any> = defineModel('queryForm', {
|
||||
default: {
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// table 实例
|
||||
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||
|
||||
// 接收 columns 并设置为响应式
|
||||
const tableColumns = reactive<TableColumn[]>(props.columns);
|
||||
|
||||
// 接收 searchItems 并设置为响应式
|
||||
const tableSearchItems = reactive<SearchItem[]>(props.searchItems);
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
// 是否显示搜索模块
|
||||
const isShowSearch = ref(props.showSearch);
|
||||
|
||||
// 控制 ToolButton 显示
|
||||
const showToolButton = (key: 'setting' | 'search') => {
|
||||
return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton;
|
||||
};
|
||||
|
||||
const nowSearchItem: Ref<SearchItem> = ref(null) as any;
|
||||
|
||||
/**
|
||||
* 改变当前的搜索项
|
||||
* @param searchItem 当前点击的搜索项
|
||||
*/
|
||||
const changeSimpleFormItem = (searchItem: SearchItem) => {
|
||||
// 将之前的值置为空,避免因为只显示一个搜索项却搜索多个条件
|
||||
queryForm.value[nowSearchItem.value.prop] = null;
|
||||
nowSearchItem.value = searchItem;
|
||||
};
|
||||
|
||||
const pageSizeChange = (val: number) => {
|
||||
emit('pageSizeChange', val);
|
||||
handlePageSizeChange(val);
|
||||
};
|
||||
const pageNumChange = (val: number) => {
|
||||
emit('pageNumChange', val);
|
||||
handlePageNumChange(val);
|
||||
};
|
||||
|
||||
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||
props.pageable,
|
||||
props.pageApi,
|
||||
queryForm,
|
||||
props.beforeQueryFn,
|
||||
props.dataHandlerFn
|
||||
);
|
||||
|
||||
const state = reactive({
|
||||
pageSizes: [] as any, // 可选每页显示的数据量
|
||||
// 输入框宽度
|
||||
formatVal: '', // 格式化后的值
|
||||
tableMaxHeight: '500px',
|
||||
});
|
||||
|
||||
const { pageSizes, formatVal, tableMaxHeight } = toRefs(state);
|
||||
|
||||
watch(tableData, (newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(tableData.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(isShowSearch, () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
tableData = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
calcuTableHeight();
|
||||
useEventListener(window, 'resize', calcuTableHeight);
|
||||
|
||||
if (props.searchItems.length > 0) {
|
||||
nowSearchItem.value = props.searchItems[0];
|
||||
}
|
||||
|
||||
let pageSize = queryForm.value.pageSize;
|
||||
// 如果pageSize设为0,则使用系统全局配置的pageSize
|
||||
if (!pageSize) {
|
||||
pageSize = themeConfig.value.defaultListPageSize;
|
||||
// 可能storage已经存在配置json,则可能没值,需要清storage重试
|
||||
if (!pageSize) {
|
||||
pageSize = 10;
|
||||
}
|
||||
}
|
||||
|
||||
queryForm.value.pageNum = 1;
|
||||
queryForm.value.pageSize = pageSize;
|
||||
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
|
||||
|
||||
if (!props.lazy) {
|
||||
await getTableData();
|
||||
}
|
||||
});
|
||||
|
||||
const calcuTableHeight = () => {
|
||||
const headerHeight = isShowSearch.value ? 330 : 250;
|
||||
state.tableMaxHeight = window.innerHeight - headerHeight + 'px';
|
||||
};
|
||||
|
||||
const searchFormItemKeyUpEnter = (event: any) => {
|
||||
event.preventDefault();
|
||||
search();
|
||||
};
|
||||
|
||||
const formatText = (data: any) => {
|
||||
state.formatVal = '';
|
||||
try {
|
||||
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
|
||||
} catch (e) {
|
||||
state.formatVal = data;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
emit('update:selectionData', val);
|
||||
};
|
||||
|
||||
const getData = () => {
|
||||
return toValue(tableData);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
tableRef: tableRef,
|
||||
search: getTableData,
|
||||
getData,
|
||||
total,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.table-box,
|
||||
.table-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// 表格 header 样式
|
||||
.table-header {
|
||||
width: 100%;
|
||||
|
||||
.header-button-lf {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
252
frontend/src/components/pagetable/index.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { getValueByPath } from '@/common/utils/object';
|
||||
import { getTextWidth } from '@/common/utils/string';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export class TableColumn {
|
||||
/**
|
||||
* 属性字段
|
||||
*/
|
||||
prop: string;
|
||||
|
||||
/**
|
||||
* 显示表头
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* 是否自动计算宽度
|
||||
*/
|
||||
autoWidth: boolean = true;
|
||||
|
||||
/**
|
||||
* 自动计算宽度时,累加该值(可能列值会进行转换 如添加图标等,宽度需要比计算出来的更宽些)
|
||||
*/
|
||||
addWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 最小宽度
|
||||
*/
|
||||
minWidth: number | string;
|
||||
|
||||
/**
|
||||
* 是否为插槽,若slotName为空则插槽名为prop属性名
|
||||
*/
|
||||
slot: boolean = false;
|
||||
|
||||
/**
|
||||
* 插槽名,
|
||||
*/
|
||||
slotName: string = '';
|
||||
|
||||
showOverflowTooltip: boolean = true;
|
||||
|
||||
sortable: boolean = false;
|
||||
|
||||
/**
|
||||
* 官方:对应列的类型。 如果设置了selection则显示多选框;
|
||||
* 如果设置了 index 则显示该行的索引(从 1 开始计算);
|
||||
*
|
||||
* 新增 tag类型,用于枚举值转换后用tag进行展示
|
||||
*
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* 类型展示需要的额外参数,如枚举转换的EnumValue值等
|
||||
*/
|
||||
typeParam: any;
|
||||
|
||||
width: number | string;
|
||||
|
||||
fixed: any;
|
||||
|
||||
align: string = 'left';
|
||||
|
||||
/**
|
||||
* 指定格式化函数对原始值进行格式化,如时间格式化等
|
||||
* param1: data, param2: prop
|
||||
*/
|
||||
formatFunc: Function;
|
||||
|
||||
/**
|
||||
* 是否显示该列
|
||||
*/
|
||||
show: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否展示美化按钮(主要用于美化json文本等)
|
||||
*/
|
||||
isBeautify: boolean = false;
|
||||
|
||||
constructor(prop: string, label: string) {
|
||||
this.prop = prop;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该列在指定行数据中的值
|
||||
* @param rowData 该行对应的数据
|
||||
* @returns 该列对应的值
|
||||
*/
|
||||
getValueByData(rowData: any) {
|
||||
if (this.formatFunc) {
|
||||
return this.formatFunc(rowData, this.prop);
|
||||
}
|
||||
return getValueByPath(rowData, this.prop);
|
||||
}
|
||||
|
||||
static new(prop: string, label: string): TableColumn {
|
||||
return new TableColumn(prop, label);
|
||||
}
|
||||
|
||||
noShowOverflowTooltip(): TableColumn {
|
||||
this.showOverflowTooltip = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
setMinWidth(minWidth: number | string): TableColumn {
|
||||
this.minWidth = minWidth;
|
||||
this.autoWidth = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
setAddWidth(addWidth: number): TableColumn {
|
||||
this.addWidth = addWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中对齐
|
||||
* @returns this
|
||||
*/
|
||||
alignCenter(): TableColumn {
|
||||
this.align = 'center';
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用标签类型展示该列(用于枚举值友好展示)
|
||||
* @param param 枚举对象, 如AccountStatusEnum
|
||||
* @returns this
|
||||
*/
|
||||
typeTag(param: any): TableColumn {
|
||||
this.type = 'tag';
|
||||
this.typeParam = param;
|
||||
return this;
|
||||
}
|
||||
|
||||
typeText(): TableColumn {
|
||||
this.type = 'text';
|
||||
return this;
|
||||
}
|
||||
|
||||
typeJson(): TableColumn {
|
||||
this.type = 'jsonText';
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标识该列为插槽
|
||||
* @returns this
|
||||
*/
|
||||
isSlot(slotName: string = ''): TableColumn {
|
||||
this.slot = true;
|
||||
this.slotName = slotName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置该列的格式化回调函数
|
||||
* @param func 格式化回调函数(参数为 -> data: 该行对应的数据,prop: 该列对应的prop属性值)
|
||||
* @returns
|
||||
*/
|
||||
setFormatFunc(func: Function): TableColumn {
|
||||
this.formatFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为时间字段,则使用默认时间格式函数
|
||||
* @returns this
|
||||
*/
|
||||
isTime(): TableColumn {
|
||||
this.setFormatFunc((data: any, prop: string) => {
|
||||
return formatDate(getValueByPath(data, prop));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标识该列枚举类,需进行枚举值转换
|
||||
* @returns this
|
||||
*/
|
||||
isEnum(enums: any): TableColumn {
|
||||
this.setFormatFunc((data: any, prop: string) => {
|
||||
return i18n.global.t(EnumValue.getLabelByValue(enums, getValueByPath(data, prop)));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
fixedRight(): TableColumn {
|
||||
this.fixed = 'right';
|
||||
return this;
|
||||
}
|
||||
|
||||
fixedLeft(): TableColumn {
|
||||
this.fixed = 'left';
|
||||
return this;
|
||||
}
|
||||
|
||||
canBeautify(): TableColumn {
|
||||
this.isBeautify = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动计算最小宽度
|
||||
* @param str 字符串
|
||||
* @param tableData 表数据
|
||||
* @param label 表头label也参与宽度计算
|
||||
* @returns 列宽度
|
||||
*/
|
||||
autoCalculateMinWidth = (tableData: any) => {
|
||||
const prop = this.prop;
|
||||
const label = this.label;
|
||||
|
||||
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let maxWidthText = '';
|
||||
let maxWidthValue;
|
||||
// 为了兼容formatFunc格式化回调函数
|
||||
let maxData;
|
||||
// 获取该列中最长的数据(内容)
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let nowData = tableData[i];
|
||||
let nowValue = getValueByPath(nowData, prop);
|
||||
if (!nowValue) {
|
||||
continue;
|
||||
}
|
||||
// 转为字符串比较长度
|
||||
let nowText = nowValue + '';
|
||||
if (nowText.length > maxWidthText.length) {
|
||||
maxWidthText = nowText;
|
||||
maxWidthValue = nowValue;
|
||||
maxData = nowData;
|
||||
}
|
||||
}
|
||||
if (this.formatFunc && maxWidthValue) {
|
||||
maxWidthText = this.formatFunc(maxData, prop) + '';
|
||||
}
|
||||
// 需要加上表格的内间距等,视情况加
|
||||
const contentWidth: number = getTextWidth(maxWidthText) + 30;
|
||||
// 获取label的宽度,取较大的宽度
|
||||
const columnWidth: number = getTextWidth(i18n.global.t(label)) + 60;
|
||||
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
|
||||
// 设置上限与累加需要额外增加的宽度
|
||||
this.minWidth = (flexWidth > 400 ? 400 : flexWidth) + this.addWidth;
|
||||
};
|
||||
}
|
||||
12
frontend/src/components/progress-notify/progress-notify.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const buildProgressProps = (): any => {
|
||||
return {
|
||||
progress: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
executedStatements: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||