Compare commits
565 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 | ||
|
|
4cb9ff3f14 | ||
|
|
e4f3e2c4c1 | ||
|
|
a80adb7dd8 | ||
|
|
195127a9d4 | ||
|
|
f2119b2c52 | ||
|
|
f15c45793b | ||
|
|
24f543e667 | ||
|
|
772995705f | ||
|
|
3475c39fe6 | ||
|
|
e6e393379f | ||
|
|
03cc91c3e5 | ||
|
|
f82f7bec6a | ||
|
|
4afd5bbd5e | ||
|
|
86aac2bf08 | ||
|
|
70c8b25a67 | ||
|
|
231af72444 | ||
|
|
480e930385 | ||
|
|
debc34f0fb | ||
|
|
99ce3bd099 | ||
|
|
99431cf9a2 | ||
|
|
83711c69f9 | ||
|
|
9e67032280 | ||
|
|
fa37937410 | ||
|
|
1be2cad78e | ||
|
|
2b1e687ed4 | ||
|
|
881009321b | ||
|
|
aed99b63b8 | ||
|
|
dfa34ba371 | ||
|
|
20beb30dd8 | ||
|
|
4475972af3 | ||
|
|
a843c65783 | ||
|
|
c2de1d3fa2 | ||
|
|
c8d091da06 | ||
|
|
553208ba57 | ||
|
|
072028699a | ||
|
|
9cdcf145a5 | ||
|
|
4df1c19e81 | ||
|
|
ac26a214bc | ||
|
|
ad616496d1 | ||
|
|
9870582779 | ||
|
|
20cc696b33 | ||
|
|
d7263f2b3c | ||
|
|
74e5ee41fb | ||
|
|
f936331dff | ||
|
|
ba311c3504 | ||
|
|
03291594b1 | ||
|
|
a6d9a4b5ae | ||
|
|
875de022c1 | ||
|
|
2c863a2774 | ||
|
|
f2f086a82c | ||
|
|
936ca61f94 | ||
|
|
87ae2f81fa | ||
|
|
ecf67db2b1 | ||
|
|
2e5589e112 | ||
|
|
2598a60898 | ||
|
|
0de226bbf3 | ||
|
|
422f0d8491 | ||
|
|
b028708b94 | ||
|
|
812c0d0f6a | ||
|
|
46df5293dd | ||
|
|
ab42b3e90b | ||
|
|
1378259cc7 | ||
|
|
c8f0b0a83f | ||
|
|
acec760ec1 | ||
|
|
2fe70d49f6 | ||
|
|
9013fff804 | ||
|
|
e925a808c4 | ||
|
|
6c197edddd | ||
|
|
c35e91b7b6 | ||
|
|
575947795a | ||
|
|
51f116c7d2 | ||
|
|
c28254855c | ||
|
|
e8f3671ffb | ||
|
|
ac62767a18 | ||
|
|
2db4c20dd3 | ||
|
|
cfb7fd5b29 | ||
|
|
22c401f9d8 | ||
|
|
be00b90c1d | ||
|
|
fb3f89c594 | ||
|
|
e7a66378ea | ||
|
|
2f88b48973 | ||
|
|
7761fe0288 | ||
|
|
09e6bdcf7e | ||
|
|
61a4d87f59 | ||
|
|
c219ec33b0 | ||
|
|
fd86f36218 | ||
|
|
efac41f392 | ||
|
|
52df61ae0d | ||
|
|
cf2bc6785c | ||
|
|
98a4c92576 | ||
|
|
b1ee9b65ff | ||
|
|
99cc4c5e5e | ||
|
|
226bb8f089 | ||
|
|
37ed5134e8 | ||
|
|
0f54d4a472 | ||
|
|
64805360d6 | ||
|
|
7f69fe2ad9 | ||
|
|
f913510d3c | ||
|
|
f2d9e7786d | ||
|
|
e1afb1ed54 | ||
|
|
12f8cf0111 | ||
|
|
daa2ef5203 | ||
|
|
1e3e183930 | ||
|
|
366563a0fe | ||
|
|
577802e5ad | ||
|
|
76d6fc3ba5 | ||
|
|
f0540559bb | ||
|
|
802e379f60 | ||
|
|
8c9253da80 | ||
|
|
5271bd21e8 | ||
|
|
db554ebdc9 | ||
|
|
1c18a01bf6 | ||
|
|
729a3d7028 | ||
|
|
b88923a128 | ||
|
|
fe8cd93c78 | ||
|
|
64b49dae2e | ||
|
|
edbbbca5f9 | ||
|
|
5ad0d90038 | ||
|
|
9b9173dea7 | ||
|
|
f58331c1c1 | ||
|
|
b2dc9dff0b | ||
|
|
51d06ab206 | ||
|
|
799e0ac9fc |
10
.gitignore
vendored
@@ -15,4 +15,12 @@
|
||||
*.sum
|
||||
|
||||
*/node_modules/
|
||||
**/vendor/
|
||||
**/vendor/
|
||||
.idea
|
||||
.vscode
|
||||
out
|
||||
|
||||
server/docs/docker-compose
|
||||
server/config.yml
|
||||
server/ip2region.xdb
|
||||
mayfly-go.log
|
||||
|
||||
18
.vscode/launch.json
vendored
@@ -1,18 +0,0 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "mayfly-go",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${fileDirname}/main.go",
|
||||
"env": {},
|
||||
"args": []
|
||||
},
|
||||
]
|
||||
}
|
||||
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"]
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
36
README.en.md
@@ -1,36 +0,0 @@
|
||||
# mayfly-go
|
||||
|
||||
#### Description
|
||||
golang实现linux运维等
|
||||
|
||||
#### Software Architecture
|
||||
Software architecture description
|
||||
|
||||
#### Installation
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Instructions
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Contribution
|
||||
|
||||
1. Fork the repository
|
||||
2. Create Feat_xxx branch
|
||||
3. Commit your code
|
||||
4. Create Pull Request
|
||||
|
||||
|
||||
#### Gitee Feature
|
||||
|
||||
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
|
||||
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
|
||||
4. The most valuable open source project [GVP](https://gitee.com/gvp)
|
||||
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
|
||||
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
114
README.md
@@ -1,33 +1,109 @@
|
||||
# mayfly-go
|
||||
# 🌈mayfly-go
|
||||
|
||||
#### 介绍
|
||||
简单基于DDD(领域驱动设计)分层架构实现web版mysql,redis,linux统一操作管理平台
|
||||
<p align="center">
|
||||
<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/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>
|
||||
|
||||
## 前言
|
||||
|
||||
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?jump_from=webapi">119699946</a>
|
||||
<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
|
||||
## 演示环境
|
||||
|
||||
### 系统功能
|
||||
http://go.mayfly.run
|
||||
账号/密码:test/test123.
|
||||
|
||||
记录操作记录
|
||||

|
||||
## 系统核心功能截图
|
||||
|
||||
#### 首页
|
||||
|
||||

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

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

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

|
||||
|
||||

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

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

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

|
||||
|
||||
#### 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.
|
||||
@@ -1,74 +0,0 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/base/global"
|
||||
"mayfly-go/base/utils"
|
||||
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func ErrIsNil(err error, msg string, params ...interface{}) {
|
||||
if err != nil {
|
||||
global.Log.Error(msg + ": " + err.Error())
|
||||
panic(NewBizErr(fmt.Sprintf(msg, params...)))
|
||||
}
|
||||
}
|
||||
|
||||
func ErrIsNilAppendErr(err error, msg string) {
|
||||
if err != nil {
|
||||
panic(NewBizErr(fmt.Sprintf(msg, err.Error())))
|
||||
}
|
||||
}
|
||||
|
||||
func IsNil(err error) {
|
||||
switch t := err.(type) {
|
||||
case *BizError:
|
||||
panic(t)
|
||||
case error:
|
||||
global.Log.Error("非业务异常: " + err.Error())
|
||||
panic(NewBizErr(fmt.Sprintf("非业务异常: %s", err.Error())))
|
||||
}
|
||||
}
|
||||
|
||||
func IsTrue(exp bool, msg string, params ...interface{}) {
|
||||
if !exp {
|
||||
panic(NewBizErr(fmt.Sprintf(msg, params...)))
|
||||
}
|
||||
}
|
||||
|
||||
func IsTrueBy(exp bool, err BizError) {
|
||||
if !exp {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NotEmpty(str string, msg string, params ...interface{}) {
|
||||
if str == "" {
|
||||
panic(NewBizErr(fmt.Sprintf(msg, params...)))
|
||||
}
|
||||
}
|
||||
|
||||
func NotNil(data interface{}, msg string) {
|
||||
if reflect.ValueOf(data).IsNil() {
|
||||
panic(NewBizErr(msg))
|
||||
}
|
||||
}
|
||||
|
||||
func NotBlank(data interface{}, msg string) {
|
||||
if utils.IsBlank(reflect.ValueOf(data)) {
|
||||
panic(NewBizErr(msg))
|
||||
}
|
||||
}
|
||||
|
||||
func IsEquals(data interface{}, data1 interface{}, msg string) {
|
||||
if data != data1 {
|
||||
panic(NewBizErr(msg))
|
||||
}
|
||||
}
|
||||
|
||||
func Nil(data interface{}, msg string) {
|
||||
if !reflect.ValueOf(data).IsNil() {
|
||||
panic(NewBizErr(msg))
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package biz
|
||||
|
||||
// 业务错误
|
||||
type BizError struct {
|
||||
code int16
|
||||
err string
|
||||
}
|
||||
|
||||
var (
|
||||
Success *BizError = NewBizErrCode(200, "success")
|
||||
BizErr *BizError = NewBizErrCode(400, "biz error")
|
||||
ServerError *BizError = NewBizErrCode(500, "server error")
|
||||
PermissionErr *BizError = NewBizErrCode(501, "token error")
|
||||
)
|
||||
|
||||
// 错误消息
|
||||
func (e *BizError) Error() string {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// 错误码
|
||||
func (e *BizError) Code() int16 {
|
||||
return e.code
|
||||
}
|
||||
|
||||
// 创建业务逻辑错误结构体,默认为业务逻辑错误
|
||||
func NewBizErr(msg string) *BizError {
|
||||
return &BizError{code: BizErr.code, err: msg}
|
||||
}
|
||||
|
||||
// 创建业务逻辑错误结构体,可设置指定错误code
|
||||
func NewBizErrCode(code int16, msg string) *BizError {
|
||||
return &BizError{code: code, err: msg}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"mayfly-go/base/biz"
|
||||
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
var store = base64Captcha.DefaultMemStore
|
||||
var driver base64Captcha.Driver = base64Captcha.DefaultDriverDigit
|
||||
|
||||
// 生成验证码
|
||||
func Generate() (string, string) {
|
||||
c := base64Captcha.NewCaptcha(driver, store)
|
||||
// 获取
|
||||
id, b64s, err := c.Generate()
|
||||
biz.ErrIsNilAppendErr(err, "获取验证码错误: %s")
|
||||
return id, b64s
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
func Verify(id string, val string) bool {
|
||||
if id == "" || val == "" {
|
||||
return false
|
||||
}
|
||||
// 同时清理掉这个图片
|
||||
return store.Verify(id, val, true)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type App struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
|
||||
func (a *App) GetAppInfo() string {
|
||||
return fmt.Sprintf("[%s:%s]", a.Name, a.Version)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"mayfly-go/base/utils"
|
||||
"mayfly-go/base/utils/assert"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// 配置文件映射对象
|
||||
var Conf *Config
|
||||
|
||||
func init() {
|
||||
configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录")
|
||||
flag.Parse()
|
||||
// 获取启动参数中,配置文件的绝对路径
|
||||
path, _ := filepath.Abs(*configFilePath)
|
||||
startConfigParam = &CmdConfigParam{ConfigFilePath: path}
|
||||
// 读取配置文件信息
|
||||
yc := &Config{}
|
||||
if err := utils.LoadYml(startConfigParam.ConfigFilePath, yc); err != nil {
|
||||
panic(fmt.Sprintf("读取配置文件[%s]失败: %s", startConfigParam.ConfigFilePath, err.Error()))
|
||||
}
|
||||
// 校验配置文件内容信息
|
||||
yc.Valid()
|
||||
Conf = yc
|
||||
}
|
||||
|
||||
// 启动配置参数
|
||||
type CmdConfigParam struct {
|
||||
ConfigFilePath string // -e 配置文件路径
|
||||
}
|
||||
|
||||
// 启动可执行文件时的参数
|
||||
var startConfigParam *CmdConfigParam
|
||||
|
||||
// yaml配置文件映射对象
|
||||
type Config struct {
|
||||
App *App `yaml:"app"`
|
||||
Server *Server `yaml:"server"`
|
||||
Jwt *Jwt `yaml:"jwt"`
|
||||
Redis *Redis `yaml:"redis"`
|
||||
Mysql *Mysql `yaml:"mysql"`
|
||||
Log *Log `yaml:"log"`
|
||||
}
|
||||
|
||||
// 配置文件内容校验
|
||||
func (c *Config) Valid() {
|
||||
assert.IsTrue(c.Jwt != nil, "配置文件的[jwt]信息不能为空")
|
||||
c.Jwt.Valid()
|
||||
}
|
||||
|
||||
// 获取执行可执行文件时,指定的启动参数
|
||||
func getStartConfig() *CmdConfigParam {
|
||||
configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录")
|
||||
flag.Parse()
|
||||
// 获取配置文件绝对路径
|
||||
path, _ := filepath.Abs(*configFilePath)
|
||||
sc := &CmdConfigParam{ConfigFilePath: path}
|
||||
return sc
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package config
|
||||
|
||||
import "mayfly-go/base/utils/assert"
|
||||
|
||||
type Jwt struct {
|
||||
Key string `yaml:"key"`
|
||||
ExpireTime uint64 `yaml:"expire-time"` // 过期时间,单位分钟
|
||||
}
|
||||
|
||||
func (j *Jwt) Valid() {
|
||||
assert.IsTrue(j.Key != "", "config.yml之 [jwt.key] 不能为空")
|
||||
assert.IsTrue(j.ExpireTime != 0, "config.yml之 [jwt.expire-time] 不能为空")
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package config
|
||||
|
||||
import "path"
|
||||
|
||||
type Log struct {
|
||||
Level string `yaml:"level"`
|
||||
File *LogFile `yaml:"file"`
|
||||
}
|
||||
|
||||
type LogFile struct {
|
||||
Name string `yaml:"name"`
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
// 获取完整路径文件名
|
||||
func (l *LogFile) GetFilename() string {
|
||||
var filepath, filename string
|
||||
if fp := l.Path; fp == "" {
|
||||
filepath = "./"
|
||||
} else {
|
||||
filepath = fp
|
||||
}
|
||||
if fn := l.Name; fn == "" {
|
||||
filename = "default.log"
|
||||
} else {
|
||||
filename = fn
|
||||
}
|
||||
|
||||
return path.Join(filepath, filename)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package config
|
||||
|
||||
type Mysql struct {
|
||||
Host string `mapstructure:"path" json:"host" yaml:"host"`
|
||||
Config string `mapstructure:"config" json:"config" yaml:"config"`
|
||||
Dbname string `mapstructure:"db-name" json:"dbname" yaml:"db-name"`
|
||||
Username string `mapstructure:"username" json:"username" yaml:"username"`
|
||||
Password string `mapstructure:"password" json:"password" yaml:"password"`
|
||||
MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"`
|
||||
MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"`
|
||||
LogMode bool `mapstructure:"log-mode" json:"logMode" yaml:"log-mode"`
|
||||
LogZap string `mapstructure:"log-zap" json:"logZap" yaml:"log-zap"`
|
||||
}
|
||||
|
||||
func (m *Mysql) Dsn() string {
|
||||
return m.Username + ":" + m.Password + "@tcp(" + m.Host + ")/" + m.Dbname + "?" + m.Config
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Server struct {
|
||||
Port int `yaml:"port"`
|
||||
Model string `yaml:"model"`
|
||||
Cors bool `yaml:"cors"`
|
||||
Tls *Tls `yaml:"tls"`
|
||||
Static *[]*Static `yaml:"static"`
|
||||
StaticFile *[]*StaticFile `yaml:"static-file"`
|
||||
}
|
||||
|
||||
func (s *Server) GetPort() string {
|
||||
return fmt.Sprintf(":%d", s.Port)
|
||||
}
|
||||
|
||||
type Static struct {
|
||||
RelativePath string `yaml:"relative-path"`
|
||||
Root string `yaml:"root"`
|
||||
}
|
||||
|
||||
type StaticFile struct {
|
||||
RelativePath string `yaml:"relative-path"`
|
||||
Filepath string `yaml:"filepath"`
|
||||
}
|
||||
|
||||
type Tls struct {
|
||||
Enable bool `yaml:"enable"` // 是否启用tls
|
||||
KeyFile string `yaml:"key-file"` // 私钥文件路径
|
||||
CertFile string `yaml:"cert-file"` // 证书文件路径
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package ctx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mayfly-go/base/biz"
|
||||
"mayfly-go/base/logger"
|
||||
"mayfly-go/base/utils"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type LogInfo struct {
|
||||
LogResp bool // 是否记录返回结果
|
||||
Description string // 请求描述
|
||||
}
|
||||
|
||||
func NewLogInfo(description string) *LogInfo {
|
||||
return &LogInfo{Description: description, LogResp: false}
|
||||
}
|
||||
|
||||
func (i *LogInfo) WithLogResp(logResp bool) *LogInfo {
|
||||
i.LogResp = logResp
|
||||
return i
|
||||
}
|
||||
|
||||
func LogHandler(rc *ReqCtx) error {
|
||||
li := rc.LogInfo
|
||||
if li == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lfs := logrus.Fields{}
|
||||
if la := rc.LoginAccount; la != nil {
|
||||
lfs["uid"] = la.Id
|
||||
lfs["uname"] = la.Username
|
||||
}
|
||||
|
||||
req := rc.GinCtx.Request
|
||||
lfs[req.Method] = req.URL.Path
|
||||
|
||||
if err := rc.Err; err != nil {
|
||||
logger.Log.WithFields(lfs).Error(getErrMsg(rc, err))
|
||||
return nil
|
||||
}
|
||||
logger.Log.WithFields(lfs).Info(getLogMsg(rc))
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLogMsg(rc *ReqCtx) string {
|
||||
msg := rc.LogInfo.Description + fmt.Sprintf(" ->%dms", rc.timed)
|
||||
if !utils.IsBlank(reflect.ValueOf(rc.ReqParam)) {
|
||||
rb, _ := json.Marshal(rc.ReqParam)
|
||||
msg = msg + fmt.Sprintf("\n--> %s", string(rb))
|
||||
}
|
||||
|
||||
// 返回结果不为空,则记录返回结果
|
||||
if rc.LogInfo.LogResp && !utils.IsBlank(reflect.ValueOf(rc.ResData)) {
|
||||
respB, _ := json.Marshal(rc.ResData)
|
||||
msg = msg + fmt.Sprintf("\n<-- %s", string(respB))
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func getErrMsg(rc *ReqCtx, err interface{}) string {
|
||||
msg := rc.LogInfo.Description
|
||||
if !utils.IsBlank(reflect.ValueOf(rc.ReqParam)) {
|
||||
rb, _ := json.Marshal(rc.ReqParam)
|
||||
msg = msg + fmt.Sprintf("\n--> %s", string(rb))
|
||||
}
|
||||
|
||||
var errMsg string
|
||||
switch t := err.(type) {
|
||||
case *biz.BizError:
|
||||
errMsg = fmt.Sprintf("\n<-e errCode: %d, errMsg: %s", t.Code(), t.Error())
|
||||
case error:
|
||||
errMsg = fmt.Sprintf("\n<-e errMsg: %s\n%s", t.Error(), string(debug.Stack()))
|
||||
case string:
|
||||
errMsg = fmt.Sprintf("\n<-e errMsg: %s\n%s", t, string(debug.Stack()))
|
||||
}
|
||||
return (msg + errMsg)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package ctx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/base/biz"
|
||||
"mayfly-go/base/cache"
|
||||
"mayfly-go/base/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Permission struct {
|
||||
NeedToken bool // 是否需要token
|
||||
Code string // 权限code
|
||||
}
|
||||
|
||||
func NewPermission(code string) *Permission {
|
||||
return &Permission{NeedToken: true, Code: code}
|
||||
}
|
||||
|
||||
func (p *Permission) WithNeedToken(needToken bool) *Permission {
|
||||
p.NeedToken = needToken
|
||||
return p
|
||||
}
|
||||
|
||||
type PermissionCodeRegistry interface {
|
||||
// 保存用户权限code
|
||||
SaveCodes(userId uint64, codes []string)
|
||||
|
||||
// 判断用户是否拥有该code的权限
|
||||
HasCode(userId uint64, code string) bool
|
||||
|
||||
Remove(userId uint64)
|
||||
}
|
||||
|
||||
type DefaultPermissionCodeRegistry struct {
|
||||
cache *cache.TimedCache
|
||||
}
|
||||
|
||||
func (r *DefaultPermissionCodeRegistry) SaveCodes(userId uint64, codes []string) {
|
||||
if r.cache == nil {
|
||||
r.cache = cache.NewTimedCache(time.Minute*time.Duration(config.Conf.Jwt.ExpireTime), 5*time.Second)
|
||||
}
|
||||
r.cache.Put(fmt.Sprintf("%v", userId), codes)
|
||||
}
|
||||
|
||||
func (r *DefaultPermissionCodeRegistry) HasCode(userId uint64, code string) bool {
|
||||
if r.cache == nil {
|
||||
return false
|
||||
}
|
||||
codes, found := r.cache.Get(fmt.Sprintf("%v", userId))
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
for _, v := range codes.([]string) {
|
||||
if v == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *DefaultPermissionCodeRegistry) Remove(userId uint64) {
|
||||
r.cache.Delete(fmt.Sprintf("%v", userId))
|
||||
}
|
||||
|
||||
// 保存用户权限code
|
||||
func SavePermissionCodes(userId uint64, codes []string) {
|
||||
permissionCodeRegistry.SaveCodes(userId, codes)
|
||||
}
|
||||
|
||||
// 删除用户权限code
|
||||
func DeletePermissionCodes(userId uint64) {
|
||||
permissionCodeRegistry.Remove(userId)
|
||||
}
|
||||
|
||||
// 设置权限code注册器
|
||||
func SetPermissionCodeRegistery(pcr PermissionCodeRegistry) {
|
||||
permissionCodeRegistry = pcr
|
||||
}
|
||||
|
||||
var (
|
||||
permissionCodeRegistry PermissionCodeRegistry = &DefaultPermissionCodeRegistry{}
|
||||
// permissionError = biz.NewBizErrCode(biz.TokenErrorCode, biz.TokenErrorMsg)
|
||||
)
|
||||
|
||||
func PermissionHandler(rc *ReqCtx) error {
|
||||
permission := rc.RequiredPermission
|
||||
// 如果需要的权限信息不为空,并且不需要token,则不返回错误,继续后续逻辑
|
||||
if permission != nil && !permission.NeedToken {
|
||||
return nil
|
||||
}
|
||||
tokenStr := rc.GinCtx.Request.Header.Get("Authorization")
|
||||
// header不存在则从查询参数token中获取
|
||||
if tokenStr == "" {
|
||||
tokenStr = rc.GinCtx.Query("token")
|
||||
}
|
||||
if tokenStr == "" {
|
||||
return biz.PermissionErr
|
||||
}
|
||||
loginAccount, err := ParseToken(tokenStr)
|
||||
if err != nil || loginAccount == nil {
|
||||
return biz.PermissionErr
|
||||
}
|
||||
// 权限不为nil,并且permission code不为空,则校验是否有权限code
|
||||
if permission != nil && permission.Code != "" {
|
||||
if !permissionCodeRegistry.HasCode(loginAccount.Id, permission.Code) {
|
||||
return biz.PermissionErr
|
||||
}
|
||||
}
|
||||
|
||||
rc.LoginAccount = loginAccount
|
||||
return nil
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package ctx
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mayfly-go/base/ginx"
|
||||
"mayfly-go/base/model"
|
||||
"mayfly-go/base/utils/assert"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 处理函数
|
||||
type HandlerFunc func(*ReqCtx)
|
||||
|
||||
type ReqCtx struct {
|
||||
GinCtx *gin.Context // gin context
|
||||
|
||||
// NeedToken bool // 是否需要token
|
||||
RequiredPermission *Permission // 需要的权限信息,默认为nil,需要校验token
|
||||
LoginAccount *model.LoginAccount // 登录账号信息,只有校验token后才会有值
|
||||
|
||||
LogInfo *LogInfo // 日志相关信息
|
||||
ReqParam interface{} // 请求参数,主要用于记录日志
|
||||
ResData interface{} // 响应结果
|
||||
Err interface{} // 请求错误
|
||||
|
||||
timed int64 // 执行时间
|
||||
noRes bool // 无需返回结果,即文件下载等
|
||||
}
|
||||
|
||||
func (rc *ReqCtx) Handle(handler HandlerFunc) {
|
||||
ginCtx := rc.GinCtx
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
rc.Err = err
|
||||
ginx.ErrorRes(ginCtx, err)
|
||||
}
|
||||
// 应用所有请求后置处理器
|
||||
ApplyHandlerInterceptor(afterHandlers, rc)
|
||||
}()
|
||||
assert.IsTrue(ginCtx != nil, "ginContext == nil")
|
||||
|
||||
// 默认为不记录请求参数,可在handler回调函数中覆盖赋值
|
||||
rc.ReqParam = 0
|
||||
// 默认响应结果为nil,可在handler中赋值
|
||||
rc.ResData = nil
|
||||
|
||||
// 调用请求前所有处理器
|
||||
err := ApplyHandlerInterceptor(beforeHandlers, rc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
begin := time.Now()
|
||||
handler(rc)
|
||||
rc.timed = time.Now().Sub(begin).Milliseconds()
|
||||
if !rc.noRes {
|
||||
ginx.SuccessRes(ginCtx, rc.ResData)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *ReqCtx) Download(reader io.Reader, filename string) {
|
||||
rc.noRes = true
|
||||
ginx.Download(rc.GinCtx, reader, filename)
|
||||
}
|
||||
|
||||
// 新建请求上下文,默认需要校验token
|
||||
func NewReqCtx() *ReqCtx {
|
||||
return &ReqCtx{}
|
||||
}
|
||||
|
||||
func NewReqCtxWithGin(g *gin.Context) *ReqCtx {
|
||||
return &ReqCtx{GinCtx: g}
|
||||
}
|
||||
|
||||
// 调用该方法设置请求描述,则默认记录日志,并不记录响应结果
|
||||
func (r *ReqCtx) WithLog(li *LogInfo) *ReqCtx {
|
||||
r.LogInfo = li
|
||||
return r
|
||||
}
|
||||
|
||||
// 设置请求上下文需要的权限信息
|
||||
func (r *ReqCtx) WithRequiredPermission(permission *Permission) *ReqCtx {
|
||||
r.RequiredPermission = permission
|
||||
return r
|
||||
}
|
||||
|
||||
// 是否需要token
|
||||
func (r *ReqCtx) WithNeedToken(needToken bool) *ReqCtx {
|
||||
r.RequiredPermission = &Permission{NeedToken: false}
|
||||
return r
|
||||
}
|
||||
|
||||
// 处理器拦截器函数
|
||||
type HandlerInterceptorFunc func(*ReqCtx) error
|
||||
type HandlerInterceptors []HandlerInterceptorFunc
|
||||
|
||||
var (
|
||||
beforeHandlers HandlerInterceptors
|
||||
afterHandlers HandlerInterceptors
|
||||
)
|
||||
|
||||
// 使用前置处理器函数
|
||||
func UseBeforeHandlerInterceptor(b HandlerInterceptorFunc) {
|
||||
beforeHandlers = append(beforeHandlers, b)
|
||||
}
|
||||
|
||||
// 使用后置处理器函数
|
||||
func UseAfterHandlerInterceptor(b HandlerInterceptorFunc) {
|
||||
afterHandlers = append(afterHandlers, b)
|
||||
}
|
||||
|
||||
// 应用指定处理器拦截器,如果有一个错误则直接返回错误
|
||||
func ApplyHandlerInterceptor(his HandlerInterceptors, rc *ReqCtx) interface{} {
|
||||
for _, handler := range his {
|
||||
if err := handler(rc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package ctx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"mayfly-go/base/biz"
|
||||
"mayfly-go/base/config"
|
||||
"mayfly-go/base/model"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
var (
|
||||
JwtKey = config.Conf.Jwt.Key
|
||||
ExpTime = config.Conf.Jwt.ExpireTime
|
||||
)
|
||||
|
||||
// 创建用户token
|
||||
func CreateToken(userId uint64, username string) string {
|
||||
// 带权限创建令牌
|
||||
// 设置有效期,过期需要重新登录获取token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"id": userId,
|
||||
"username": username,
|
||||
"exp": time.Now().Add(time.Minute * time.Duration(ExpTime)).Unix(),
|
||||
})
|
||||
|
||||
// 使用自定义字符串加密 and get the complete encoded token as a string
|
||||
tokenString, err := token.SignedString([]byte(JwtKey))
|
||||
biz.ErrIsNil(err, "token创建失败")
|
||||
return tokenString
|
||||
}
|
||||
|
||||
// 解析token,并返回登录者账号信息
|
||||
func ParseToken(tokenStr string) (*model.LoginAccount, error) {
|
||||
if tokenStr == "" {
|
||||
return nil, errors.New("token error")
|
||||
}
|
||||
// Parse token
|
||||
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(JwtKey), nil
|
||||
})
|
||||
if err != nil || token == nil {
|
||||
return nil, err
|
||||
}
|
||||
i := token.Claims.(jwt.MapClaims)
|
||||
return &model.LoginAccount{Id: uint64(i["id"].(float64)), Username: i["username"].(string)}, nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package ginx
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mayfly-go/base/biz"
|
||||
"mayfly-go/base/global"
|
||||
"mayfly-go/base/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 绑定并校验请求结构体参数
|
||||
func BindJsonAndValid(g *gin.Context, data interface{}) {
|
||||
if err := g.ShouldBindJSON(data); err != nil {
|
||||
panic(biz.NewBizErr(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定查询字符串到
|
||||
func BindQuery(g *gin.Context, data interface{}) {
|
||||
if err := g.ShouldBindQuery(data); err != nil {
|
||||
panic(biz.NewBizErr(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分页参数
|
||||
func GetPageParam(g *gin.Context) *model.PageParam {
|
||||
return &model.PageParam{PageNum: QueryInt(g, "pageNum", 1), PageSize: QueryInt(g, "pageSize", 10)}
|
||||
}
|
||||
|
||||
// 获取查询参数中指定参数值,并转为int
|
||||
func QueryInt(g *gin.Context, qm string, defaultInt int) int {
|
||||
qv := g.Query(qm)
|
||||
if qv == "" {
|
||||
return defaultInt
|
||||
}
|
||||
qvi, err := strconv.Atoi(qv)
|
||||
biz.ErrIsNil(err, "query param not int")
|
||||
return qvi
|
||||
}
|
||||
|
||||
// 获取路径参数
|
||||
func PathParamInt(g *gin.Context, pm string) int {
|
||||
value, _ := strconv.Atoi(g.Param(pm))
|
||||
return value
|
||||
}
|
||||
|
||||
// 文件下载
|
||||
func Download(g *gin.Context, reader io.Reader, filename string) {
|
||||
g.Header("Content-Type", "application/octet-stream")
|
||||
g.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
io.Copy(g.Writer, reader)
|
||||
}
|
||||
|
||||
// 返回统一成功结果
|
||||
func SuccessRes(g *gin.Context, data interface{}) {
|
||||
g.JSON(http.StatusOK, model.Success(data))
|
||||
}
|
||||
|
||||
// 返回失败结果集
|
||||
func ErrorRes(g *gin.Context, err interface{}) {
|
||||
switch t := err.(type) {
|
||||
case *biz.BizError:
|
||||
g.JSON(http.StatusOK, model.Error(t))
|
||||
case error:
|
||||
g.JSON(http.StatusOK, model.ServerError())
|
||||
global.Log.Error(t)
|
||||
// panic(err)
|
||||
case string:
|
||||
g.JSON(http.StatusOK, model.ServerError())
|
||||
global.Log.Error(t)
|
||||
// panic(err)
|
||||
default:
|
||||
global.Log.Error(t)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
Log *logrus.Logger // 日志
|
||||
Db *gorm.DB // gorm
|
||||
RedisCli *redis.Client // redis
|
||||
)
|
||||
@@ -1,66 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/base/config"
|
||||
"mayfly-go/base/global"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Log = logrus.New()
|
||||
|
||||
func init() {
|
||||
Log.SetFormatter(new(LogFormatter))
|
||||
Log.SetReportCaller(true)
|
||||
|
||||
logConf := config.Conf.Log
|
||||
// 如果不存在日志配置信息,则默认debug级别
|
||||
if logConf == nil {
|
||||
Log.SetLevel(logrus.DebugLevel)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据配置文件设置日志级别
|
||||
if level := logConf.Level; level != "" {
|
||||
l, err := logrus.ParseLevel(level)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("日志级别不存在: %s", level))
|
||||
}
|
||||
Log.SetLevel(l)
|
||||
} else {
|
||||
Log.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
if logFile := logConf.File; logFile != nil {
|
||||
//写入文件
|
||||
file, err := os.OpenFile(logFile.GetFilename(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModeAppend|0666)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("创建日志文件失败: %s", err.Error()))
|
||||
}
|
||||
|
||||
Log.Out = file
|
||||
}
|
||||
|
||||
global.Log = Log
|
||||
}
|
||||
|
||||
type LogFormatter struct{}
|
||||
|
||||
func (l *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
timestamp := time.Now().Local().Format("2006-01-02 15:04:05.000")
|
||||
level := entry.Level
|
||||
logMsg := fmt.Sprintf("%s [%s]", timestamp, strings.ToUpper(level.String()))
|
||||
// 如果存在调用信息,记录方法信息及行号
|
||||
if caller := entry.Caller; caller != nil {
|
||||
logMsg = logMsg + fmt.Sprintf(" [%s:%d]", caller.Function, caller.Line)
|
||||
}
|
||||
for k, v := range entry.Data {
|
||||
logMsg = logMsg + fmt.Sprintf(" [%s=%v]", k, v)
|
||||
}
|
||||
logMsg = logMsg + fmt.Sprintf(" : %s\n", entry.Message)
|
||||
return []byte(logMsg), nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package model
|
||||
|
||||
type AppContext struct {
|
||||
}
|
||||
|
||||
type LoginAccount struct {
|
||||
Id uint64
|
||||
Username string
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
CheckToken bool // 是否检查token
|
||||
Code string // 权限码
|
||||
Name string // 描述
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/base/biz"
|
||||
"mayfly-go/base/global"
|
||||
"strconv"
|
||||
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id uint64 `json:"id"`
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
CreatorId uint64 `json:"creatorId"`
|
||||
Creator string `json:"creator"`
|
||||
UpdateTime *time.Time `json:"updateTime"`
|
||||
ModifierId uint64 `json:"modifierId"`
|
||||
Modifier string `json:"modifier"`
|
||||
}
|
||||
|
||||
// 设置基础信息. 如创建时间,修改时间,创建者,修改者信息
|
||||
func (m *Model) SetBaseInfo(account *LoginAccount) {
|
||||
nowTime := time.Now()
|
||||
isCreate := m.Id == 0
|
||||
if isCreate {
|
||||
m.CreateTime = &nowTime
|
||||
}
|
||||
m.UpdateTime = &nowTime
|
||||
|
||||
if account == nil {
|
||||
return
|
||||
}
|
||||
id := account.Id
|
||||
name := account.Username
|
||||
if isCreate {
|
||||
m.CreatorId = id
|
||||
m.Creator = name
|
||||
}
|
||||
m.Modifier = name
|
||||
m.ModifierId = id
|
||||
}
|
||||
|
||||
func Tx(funcs ...func(db *gorm.DB) error) (err error) {
|
||||
tx := global.Db.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
err = fmt.Errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
for _, f := range funcs {
|
||||
err = f(tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
}
|
||||
err = tx.Commit().Error
|
||||
return
|
||||
}
|
||||
|
||||
// 根据id获取实体对象。model需为指针类型(需要将查询出来的值赋值给model)
|
||||
//
|
||||
// 若error不为nil则为不存在该记录
|
||||
func GetById(model interface{}, id uint64, cols ...string) error {
|
||||
return global.Db.Select(cols).Where("id = ?", id).First(model).Error
|
||||
}
|
||||
|
||||
// 根据id列表查询
|
||||
func GetByIdIn(model interface{}, list interface{}, ids []uint64, orderBy ...string) {
|
||||
var orderByStr string
|
||||
if orderBy == nil {
|
||||
orderByStr = "id desc"
|
||||
} else {
|
||||
orderByStr = strings.Join(orderBy, ",")
|
||||
}
|
||||
global.Db.Model(model).Where("id in (?)", ids).Order(orderByStr).Find(list)
|
||||
}
|
||||
|
||||
// 根据id列表查询
|
||||
func CountBy(model interface{}) int64 {
|
||||
var count int64
|
||||
global.Db.Model(model).Where(model).Count(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
// 根据id更新model,更新字段为model中不为空的值,即int类型不为0,ptr类型不为nil这类字段值
|
||||
func UpdateById(model interface{}) error {
|
||||
return global.Db.Model(model).Updates(model).Error
|
||||
}
|
||||
|
||||
// 根据id删除model
|
||||
func DeleteById(model interface{}, id uint64) error {
|
||||
return global.Db.Delete(model, "id = ?", id).Error
|
||||
}
|
||||
|
||||
// 根据条件删除
|
||||
func DeleteByCondition(model interface{}) error {
|
||||
return global.Db.Where(model).Delete(model).Error
|
||||
}
|
||||
|
||||
// 插入model
|
||||
func Insert(model interface{}) error {
|
||||
return global.Db.Create(model).Error
|
||||
}
|
||||
|
||||
// 获取满足model中不为空的字段值条件的所有数据.
|
||||
//
|
||||
// @param list为数组类型 如 var users *[]User,可指定为非model结构体,即只包含需要返回的字段结构体
|
||||
func ListBy(model interface{}, list interface{}, cols ...string) {
|
||||
global.Db.Model(model).Select(cols).Where(model).Find(list)
|
||||
}
|
||||
|
||||
// 获取满足model中不为空的字段值条件的所有数据.
|
||||
//
|
||||
// @param list为数组类型 如 var users *[]User,可指定为非model结构体
|
||||
func ListByOrder(model interface{}, list interface{}, order ...string) {
|
||||
var orderByStr string
|
||||
if order == nil {
|
||||
orderByStr = "id desc"
|
||||
} else {
|
||||
orderByStr = strings.Join(order, ",")
|
||||
}
|
||||
global.Db.Model(model).Where(model).Order(orderByStr).Find(list)
|
||||
}
|
||||
|
||||
// 获取满足model中不为空的字段值条件的单个对象。model需为指针类型(需要将查询出来的值赋值给model)
|
||||
//
|
||||
// 若 error不为nil,则为不存在该记录
|
||||
func GetBy(model interface{}, cols ...string) error {
|
||||
return global.Db.Select(cols).Where(model).First(model).Error
|
||||
}
|
||||
|
||||
// 获取满足conditionModel中不为空的字段值条件的单个对象。model需为指针类型(需要将查询出来的值赋值给model)
|
||||
// @param toModel 需要查询的字段
|
||||
// 若 error不为nil,则为不存在该记录
|
||||
func GetByConditionTo(conditionModel interface{}, toModel interface{}) error {
|
||||
return global.Db.Model(conditionModel).Where(conditionModel).First(toModel).Error
|
||||
}
|
||||
|
||||
// 获取分页结果
|
||||
func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interface{}, orderBy ...string) *PageResult {
|
||||
var count int64
|
||||
err := global.Db.Model(conditionModel).Where(conditionModel).Count(&count).Error
|
||||
biz.ErrIsNilAppendErr(err, " 查询错误:%s")
|
||||
if count == 0 {
|
||||
return &PageResult{Total: 0, List: []string{}}
|
||||
}
|
||||
|
||||
page := pageParam.PageNum
|
||||
pageSize := pageParam.PageSize
|
||||
var orderByStr string
|
||||
if orderBy == nil {
|
||||
orderByStr = "id desc"
|
||||
} else {
|
||||
orderByStr = strings.Join(orderBy, ",")
|
||||
}
|
||||
err = global.Db.Model(conditionModel).Where(conditionModel).Order(orderByStr).Limit(pageSize).Offset((page - 1) * pageSize).Find(toModels).Error
|
||||
biz.ErrIsNil(err, "查询失败")
|
||||
return &PageResult{Total: count, List: toModels}
|
||||
}
|
||||
|
||||
// 根据sql获取分页对象
|
||||
func GetPageBySql(sql string, param *PageParam, toModel interface{}, args ...interface{}) *PageResult {
|
||||
db := global.Db
|
||||
selectIndex := strings.Index(sql, "SELECT ") + 7
|
||||
fromIndex := strings.Index(sql, " FROM")
|
||||
selectCol := sql[selectIndex:fromIndex]
|
||||
countSql := strings.Replace(sql, selectCol, "COUNT(*) AS total ", 1)
|
||||
// 查询count
|
||||
var count int
|
||||
err := db.Raw(countSql, args...).Scan(&count).Error
|
||||
biz.ErrIsNilAppendErr(err, "查询失败: %s")
|
||||
if count == 0 {
|
||||
return &PageResult{Total: 0, List: []string{}}
|
||||
}
|
||||
// 分页查询
|
||||
limitSql := sql + " LIMIT " + strconv.Itoa((param.PageNum-1)*param.PageSize) + ", " + strconv.Itoa(param.PageSize)
|
||||
err = db.Raw(limitSql).Scan(toModel).Error
|
||||
biz.ErrIsNil(err, "查询失败: %s")
|
||||
return &PageResult{Total: int64(count), List: toModel}
|
||||
}
|
||||
|
||||
func GetListBySql(sql string, params ...interface{}) []map[string]interface{} {
|
||||
var maps []map[string]interface{}
|
||||
global.Db.Raw(sql, params...).Scan(&maps)
|
||||
return maps
|
||||
}
|
||||
|
||||
func GetListBySql2Model(sql string, toEntity interface{}, params ...interface{}) error {
|
||||
return global.Db.Raw(sql, params...).Find(toEntity).Error
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package model
|
||||
|
||||
// 分页参数
|
||||
type PageParam struct {
|
||||
PageNum int `json:"pageNum"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// 分页结果
|
||||
type PageResult struct {
|
||||
Total int64 `json:"total"`
|
||||
List interface{} `json:"list"`
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package rediscli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
var cli *redis.Client
|
||||
|
||||
func SetCli(client *redis.Client) {
|
||||
cli = client
|
||||
}
|
||||
|
||||
func GetCli() *redis.Client {
|
||||
return cli
|
||||
}
|
||||
|
||||
// get key value
|
||||
func Get(key string) string {
|
||||
val, err := cli.Get(key).Result()
|
||||
switch {
|
||||
case err == redis.Nil:
|
||||
fmt.Println("key does not exist")
|
||||
case err != nil:
|
||||
fmt.Println("Get failed", err)
|
||||
case val == "":
|
||||
fmt.Println("value is empty")
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// set key value
|
||||
func Set(key string, val string, expiration time.Duration) {
|
||||
cli.Set(key, val, expiration)
|
||||
}
|
||||
|
||||
func HSet(key string, field string, val interface{}) {
|
||||
cli.HSet(key, field, val)
|
||||
}
|
||||
|
||||
// hget
|
||||
func HGet(key string, field string) string {
|
||||
val, _ := cli.HGet(key, field).Result()
|
||||
return val
|
||||
}
|
||||
|
||||
// hget
|
||||
func HExist(key string, field string) bool {
|
||||
val, _ := cli.HExists(key, field).Result()
|
||||
return val
|
||||
}
|
||||
|
||||
// hgetall
|
||||
func HGetAll(key string) map[string]string {
|
||||
vals, _ := cli.HGetAll(key).Result()
|
||||
return vals
|
||||
}
|
||||
|
||||
// hdel
|
||||
func HDel(key string, fields ...string) int {
|
||||
return int(cli.HDel(key, fields...).Val())
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package starter
|
||||
|
||||
import (
|
||||
"mayfly-go/base/global"
|
||||
)
|
||||
|
||||
func PrintBanner() {
|
||||
global.Log.Print(`
|
||||
__ _
|
||||
_ __ ___ __ _ _ _ / _| |_ _ __ _ ___
|
||||
| '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \
|
||||
| | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) |
|
||||
|_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/
|
||||
|___/ |___/ |___/
|
||||
`)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package starter
|
||||
|
||||
import (
|
||||
"mayfly-go/base/config"
|
||||
"mayfly-go/base/global"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func InitDb() {
|
||||
global.Db = GormMysql()
|
||||
}
|
||||
|
||||
func GormMysql() *gorm.DB {
|
||||
m := config.Conf.Mysql
|
||||
if m == nil || m.Dbname == "" {
|
||||
global.Log.Panic("未找到数据库配置信息")
|
||||
return nil
|
||||
}
|
||||
global.Log.Infof("连接mysql [%s]", m.Host)
|
||||
mysqlConfig := mysql.Config{
|
||||
DSN: m.Dsn(), // DSN data source name
|
||||
DefaultStringSize: 191, // string 类型字段的默认长度
|
||||
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
|
||||
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
|
||||
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
|
||||
SkipInitializeWithVersion: false, // 根据版本自动配置
|
||||
}
|
||||
ormConfig := &gorm.Config{NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "t_",
|
||||
SingularTable: true,
|
||||
}, Logger: logger.Default.LogMode(logger.Silent)}
|
||||
if db, err := gorm.Open(mysql.New(mysqlConfig), ormConfig); err != nil {
|
||||
global.Log.Panicf("连接mysql失败! [%s]", err.Error())
|
||||
return nil
|
||||
} else {
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
|
||||
return db
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package starter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/base/config"
|
||||
"mayfly-go/base/global"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
func InitRedis() {
|
||||
global.RedisCli = ConnRedis()
|
||||
}
|
||||
|
||||
func ConnRedis() *redis.Client {
|
||||
// 设置redis客户端
|
||||
redisConf := config.Conf.Redis
|
||||
if redisConf == nil {
|
||||
global.Log.Panic("未找到redis配置信息")
|
||||
return nil
|
||||
}
|
||||
global.Log.Infof("连接redis [%s:%d]", redisConf.Host, redisConf.Port)
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", redisConf.Host, redisConf.Port),
|
||||
Password: redisConf.Password, // no password set
|
||||
DB: redisConf.Db, // use default DB
|
||||
})
|
||||
// 测试连接
|
||||
_, e := rdb.Ping().Result()
|
||||
if e != nil {
|
||||
global.Log.Panic(fmt.Sprintf("连接redis失败! [%s:%d]", redisConf.Host, redisConf.Port))
|
||||
}
|
||||
return rdb
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package starter
|
||||
|
||||
import (
|
||||
"mayfly-go/base/biz"
|
||||
"mayfly-go/base/config"
|
||||
"mayfly-go/base/ctx"
|
||||
"mayfly-go/base/global"
|
||||
"mayfly-go/server/initialize"
|
||||
)
|
||||
|
||||
func RunWebServer() {
|
||||
// 权限处理器
|
||||
ctx.UseBeforeHandlerInterceptor(ctx.PermissionHandler)
|
||||
// 日志处理器
|
||||
ctx.UseAfterHandlerInterceptor(ctx.LogHandler)
|
||||
// 注册路由
|
||||
web := initialize.InitRouter()
|
||||
|
||||
server := config.Conf.Server
|
||||
port := server.GetPort()
|
||||
if app := config.Conf.App; app != nil {
|
||||
global.Log.Infof("%s- Listening and serving HTTP on %s", app.GetAppInfo(), port)
|
||||
} else {
|
||||
global.Log.Infof("Listening and serving HTTP on %s", port)
|
||||
}
|
||||
|
||||
var err error
|
||||
if server.Tls != nil && server.Tls.Enable {
|
||||
err = web.RunTLS(port, server.Tls.CertFile, server.Tls.KeyFile)
|
||||
} else {
|
||||
err = web.Run(port)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(err, "服务启动失败: %s")
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package utils
|
||||
|
||||
// 数组比较
|
||||
// 依次返回,新增值,删除值,以及不变值
|
||||
func ArrayCompare(newArr []interface{}, oldArr []interface{}, compareFun func(interface{}, interface{}) bool) ([]interface{}, []interface{}, []interface{}) {
|
||||
var unmodifierValue []interface{}
|
||||
ni, oi := 0, 0
|
||||
for {
|
||||
if ni >= len(newArr) {
|
||||
break
|
||||
}
|
||||
nv := newArr[ni]
|
||||
for {
|
||||
if oi >= len(oldArr) {
|
||||
oi = 0
|
||||
break
|
||||
}
|
||||
ov := oldArr[oi]
|
||||
if compareFun(nv, ov) {
|
||||
unmodifierValue = append(unmodifierValue, nv)
|
||||
// 新数组移除该位置值
|
||||
newArr = append(newArr[:ni], newArr[ni+1:]...)
|
||||
oldArr = append(oldArr[:oi], oldArr[oi+1:]...)
|
||||
ni = ni - 1
|
||||
oi = oi - 1
|
||||
}
|
||||
oi = oi + 1
|
||||
}
|
||||
ni = ni + 1
|
||||
}
|
||||
|
||||
return newArr, oldArr, unmodifierValue
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArrayCompare(t *testing.T) {
|
||||
newArr := []interface{}{1, 2, 3, 5}
|
||||
oldArr := []interface{}{3, 6}
|
||||
add, del, unmodifier := ArrayCompare(newArr, oldArr, func(i1, i2 interface{}) bool {
|
||||
return i1.(int) == i2.(int)
|
||||
})
|
||||
fmt.Println(add...)
|
||||
fmt.Println(del...)
|
||||
fmt.Println(unmodifier...)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// md5
|
||||
func Md5(str string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(str))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func Json2Map(jsonStr string) map[string]interface{} {
|
||||
var res map[string]interface{}
|
||||
if jsonStr == "" {
|
||||
return res
|
||||
}
|
||||
_ = json.Unmarshal([]byte(jsonStr), &res)
|
||||
return res
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetString4Map(m map[string]interface{}, key string) string {
|
||||
return m[key].(string)
|
||||
}
|
||||
|
||||
func GetInt4Map(m map[string]interface{}, key string) int {
|
||||
i := m[key]
|
||||
iKind := reflect.TypeOf(i).Kind()
|
||||
if iKind == reflect.Int {
|
||||
return i.(int)
|
||||
}
|
||||
if iKind == reflect.String {
|
||||
i, _ := strconv.Atoi(i.(string))
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// map构造器
|
||||
type mapBuilder struct {
|
||||
m map[string]interface{}
|
||||
}
|
||||
|
||||
func MapBuilder(key string, value interface{}) *mapBuilder {
|
||||
mb := new(mapBuilder)
|
||||
mb.m = make(map[string]interface{}, 4)
|
||||
mb.m[key] = value
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *mapBuilder) Put(key string, value interface{}) *mapBuilder {
|
||||
mb.m[key] = value
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *mapBuilder) ToMap() map[string]interface{} {
|
||||
return mb.m
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package utils
|
||||
|
||||
import "runtime"
|
||||
|
||||
// 获取调用堆栈信息
|
||||
func GetStackTrace() string {
|
||||
var buf [2 << 10]byte
|
||||
return string(buf[:runtime.Stack(buf[:], false)])
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package ws
|
||||
|
||||
const SuccessMsgType = 1
|
||||
const ErrorMsgType = 0
|
||||
const InfoMsgType = 2
|
||||
|
||||
// websocket消息
|
||||
type Msg struct {
|
||||
Type int `json:"type"` // 消息类型
|
||||
Title string `json:"title"` // 消息标题
|
||||
Msg string `json:"msg"` // 消息内容
|
||||
}
|
||||
|
||||
// 普通消息
|
||||
func NewMsg(title, msg string) *Msg {
|
||||
return &Msg{Type: InfoMsgType, Title: title, Msg: msg}
|
||||
}
|
||||
|
||||
// 成功消息
|
||||
func SuccessMsg(title, msg string) *Msg {
|
||||
return &Msg{Type: SuccessMsgType, Title: title, Msg: msg}
|
||||
}
|
||||
|
||||
// 错误消息
|
||||
func ErrMsg(title, msg string) *Msg {
|
||||
return &Msg{Type: ErrorMsgType, Title: title, Msg: msg}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/base/global"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var Upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024 * 1024 * 10,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
var conns = make(map[uint64]*websocket.Conn, 100)
|
||||
|
||||
func init() {
|
||||
checkConn()
|
||||
}
|
||||
|
||||
// 放置ws连接
|
||||
func Put(userId uint64, conn *websocket.Conn) {
|
||||
existConn := conns[userId]
|
||||
if existConn != nil {
|
||||
Delete(userId)
|
||||
}
|
||||
|
||||
conn.SetCloseHandler(func(code int, text string) error {
|
||||
Delete(userId)
|
||||
return nil
|
||||
})
|
||||
conns[userId] = conn
|
||||
}
|
||||
|
||||
func checkConn() {
|
||||
heartbeat := time.Duration(60) * time.Second
|
||||
tick := time.NewTicker(heartbeat)
|
||||
go func() {
|
||||
for range tick.C {
|
||||
// 遍历所有连接,ping失败的则删除掉
|
||||
for uid, conn := range conns {
|
||||
err := conn.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(heartbeat/2))
|
||||
if err != nil {
|
||||
Delete(uid)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 删除ws连接
|
||||
func Delete(userid uint64) {
|
||||
global.Log.Info("移除websocket连接:uid = ", userid)
|
||||
conn := conns[userid]
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
delete(conns, userid)
|
||||
}
|
||||
}
|
||||
|
||||
// 对指定用户发送消息
|
||||
func SendMsg(userId uint64, msg *Msg) {
|
||||
conn := conns[userId]
|
||||
if conn != nil {
|
||||
bytes, _ := json.Marshal(msg)
|
||||
conn.WriteMessage(websocket.TextMessage, bytes)
|
||||
}
|
||||
}
|
||||
201
build_release.sh
Executable file
@@ -0,0 +1,201 @@
|
||||
#bin/bash
|
||||
|
||||
#----------------------------------------------
|
||||
# 前后端打包编译至指定目录,即快速制作发行版
|
||||
#----------------------------------------------
|
||||
|
||||
project_path=`pwd`
|
||||
# 构建后的二进制执行文件名
|
||||
exec_file_name="mayfly-go"
|
||||
# web项目目录
|
||||
web_folder="${project_path}/frontend"
|
||||
# server目录
|
||||
server_folder="${project_path}/server"
|
||||
|
||||
function echo_red() {
|
||||
echo -e "\033[1;31m$1\033[0m"
|
||||
}
|
||||
|
||||
function echo_green() {
|
||||
echo -e "\033[1;32m$1\033[0m"
|
||||
}
|
||||
|
||||
function echo_yellow() {
|
||||
echo -e "\033[1;33m$1\033[0m"
|
||||
}
|
||||
|
||||
function buildWeb() {
|
||||
cd ${web_folder}
|
||||
copy2Server=$1
|
||||
|
||||
echo_yellow "-------------------Start bundling frontends-------------------"
|
||||
yarn run build
|
||||
if [ "${copy2Server}" == "2" ] ; then
|
||||
echo_green 'Copy the packaged static files to server/static/static'
|
||||
rm -rf ${server_folder}/static/static && mkdir -p ${server_folder}/static/static && cp -r ${web_folder}/dist/* ${server_folder}/static/static
|
||||
fi
|
||||
echo_yellow ">>>>>>>>>>>>>>>>>>>End of packaging frontend<<<<<<<<<<<<<<<<<<<<\n"
|
||||
}
|
||||
|
||||
function build() {
|
||||
cd ${project_path}
|
||||
|
||||
# 打包产物的输出目录
|
||||
toFolder=$1
|
||||
os=$2
|
||||
arch=$3
|
||||
copyDocScript=$4
|
||||
|
||||
echo_yellow "-------------------Start a bundle build - ${os}-${arch}-------------------"
|
||||
|
||||
cd ${server_folder}
|
||||
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 -ldflags=-w -o ${execFileName} main.go
|
||||
|
||||
if [ -d ${toFolder} ] ; then
|
||||
echo_green "The desired folder already exists. Clear the folder"
|
||||
sudo rm -rf ${toFolder}
|
||||
fi
|
||||
echo_green "Create '${toFolder}' Directory"
|
||||
mkdir ${toFolder}
|
||||
|
||||
echo_green "Move binary to '${toFolder}'"
|
||||
mv ${server_folder}/${execFileName} ${toFolder}
|
||||
|
||||
# if [ "${copy2Server}" == "1" ] ; then
|
||||
# echo_green "拷贝前端静态页面至'${toFolder}/static'"
|
||||
# mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
|
||||
# fi
|
||||
|
||||
if [ "${copyDocScript}" == "1" ] ; then
|
||||
echo_green "Copy resources such as scripts [config.yml.example、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_yellow ">>>>>>>>>>>>>>>>>>> ${os}-${arch} - Bundle build complete <<<<<<<<<<<<<<<<<<<<\n"
|
||||
}
|
||||
|
||||
function buildLinuxAmd64() {
|
||||
build "$1/mayfly-go-linux-amd64" "linux" "amd64" $2
|
||||
}
|
||||
|
||||
function buildLinuxArm64() {
|
||||
build "$1/mayfly-go-linux-arm64" "linux" "arm64" $2
|
||||
}
|
||||
|
||||
function buildWindows() {
|
||||
build "$1/mayfly-go-windows" "windows" "amd64" $2
|
||||
}
|
||||
|
||||
function buildMac() {
|
||||
build "$1/mayfly-go-mac" "darwin" "amd64" $2
|
||||
}
|
||||
|
||||
function buildDocker() {
|
||||
echo_yellow "-------------------Start building the docker image-------------------"
|
||||
imageVersion=$1
|
||||
imageName="mayfly/mayfly-go:${imageVersion}"
|
||||
docker build --no-cache --platform linux/amd64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
|
||||
echo_green "The docker image is built -> [${imageName}]"
|
||||
echo_yellow "-------------------Finished building the docker image-------------------"
|
||||
}
|
||||
|
||||
function buildxDocker() {
|
||||
echo_yellow "-------------------The docker buildx build image starts-------------------"
|
||||
imageVersion=$1
|
||||
imageName="ccr.ccs.tencentyun.com/mayfly/mayfly-go:${imageVersion}"
|
||||
docker buildx build --no-cache --push --platform linux/amd64,linux/arm64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
|
||||
echo_green "The docker multi-architecture version image is built -> [${imageName}]"
|
||||
echo_yellow "-------------------The docker buildx image is finished-------------------"
|
||||
}
|
||||
|
||||
function runBuild() {
|
||||
read -p "Select build version [0 | Other->Other than docker image 1->linux-amd64 2->linux-arm64 3->windows 4->mac 5->docker 6->docker buildx]: " buildType
|
||||
|
||||
toPath="."
|
||||
imageVersion="latest"
|
||||
copyDocScript="1"
|
||||
|
||||
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
|
||||
# 构建结果的目的路径
|
||||
read -p "Please enter the build product output directory [default current path]: " toPath
|
||||
if [ ! -d ${toPath} ] ; then
|
||||
echo_red "Build product output directory does not exist!"
|
||||
exit;
|
||||
fi
|
||||
if [ "${toPath}" == "" ] ; then
|
||||
toPath="."
|
||||
fi
|
||||
|
||||
read -p "Whether to copy documents & Scripts [0-> No 1-> Yes][Default yes]: " copyDocScript
|
||||
if [ "${copyDocScript}" == "" ] ; then
|
||||
copyDocScript="1"
|
||||
fi
|
||||
|
||||
# 进入目标路径,并赋值全路径
|
||||
cd ${toPath}
|
||||
toPath=`pwd`
|
||||
|
||||
# read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
|
||||
runBuildWeb="2"
|
||||
# 编译web前端
|
||||
buildWeb ${runBuildWeb}
|
||||
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} ${copyDocScript}
|
||||
;;
|
||||
"2")
|
||||
buildLinuxArm64 ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"3")
|
||||
buildWindows ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"4")
|
||||
buildMac ${toPath} ${copyDocScript}
|
||||
;;
|
||||
"5")
|
||||
buildDocker ${imageVersion}
|
||||
;;
|
||||
"6")
|
||||
buildxDocker ${imageVersion}
|
||||
;;
|
||||
*)
|
||||
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
|
||||
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,
|
||||