mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-21 17:21:09 +01:00
code cleanup
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
FROM golang:1.12-alpine AS build
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY . .
|
||||
|
||||
ENV GO111MODULE on
|
||||
ENV GOPROXY https://mirrors.aliyun.com/goproxy/
|
||||
|
||||
RUN go mod vendor
|
||||
RUN go install -v ./...
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /root
|
||||
COPY --from=build /go/src/app .
|
||||
COPY --from=build /go/bin/crawlab /usr/local/bin
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["crawlab"]
|
||||
@@ -2,30 +2,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"crawlab/apps"
|
||||
"fmt"
|
||||
"github.com/crawlab-team/crawlab-core/entity"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
runOnMaster bool
|
||||
masterConfigPath string
|
||||
masterGrpcAddress string
|
||||
masterGrpcAuthKey string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(masterCmd)
|
||||
|
||||
masterCmd.PersistentFlags().StringVarP(&masterConfigPath, "config-path", "c", "", "Config path of master node")
|
||||
_ = viper.BindPFlag("configPath", masterCmd.PersistentFlags().Lookup("configPath"))
|
||||
|
||||
masterCmd.PersistentFlags().StringVarP(&masterGrpcAddress, "grpc-address", "g", "", "gRPC address of master node")
|
||||
_ = viper.BindPFlag("grpcAddress", masterCmd.PersistentFlags().Lookup("grpcAddress"))
|
||||
|
||||
masterCmd.PersistentFlags().StringVarP(&masterGrpcAuthKey, "grpc-auth-key", "a", "", "gRPC auth key of master node")
|
||||
_ = viper.BindPFlag("grpcAuthKey", masterCmd.PersistentFlags().Lookup("grpcAuthKey"))
|
||||
}
|
||||
|
||||
var masterCmd = &cobra.Command{
|
||||
@@ -37,23 +18,6 @@ which runs api and assign tasks to worker nodes`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// options
|
||||
var opts []apps.MasterOption
|
||||
if masterConfigPath != "" {
|
||||
opts = append(opts, apps.WithMasterConfigPath(masterConfigPath))
|
||||
viper.Set("config.path", masterConfigPath)
|
||||
}
|
||||
opts = append(opts, apps.WithRunOnMaster(runOnMaster))
|
||||
if masterGrpcAddress != "" {
|
||||
address, err := entity.NewAddressFromString(masterGrpcAddress)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Sprintf("invalid grpc-address: %s", masterGrpcAddress))
|
||||
}
|
||||
opts = append(opts, apps.WithMasterGrpcAddress(address))
|
||||
viper.Set("grpc.address", masterGrpcAddress)
|
||||
viper.Set("grpc.server.address", masterGrpcAddress)
|
||||
}
|
||||
if masterGrpcAuthKey != "" {
|
||||
viper.Set("grpc.authKey", masterGrpcAuthKey)
|
||||
}
|
||||
|
||||
// app
|
||||
master := apps.NewMaster(opts...)
|
||||
|
||||
@@ -50,6 +50,7 @@ func initConfig() {
|
||||
replacer := strings.NewReplacer(".", "_")
|
||||
viper.SetEnvKeyReplacer(replacer)
|
||||
|
||||
// read config file
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
@@ -2,29 +2,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"crawlab/apps"
|
||||
"fmt"
|
||||
"github.com/crawlab-team/crawlab-core/entity"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
workerConfigPath string
|
||||
workerGrpcAddress string
|
||||
workerGrpcAuthKey string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(workerCmd)
|
||||
|
||||
workerCmd.PersistentFlags().StringVarP(&workerConfigPath, "config-path", "c", "", "Config path of worker node")
|
||||
_ = viper.BindPFlag("configPath", workerCmd.PersistentFlags().Lookup("configPath"))
|
||||
|
||||
workerCmd.PersistentFlags().StringVarP(&workerGrpcAddress, "grpc-address", "g", "", "gRPC address of worker node")
|
||||
_ = viper.BindPFlag("grpcAddress", workerCmd.PersistentFlags().Lookup("grpcAddress"))
|
||||
|
||||
workerCmd.PersistentFlags().StringVarP(&workerGrpcAuthKey, "grpc-auth-key", "a", "", "gRPC auth key of worker node")
|
||||
_ = viper.BindPFlag("grpcAuthKey", workerCmd.PersistentFlags().Lookup("grpcAuthKey"))
|
||||
}
|
||||
|
||||
var workerCmd = &cobra.Command{
|
||||
@@ -37,22 +19,6 @@ assigned by the master node`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// options
|
||||
var opts []apps.WorkerOption
|
||||
if workerConfigPath != "" {
|
||||
opts = append(opts, apps.WithWorkerConfigPath(workerConfigPath))
|
||||
viper.Set("config.path", workerConfigPath)
|
||||
}
|
||||
if workerGrpcAddress != "" {
|
||||
address, err := entity.NewAddressFromString(workerGrpcAddress)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Sprintf("invalid grpc-address: %s", workerGrpcAddress))
|
||||
return
|
||||
}
|
||||
opts = append(opts, apps.WithWorkerGrpcAddress(address))
|
||||
viper.Set("grpc.address", workerGrpcAddress)
|
||||
}
|
||||
if workerGrpcAuthKey != "" {
|
||||
viper.Set("grpc.authKey", workerGrpcAuthKey)
|
||||
}
|
||||
|
||||
// app
|
||||
master := apps.NewWorker(opts...)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"key": "master",
|
||||
"is_master": true,
|
||||
"name": "master",
|
||||
"name": "Master Node",
|
||||
"ip": "",
|
||||
"mac": "",
|
||||
"hostname": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"key": "worker",
|
||||
"key": "worker-01",
|
||||
"is_master": false,
|
||||
"name": "worker",
|
||||
"name": "Worker Node 01",
|
||||
"ip": "",
|
||||
"mac": "",
|
||||
"hostname": "",
|
||||
10
backend/test/config-worker-02.json
Normal file
10
backend/test/config-worker-02.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"key": "worker-02",
|
||||
"is_master": false,
|
||||
"name": "Worker Node 02",
|
||||
"ip": "",
|
||||
"mac": "",
|
||||
"hostname": "",
|
||||
"description": "",
|
||||
"auth_key": "Crawlab2021!"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
@@ -1 +0,0 @@
|
||||
node_modules/
|
||||
@@ -1,32 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# charset = utf-8
|
||||
# end_of_line = lf
|
||||
# indent_size = 4
|
||||
# indent_style = space
|
||||
# insert_final_newline = true
|
||||
# max_line_length = 120
|
||||
# tab_width = 4
|
||||
# trim_trailing_whitespace = true
|
||||
|
||||
[*.scss]
|
||||
# indent_size = 2
|
||||
|
||||
[{*.ats, *.ts}]
|
||||
# indent_size = 2
|
||||
# tab_width = 2
|
||||
|
||||
[{*.js, *.cjs}]
|
||||
# indent_size = 2
|
||||
# tab_width = 2
|
||||
|
||||
[{*.sht, *.html, *.shtm, *.shtml, *.htm, *.ng}]
|
||||
# indent_size = 2
|
||||
# tab_width = 2
|
||||
|
||||
[{.analysis_options, *.yml, *.yaml}]
|
||||
# indent_size = 2
|
||||
|
||||
[{.babelrc, .prettierrc, .stylelintrc, .eslintrc, jest.config, *.json, *.jsb3, *.jsb2, *.bowerrc}]
|
||||
# indent_size = 2
|
||||
@@ -1,2 +0,0 @@
|
||||
NODE_ENV='development'
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000
|
||||
@@ -1,2 +0,0 @@
|
||||
NODE_ENV='production'
|
||||
VUE_APP_API_BASE_URL='VUE_APP_API_BASE_URL'
|
||||
@@ -1,2 +0,0 @@
|
||||
NODE_ENV='production'
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000
|
||||
@@ -1,2 +0,0 @@
|
||||
NODE_ENV='test'
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000
|
||||
@@ -1,2 +1,3 @@
|
||||
*/**/*.js
|
||||
./src/i18n/**/*.ts
|
||||
*/**/*.js
|
||||
*.js
|
||||
|
||||
6
frontend/.gitignore
vendored
6
frontend/.gitignore
vendored
@@ -23,3 +23,9 @@ pnpm-debug.log*
|
||||
*.sw?
|
||||
|
||||
tmp/
|
||||
lib/
|
||||
**/.DS_Store
|
||||
**/.idea
|
||||
**/dist
|
||||
**/node_modules
|
||||
**/package-lock.json
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020, Crawlab Team
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,24 +0,0 @@
|
||||
# crawlab-frontend
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -1,73 +1,23 @@
|
||||
{
|
||||
"name": "@crawlab/frontend",
|
||||
"version": "0.6.0-beta.20210715",
|
||||
"private": false,
|
||||
"name": "@crawlab/app",
|
||||
"version": "0.6.0-beta.202109062026",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --port=8081",
|
||||
"serve:build:local": "vue-cli-service serve --port=8082 --model local",
|
||||
"build": "vue-cli-service build",
|
||||
"build:docker": "vue-cli-service build --mode docker",
|
||||
"build:local": "vue-cli-service build --mode local",
|
||||
"lint": "vue-cli-service lint",
|
||||
"test": "jest"
|
||||
"serve": "vue-cli-service serve",
|
||||
"buid": "vue-cli-service build"
|
||||
},
|
||||
"author": {
|
||||
"name": "Marvin Zhang",
|
||||
"email": "tikazyq@163.com"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-2",
|
||||
"@popperjs/core": "^2.6.0",
|
||||
"@types/codemirror": "^0.0.103",
|
||||
"@types/echarts": "^4.9.8",
|
||||
"@types/getos": "^3.0.1",
|
||||
"@types/humanize-duration": "^3.25.0",
|
||||
"@types/javascript-time-ago": "^2.0.2",
|
||||
"@types/md5": "^2.2.1",
|
||||
"@types/pinyin": "^2.8.2",
|
||||
"atom-material-icons": "^3.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"codemirror": "^5.59.1",
|
||||
"core-js": "^3.6.5",
|
||||
"cron-parser": "^3.5.0",
|
||||
"cronstrue": "^1.114.0",
|
||||
"dayjs": "^1.10.5",
|
||||
"echarts": "^5.1.2",
|
||||
"element-plus": "1.0.2-beta.40",
|
||||
"font-awesome": "^4.7.0",
|
||||
"getos": "^3.2.1",
|
||||
"humanize-duration": "^3.26.0",
|
||||
"javascript-time-ago": "^2.3.6",
|
||||
"md5": "^2.3.0",
|
||||
"node-sass": "^5.0.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinyin": "^2.10.2",
|
||||
"point-cluster": "^3.1.8",
|
||||
"vue": "^3.0.4",
|
||||
"vue-clipboard3": "^1.0.1",
|
||||
"vue-i18n": "^9.0.0-beta.11",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vue3-dropzone": "^0.0.7",
|
||||
"vue3-sfc-loader": "^0.8.4",
|
||||
"vuex": "^4.0.0-0"
|
||||
"crawlab-ui": "0.6.0-beta.202109082041",
|
||||
"vue": "3.0.11",
|
||||
"vue-router": "^4.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@types/jest": "^26.0.19",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"scss-loader": "^0.0.1",
|
||||
"typescript": "~3.9.3"
|
||||
"@vue/cli-service": "^4.5.13",
|
||||
"@vue/compiler-sfc": "^3.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
4
frontend/public/font-awesome.min.css
vendored
4
frontend/public/font-awesome.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,215 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
||||
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
|
||||
<link href="font-awesome.min.css" rel="stylesheet">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<script src="/js/vue3-sfc-loader.js"></script>
|
||||
<style>
|
||||
#loading-placeholder {
|
||||
position: fixed;
|
||||
background: white;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#loading-placeholder .title-wrapper {
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
#loading-placeholder .title {
|
||||
font-family: "Verdana", serif;
|
||||
font-weight: 600;
|
||||
font-size: 48px;
|
||||
color: #409EFF;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
letter-spacing: -5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span {
|
||||
display: inline-block;
|
||||
animation: change-shape 1s infinite;
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(1) {
|
||||
animation-delay: calc(1s / 7 * 0 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(2) {
|
||||
animation-delay: calc(1s / 7 * 1 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(3) {
|
||||
animation-delay: calc(1s / 7 * 2 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(4) {
|
||||
animation-delay: calc(1s / 7 * 3 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(5) {
|
||||
animation-delay: calc(1s / 7 * 4 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(6) {
|
||||
animation-delay: calc(1s / 7 * 5 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(7) {
|
||||
animation-delay: calc(1s / 7 * 6 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .sub-title-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
#loading-placeholder .sub-title-wrapper .sub-title {
|
||||
position: absolute;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
font-family: "Verdana", serif;
|
||||
font-style: italic;
|
||||
color: #67C23A;
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
animation: flip 20s infinite;
|
||||
/*color: #E6A23C;*/
|
||||
/*color: #F56C6C;*/
|
||||
}
|
||||
|
||||
#loading-placeholder .sub-title-wrapper > .sub-title:nth-child(1) {
|
||||
animation-delay: calc(20s / 4 * 0);
|
||||
}
|
||||
|
||||
#loading-placeholder .sub-title-wrapper > .sub-title:nth-child(2) {
|
||||
animation-delay: calc(20s / 4 * 1);
|
||||
}
|
||||
|
||||
#loading-placeholder .sub-title-wrapper > .sub-title:nth-child(3) {
|
||||
animation-delay: calc(20s / 4 * 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .sub-title-wrapper > .sub-title:nth-child(4) {
|
||||
animation-delay: calc(20s / 4 * 3);
|
||||
}
|
||||
|
||||
#loading-placeholder .loading-text {
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
font-family: "Verdana", serif;
|
||||
font-style: italic;
|
||||
color: #889aa4;
|
||||
font-size: 14px;
|
||||
animation: blink-loading 2s ease-in infinite;
|
||||
}
|
||||
|
||||
@keyframes blink-loading {
|
||||
0% {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes change-shape {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flip {
|
||||
0% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
2% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
7% {
|
||||
transform: rotate3d(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
23% {
|
||||
transform: rotate3d(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
27% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
||||
<script src="/js/vue3-sfc-loader.js"></script>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.</strong>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="loading-placeholder">
|
||||
<div style="margin-bottom: 150px">
|
||||
<div class="title-wrapper">
|
||||
<h3 class="title">
|
||||
<span>C</span>
|
||||
<span>R</span>
|
||||
<span>A</span>
|
||||
<span>W</span>
|
||||
<span>L</span>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="sub-title-wrapper">
|
||||
<span class="sub-title"><i class="fa fa-cloud-download"></i> Easy Crawling</span>
|
||||
<span class="sub-title"><i class="fa fa-diamond"></i> Better Management</span>
|
||||
<span class="sub-title"><i class="fa fa-dollar"></i> Gain Data Value</span>
|
||||
<span class="sub-title"><i class="fa fa-server"></i> Good Scalability</span>
|
||||
</div>
|
||||
<div class="loading-text">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
1
frontend/public/js/vue3-sfc-loader.js.map
Normal file
1
frontend/public/js/vue3-sfc-loader.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
App
|
||||
<hello-world/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HelloWorld from './HelloWorld.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
HelloWorld,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
Hello World
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent, onBeforeMount} from 'vue';
|
||||
import {initPlugins} from '@/utils/plugin';
|
||||
import {useStore} from 'vuex';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
setup() {
|
||||
onBeforeMount(() => {
|
||||
const store = useStore();
|
||||
|
||||
initPlugins(store);
|
||||
});
|
||||
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
// baidu tongji
|
||||
export const initBaiduTonji = () => {
|
||||
if (localStorage.getItem('useStats') !== '0') {
|
||||
window._hmt = window._hmt || [];
|
||||
(function () {
|
||||
const hm = document.createElement('script');
|
||||
hm.src = 'https://hm.baidu.com/hm.js?c35e3a563a06caee2524902c81975add';
|
||||
const s = document.getElementsByTagName('script')[0];
|
||||
s?.parentNode?.insertBefore(hm, s);
|
||||
})();
|
||||
}
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,262 +0,0 @@
|
||||
function initCanvas() {
|
||||
let canvas, ctx, circ, nodes, mouse, SENSITIVITY, SIBLINGS_LIMIT, DENSITY, NODES_QTY, ANCHOR_LENGTH, MOUSE_RADIUS,
|
||||
TURBULENCE, MOUSE_MOVING_TURBULENCE, MOUSE_ANGLE_TURBULENCE, MOUSE_MOVING_RADIUS, BASE_BRIGHTNESS, RADIUS_DEGRADE,
|
||||
SAMPLE_SIZE
|
||||
|
||||
let handle
|
||||
|
||||
// how close next node must be to activate connection (in px)
|
||||
// shorter distance == better connection (line width)
|
||||
SENSITIVITY = 200
|
||||
// note that siblings limit is not 'accurate' as the node can actually have more connections than this value that's because the node accepts sibling nodes with no regard to their current connections this is acceptable because potential fix would not result in significant visual difference
|
||||
// more siblings == bigger node
|
||||
SIBLINGS_LIMIT = 10
|
||||
// default node margin
|
||||
DENSITY = 100
|
||||
// total number of nodes used (incremented after creation)
|
||||
NODES_QTY = 0
|
||||
// avoid nodes spreading
|
||||
ANCHOR_LENGTH = 100
|
||||
// highlight radius
|
||||
MOUSE_RADIUS = 200
|
||||
// turbulence of randomness
|
||||
TURBULENCE = 3
|
||||
// turbulence of mouse moving
|
||||
MOUSE_MOVING_TURBULENCE = 50
|
||||
// turbulence of mouse moving angle
|
||||
MOUSE_ANGLE_TURBULENCE = 0.002
|
||||
// moving radius of mouse
|
||||
MOUSE_MOVING_RADIUS = 600
|
||||
// base brightness
|
||||
BASE_BRIGHTNESS = 0.12
|
||||
// radius degrade
|
||||
RADIUS_DEGRADE = 0.4
|
||||
// sample size
|
||||
SAMPLE_SIZE = 0.5
|
||||
|
||||
circ = 2 * Math.PI
|
||||
nodes = []
|
||||
|
||||
canvas = document.querySelector('canvas')
|
||||
resizeWindow()
|
||||
ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
alert('Ooops! Your browser does not support canvas :\'(')
|
||||
}
|
||||
|
||||
function Mouse(x, y) {
|
||||
this.anchorX = x
|
||||
this.anchorY = y
|
||||
this.x = x
|
||||
this.y = y - MOUSE_RADIUS / 2
|
||||
this.angle = 0
|
||||
}
|
||||
|
||||
Mouse.prototype.computePosition = function () {
|
||||
// this.x = this.anchorX + MOUSE_MOVING_RADIUS / 2 * Math.sin(this.angle)
|
||||
// this.y = this.anchorY - MOUSE_MOVING_RADIUS / 2 * Math.cos(this.angle)
|
||||
}
|
||||
|
||||
Mouse.prototype.move = function () {
|
||||
let vx = Math.random() * MOUSE_MOVING_TURBULENCE
|
||||
let vy = Math.random() * MOUSE_MOVING_TURBULENCE
|
||||
if (this.x + vx + MOUSE_RADIUS / 2 > window.innerWidth || this.x + vx - MOUSE_RADIUS / 2 < 0) {
|
||||
vx = -vx
|
||||
}
|
||||
if (this.y + vy + MOUSE_RADIUS / 2 > window.innerHeight || this.y + vy - MOUSE_RADIUS / 2 < 0) {
|
||||
vy = -vy
|
||||
}
|
||||
this.x += vx
|
||||
this.y += vy
|
||||
// this.angle += Math.random() * MOUSE_ANGLE_TURBULENCE * 2 * Math.PI
|
||||
// this.angle -= Math.floor(this.angle / (2 * Math.PI)) * 2 * Math.PI
|
||||
// this.computePosition()
|
||||
}
|
||||
|
||||
function Node(x, y) {
|
||||
this.anchorX = x
|
||||
this.anchorY = y
|
||||
this.x = Math.random() * (x - (x - ANCHOR_LENGTH)) + (x - ANCHOR_LENGTH)
|
||||
this.y = Math.random() * (y - (y - ANCHOR_LENGTH)) + (y - ANCHOR_LENGTH)
|
||||
this.vx = Math.random() * TURBULENCE - 1
|
||||
this.vy = Math.random() * TURBULENCE - 1
|
||||
this.energy = Math.random() * 100
|
||||
this.radius = Math.random()
|
||||
this.siblings = []
|
||||
this.brightness = 0
|
||||
}
|
||||
|
||||
Node.prototype.drawNode = function () {
|
||||
let color = 'rgba(64, 156, 255, ' + this.brightness + ')'
|
||||
ctx.beginPath()
|
||||
ctx.arc(this.x, this.y, 2 * this.radius + 2 * this.siblings.length / SIBLINGS_LIMIT / 1.5, 0, circ)
|
||||
ctx.fillStyle = color
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
Node.prototype.drawConnections = function () {
|
||||
for (let i = 0; i < this.siblings.length; i++) {
|
||||
let color = 'rgba(64, 156, 255, ' + this.brightness + ')'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(this.x, this.y)
|
||||
ctx.lineTo(this.siblings[i].x, this.siblings[i].y)
|
||||
ctx.lineWidth = 1 - calcDistance(this, this.siblings[i]) / SENSITIVITY
|
||||
ctx.strokeStyle = color
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
Node.prototype.moveNode = function () {
|
||||
this.energy -= 2
|
||||
if (this.energy < 1) {
|
||||
this.energy = Math.random() * 100
|
||||
if (this.x - this.anchorX < -ANCHOR_LENGTH) {
|
||||
this.vx = Math.random() * TURBULENCE
|
||||
} else if (this.x - this.anchorX > ANCHOR_LENGTH) {
|
||||
this.vx = Math.random() * -TURBULENCE
|
||||
} else {
|
||||
this.vx = Math.random() * 2 * TURBULENCE - TURBULENCE
|
||||
}
|
||||
if (this.y - this.anchorY < -ANCHOR_LENGTH) {
|
||||
this.vy = Math.random() * TURBULENCE
|
||||
} else if (this.y - this.anchorY > ANCHOR_LENGTH) {
|
||||
this.vy = Math.random() * -TURBULENCE
|
||||
} else {
|
||||
this.vy = Math.random() * 2 * TURBULENCE - TURBULENCE
|
||||
}
|
||||
}
|
||||
this.x += this.vx * this.energy / 100
|
||||
this.y += this.vy * this.energy / 100
|
||||
}
|
||||
|
||||
function Handle() {
|
||||
this.isStopped = false
|
||||
}
|
||||
|
||||
Handle.prototype.stop = function () {
|
||||
this.isStopped = true
|
||||
}
|
||||
|
||||
function initNodes() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
nodes = []
|
||||
for (let i = DENSITY; i < canvas.width; i += DENSITY) {
|
||||
for (let j = DENSITY; j < canvas.height; j += DENSITY) {
|
||||
nodes.push(new Node(i, j))
|
||||
NODES_QTY++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initMouse() {
|
||||
mouse = new Mouse(canvas.width / 2, canvas.height / 2)
|
||||
}
|
||||
|
||||
function initHandle() {
|
||||
handle = new Handle()
|
||||
}
|
||||
|
||||
function calcDistance(node1, node2) {
|
||||
return Math.sqrt(Math.pow(node1.x - node2.x, 2) + (Math.pow(node1.y - node2.y, 2)))
|
||||
}
|
||||
|
||||
function findSiblings() {
|
||||
let node1, node2, distance
|
||||
for (let i = 0; i < NODES_QTY; i++) {
|
||||
node1 = nodes[i]
|
||||
node1.siblings = []
|
||||
for (let j = 0; j < NODES_QTY; j++) {
|
||||
node2 = nodes[j]
|
||||
if (node1 !== node2) {
|
||||
distance = calcDistance(node1, node2)
|
||||
if (distance < SENSITIVITY) {
|
||||
if (node1.siblings.length < SIBLINGS_LIMIT) {
|
||||
node1.siblings.push(node2)
|
||||
} else {
|
||||
let node_sibling_distance = 0
|
||||
let max_distance = 0
|
||||
let s
|
||||
for (let k = 0; k < SIBLINGS_LIMIT; k++) {
|
||||
node_sibling_distance = calcDistance(node1, node1.siblings[k])
|
||||
if (node_sibling_distance > max_distance) {
|
||||
max_distance = node_sibling_distance
|
||||
s = k
|
||||
}
|
||||
}
|
||||
if (distance < max_distance) {
|
||||
node1.siblings.splice(s, 1)
|
||||
node1.siblings.push(node2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function redrawScene() {
|
||||
if (handle && handle.isStopped) {
|
||||
return
|
||||
}
|
||||
resizeWindow()
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
findSiblings()
|
||||
let i, node, distance
|
||||
for (i = 0; i < NODES_QTY; i++) {
|
||||
node = nodes[i]
|
||||
distance = calcDistance({
|
||||
x: mouse.x,
|
||||
y: mouse.y
|
||||
}, node)
|
||||
node.brightness = (1 - Math.log(distance / MOUSE_RADIUS * RADIUS_DEGRADE)) * BASE_BRIGHTNESS
|
||||
}
|
||||
for (i = 0; i < NODES_QTY; i++) {
|
||||
node = nodes[i]
|
||||
if (node.brightness) {
|
||||
node.drawNode()
|
||||
node.drawConnections()
|
||||
}
|
||||
node.moveNode()
|
||||
}
|
||||
// mouse.move()
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(redrawScene)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function initHandlers() {
|
||||
document.addEventListener('resize', resizeWindow, false)
|
||||
// canvas.addEventListener('mousemove', mousemoveHandler, false)
|
||||
}
|
||||
|
||||
function resizeWindow() {
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
}
|
||||
|
||||
function mousemoveHandler(e) {
|
||||
mouse.x = e.clientX
|
||||
mouse.y = e.clientY
|
||||
}
|
||||
|
||||
function init() {
|
||||
initHandlers()
|
||||
initNodes()
|
||||
initMouse()
|
||||
initHandle()
|
||||
redrawScene()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
handle.isStopped = true
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
window.resetCanvas = reset
|
||||
}
|
||||
|
||||
(function () {
|
||||
window.initCanvas = initCanvas
|
||||
window.initCanvas()
|
||||
}())
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,16 +0,0 @@
|
||||
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<circle cx="150" cy="150" r="130" fill="none" stroke-width="40" stroke="#409eff">
|
||||
</circle>
|
||||
<circle cx="150" cy="150" r="110" fill="white">
|
||||
</circle>
|
||||
<circle cx="150" cy="150" r="70" fill="#409eff">
|
||||
</circle>
|
||||
<path d="
|
||||
M 150,150
|
||||
L 280,225
|
||||
A 150,150 90 0 0 280,75
|
||||
" fill="#409eff">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 503 B |
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<el-tooltip :content="tooltip" :disabled="!tooltip">
|
||||
<span :class="[noMargin ? 'no-margin' : '']" class="button-wrapper">
|
||||
<el-button
|
||||
:circle="circle"
|
||||
:class="[isIcon ? 'icon-button' : '']"
|
||||
:disabled="disabled"
|
||||
:plain="plain"
|
||||
:round="round"
|
||||
:size="size"
|
||||
:title="tooltip"
|
||||
:type="type"
|
||||
:loading="loading"
|
||||
class="button"
|
||||
@click="() => $emit('click')"
|
||||
>
|
||||
<slot></slot>
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
|
||||
export const buttonProps = {
|
||||
tooltip: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<BasicType>,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<BasicSize>,
|
||||
required: false,
|
||||
default: 'mini',
|
||||
},
|
||||
round: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
plain: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
noMargin: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Button',
|
||||
props: buttonProps,
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup(props) {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-wrapper {
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
|
||||
&.no-margin {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.button-wrapper >>> .icon-button {
|
||||
padding: 7px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
:circle="circle"
|
||||
:disabled="disabled"
|
||||
:plain="plain"
|
||||
:round="round"
|
||||
:size="size"
|
||||
:tooltip="tooltip"
|
||||
:type="type"
|
||||
is-icon
|
||||
class="fa-icon-button"
|
||||
@click="() => $emit('click')"
|
||||
>
|
||||
<font-awesome-icon :icon="icon"/>
|
||||
<div v-if="badgeIcon" class="badge-icon">
|
||||
<font-awesome-icon :icon="badgeIcon"/>
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
import {buttonProps} from './Button.vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
|
||||
export const faIconButtonProps = {
|
||||
icon: {
|
||||
type: [Array, String] as PropType<Icon>,
|
||||
required: true,
|
||||
},
|
||||
badgeIcon: {
|
||||
type: [Array, String] as PropType<Icon>,
|
||||
required: false,
|
||||
},
|
||||
...buttonProps,
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FaIconButton',
|
||||
components: {Button},
|
||||
props: faIconButtonProps,
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables";
|
||||
|
||||
.badge-icon {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 2px;
|
||||
font-size: 8px;
|
||||
color: $white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.el-button,
|
||||
.el-button--mini,
|
||||
.fa-icon-button,
|
||||
.fa-icon-button >>> .el-button,
|
||||
.fa-icon-button >>> .el-button--mini,
|
||||
.fa-icon-button >>> .button {
|
||||
padding: 7px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<el-tooltip :content="tooltip ? tooltip : undefined">
|
||||
<span>
|
||||
<el-button
|
||||
:circle="circle"
|
||||
:disabled="disabled"
|
||||
:icon="icon"
|
||||
:plain="plain"
|
||||
:round="round"
|
||||
:size="size"
|
||||
:title="tooltip"
|
||||
:type="type"
|
||||
class="button"
|
||||
style="padding: 7px"
|
||||
@click="() => $emit('click')"
|
||||
/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {buttonProps} from '@/components/button/Button.vue';
|
||||
|
||||
export const iconButtonProps = {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
...buttonProps,
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IconButton',
|
||||
props: iconButtonProps,
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
:circle="circle"
|
||||
:disabled="disabled"
|
||||
:plain="plain"
|
||||
:round="round"
|
||||
:size="size"
|
||||
:tooltip="tooltip"
|
||||
:type="type"
|
||||
class="label-button"
|
||||
@click="() => $emit('click')"
|
||||
>
|
||||
<font-awesome-icon v-if="icon" :icon="icon" class="icon"/>
|
||||
{{ label }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
import Button, {buttonProps} from '@/components/button/Button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LabelButton',
|
||||
components: {Button},
|
||||
props: {
|
||||
icon: {
|
||||
type: [Array, String] as PropType<Icon>,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
...buttonProps
|
||||
},
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup(props: LabelButtonProps, {emit}) {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.label-button {
|
||||
.icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,172 +0,0 @@
|
||||
<template>
|
||||
<div :style="style" class="line-chart">
|
||||
<div ref="elRef" class="echarts-element"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, PropType, ref, watch} from 'vue';
|
||||
import {init} from 'echarts';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LineChart',
|
||||
props: {
|
||||
config: {
|
||||
type: Object as PropType<EChartsConfig>,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light',
|
||||
},
|
||||
labelKey: {
|
||||
type: String,
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
},
|
||||
isTimeSeries: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props: LineChartProps, {emit}) {
|
||||
const style = computed<Partial<CSSStyleDeclaration>>(() => {
|
||||
const {width, height} = props;
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
});
|
||||
|
||||
const elRef = ref<HTMLDivElement>();
|
||||
const chart = ref<ECharts>();
|
||||
|
||||
const isMultiSeries = computed<boolean>(() => {
|
||||
const {config} = props;
|
||||
const {dataMetas} = config;
|
||||
return dataMetas ? dataMetas.length > 1 : false;
|
||||
});
|
||||
|
||||
const getSeriesData = (data: StatsResult[], key?: string) => {
|
||||
const {valueKey, labelKey, isTimeSeries} = props;
|
||||
const _valueKey = !key ? valueKey : key;
|
||||
|
||||
if (_valueKey) {
|
||||
if (isTimeSeries) {
|
||||
// time series
|
||||
return data.map(d => [d[labelKey || 'date'], d[_valueKey] || 0]);
|
||||
} else {
|
||||
// not time series
|
||||
return data.map(d => d[_valueKey] || 0);
|
||||
}
|
||||
} else {
|
||||
// default
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const getSeries = (): EChartSeries[] => {
|
||||
const {config} = props;
|
||||
const {data, dataMetas} = config;
|
||||
|
||||
if (!isMultiSeries.value) {
|
||||
// single series
|
||||
return [{
|
||||
type: 'line',
|
||||
data: getSeriesData(data),
|
||||
}];
|
||||
} else {
|
||||
// multiple series
|
||||
const series = [] as EChartSeries[];
|
||||
if (!dataMetas) return series;
|
||||
dataMetas.forEach(({key, name, yAxisIndex}) => {
|
||||
series.push({
|
||||
name,
|
||||
yAxisIndex,
|
||||
type: 'line',
|
||||
data: getSeriesData(data, key),
|
||||
});
|
||||
});
|
||||
return series;
|
||||
}
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const {config, theme, isTimeSeries} = props;
|
||||
const {option} = config;
|
||||
|
||||
// dom
|
||||
const el = elRef.value;
|
||||
if (!el) return;
|
||||
|
||||
// xAxis
|
||||
if (!option.xAxis) {
|
||||
option.xAxis = {};
|
||||
if (isTimeSeries) {
|
||||
option.xAxis.type = 'time';
|
||||
}
|
||||
}
|
||||
|
||||
// yAxis
|
||||
if (!option.yAxis) {
|
||||
option.yAxis = {};
|
||||
}
|
||||
|
||||
// series
|
||||
option.series = getSeries();
|
||||
|
||||
// tooltip
|
||||
if (!option.tooltip) {
|
||||
option.tooltip = {
|
||||
// trigger: 'axis',
|
||||
// position: ['50%', '50%'],
|
||||
// axisPointer: {
|
||||
// type: 'cross',
|
||||
// },
|
||||
};
|
||||
}
|
||||
|
||||
// legend
|
||||
option.legend = {};
|
||||
|
||||
// render
|
||||
if (!chart.value) {
|
||||
// init
|
||||
chart.value = init(el, theme);
|
||||
}
|
||||
(chart.value as ECharts).setOption(option);
|
||||
};
|
||||
|
||||
watch(() => props.config.data, render);
|
||||
watch(() => props.theme, render);
|
||||
|
||||
onMounted(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
return {
|
||||
style,
|
||||
elRef,
|
||||
render,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.line-chart {
|
||||
.echarts-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<div :class="[clickable ? 'clickable' : '']" :style="style" class="metric" @click="onClick">
|
||||
<div class="background"/>
|
||||
<div class="icon">
|
||||
<font-awesome-icon :icon="icon"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="value">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Metric',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
type: [Number, String],
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array] as PropType<Icon>,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup(props: MetricProps, {emit}) {
|
||||
const style = computed<Partial<CSSStyleDeclaration>>(() => {
|
||||
const {color} = props;
|
||||
return {
|
||||
backgroundColor: color,
|
||||
};
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
const {clickable} = props;
|
||||
if (!clickable) return;
|
||||
emit('click');
|
||||
};
|
||||
|
||||
return {
|
||||
style,
|
||||
onClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables";
|
||||
|
||||
.metric {
|
||||
padding: 10px;
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
border: 1px solid $infoLightColor;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
left: calc(64px + 10px);
|
||||
top: 0;
|
||||
width: calc(100% - 64px - 10px);
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
filter: alpha(0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-basis: 64px;
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
padding-right: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 20px;
|
||||
height: 48px;
|
||||
color: white;
|
||||
z-index: 2;
|
||||
|
||||
.title {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-weight: bolder;
|
||||
//padding: 5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-weight: bold;
|
||||
//padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<div :style="style" class="pie-chart">
|
||||
<div v-if="isEmpty" class="empty-placeholder">
|
||||
No Data Available
|
||||
</div>
|
||||
<div ref="elRef" class="echarts-element"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, PropType, ref, watch} from 'vue';
|
||||
import {init} from 'echarts';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PieChart',
|
||||
props: {
|
||||
config: {
|
||||
type: Object as PropType<EChartsConfig>,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light',
|
||||
},
|
||||
labelKey: {
|
||||
type: String,
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props: PieChartProps, {emit}) {
|
||||
const style = computed<Partial<CSSStyleDeclaration>>(() => {
|
||||
const {width, height} = props;
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
});
|
||||
|
||||
const elRef = ref<HTMLDivElement>();
|
||||
const chart = ref<ECharts>();
|
||||
|
||||
const isEmpty = computed<boolean>(() => {
|
||||
const {config} = props;
|
||||
const {data} = config;
|
||||
if (!data) return true;
|
||||
return data.length === 0;
|
||||
|
||||
});
|
||||
|
||||
const getSeriesData = (data: StatsResult[], key?: string) => {
|
||||
const {valueKey, labelKey} = props;
|
||||
const _valueKey = !key ? valueKey : key;
|
||||
|
||||
if (_valueKey) {
|
||||
return data.map(d => {
|
||||
return {
|
||||
name: d[labelKey || '_id'],
|
||||
value: d[_valueKey] || 0,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// default
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const getSeries = (): EChartSeries[] => {
|
||||
const {config} = props;
|
||||
const {data, itemStyleColorFunc} = config;
|
||||
|
||||
const seriesItem = {
|
||||
type: 'pie',
|
||||
data: getSeriesData(data || []),
|
||||
radius: ['40%', '70%'],
|
||||
alignTo: 'labelLine',
|
||||
} as EChartSeries;
|
||||
|
||||
if (itemStyleColorFunc) {
|
||||
seriesItem.itemStyle = {color: itemStyleColorFunc};
|
||||
}
|
||||
|
||||
return [seriesItem];
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const {config, theme} = props;
|
||||
const {option} = config;
|
||||
|
||||
// dom
|
||||
const el = elRef.value;
|
||||
if (!el) return;
|
||||
|
||||
// series
|
||||
option.series = getSeries();
|
||||
|
||||
// tooltip
|
||||
if (!option.tooltip) {
|
||||
option.tooltip = {
|
||||
// trigger: 'axis',
|
||||
// position: ['50%', '50%'],
|
||||
// axisPointer: {
|
||||
// type: 'cross',
|
||||
// },
|
||||
};
|
||||
}
|
||||
|
||||
// render
|
||||
if (!chart.value) {
|
||||
// init
|
||||
chart.value = init(el, theme);
|
||||
}
|
||||
(chart.value as ECharts).setOption(option);
|
||||
};
|
||||
|
||||
watch(() => props.config.data, render);
|
||||
watch(() => props.theme, render);
|
||||
|
||||
onMounted(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
return {
|
||||
isEmpty,
|
||||
style,
|
||||
elRef,
|
||||
render,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables";
|
||||
|
||||
.pie-chart {
|
||||
position: relative;
|
||||
|
||||
.empty-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.echarts-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div class="color-picker">
|
||||
<el-color-picker
|
||||
v-model="internalValue"
|
||||
:disabled="disabled"
|
||||
:predefine="predefine"
|
||||
:show-alpha="showAlpha"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onMounted, PropType, ref, watch} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ColorPicker',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
predefine: {
|
||||
type: Array as PropType<string[]>,
|
||||
},
|
||||
showAlpha: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'update:model-value',
|
||||
'change',
|
||||
],
|
||||
setup(props: ColorPickerProps, {emit}) {
|
||||
const internalValue = ref<string>();
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
internalValue.value = props.modelValue;
|
||||
});
|
||||
|
||||
const onChange = (value: string) => {
|
||||
emit('update:model-value', value);
|
||||
emit('change', value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const {modelValue} = props;
|
||||
internalValue.value = modelValue;
|
||||
});
|
||||
|
||||
return {
|
||||
internalValue,
|
||||
onChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-picker >>> .el-color-picker__trigger {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.color-picker >>> .el-color-picker__mask {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<el-popover
|
||||
:placement="placement"
|
||||
:show-arrow="false"
|
||||
:visible="visible"
|
||||
popper-class="context-menu"
|
||||
trigger="manual"
|
||||
>
|
||||
<template #default>
|
||||
<slot name="default"></slot>
|
||||
</template>
|
||||
<template #reference>
|
||||
<div v-click-outside="onClickOutside">
|
||||
<slot name="reference"></slot>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {ClickOutside} from 'element-plus/lib/directives';
|
||||
|
||||
export const contextMenuDefaultProps = {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'right-start',
|
||||
},
|
||||
clicking: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
};
|
||||
|
||||
export const contextMenuDefaultEmits = [
|
||||
'hide',
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ContextMenu',
|
||||
directives: {
|
||||
ClickOutside,
|
||||
},
|
||||
emits: contextMenuDefaultEmits,
|
||||
props: contextMenuDefaultProps,
|
||||
setup(props, {emit}) {
|
||||
const onClickOutside = () => {
|
||||
if (props.clicking) return;
|
||||
emit('hide');
|
||||
};
|
||||
|
||||
return {
|
||||
onClickOutside,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<ul class="context-menu-list">
|
||||
<li
|
||||
v-for="(item, $index) in items"
|
||||
:key="$index"
|
||||
class="context-menu-item"
|
||||
@click="onClick(item)"
|
||||
>
|
||||
<span class="prefix">
|
||||
<template v-if="item.icon">
|
||||
<font-awesome-icon v-if="Array.isArray(item.icon)" :icon="item.icon"/>
|
||||
<atom-material-icon v-else-if="typeof item.icon === 'string'" :is-dir="false" :name="item.icon"/>
|
||||
</template>
|
||||
</span>
|
||||
<span class="title">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import AtomMaterialIcon from '@/components/icon/AtomMaterialIcon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ContextMenuList',
|
||||
components: {AtomMaterialIcon},
|
||||
props: {
|
||||
items: {
|
||||
type: [Array, String],
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
const onClick = (item: ContextMenuItem) => {
|
||||
if (!item.action) return;
|
||||
item.action();
|
||||
emit('hide');
|
||||
};
|
||||
|
||||
return {
|
||||
onClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.context-menu-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: auto;
|
||||
|
||||
.context-menu-item {
|
||||
height: $contextMenuItemHeight;
|
||||
max-width: $contextMenuItemMaxWidth;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $primaryPlainColor;
|
||||
}
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
width: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:confirm-loading="confirmLoading"
|
||||
:title="title"
|
||||
:visible="visible"
|
||||
@cancel="onCancel"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<template v-if="content">
|
||||
{{ content }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref} from 'vue';
|
||||
import Dialog from '@/components/dialog/Dialog.vue';
|
||||
import {voidFunc} from '@/utils/func';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ConfirmDialog',
|
||||
components: {Dialog},
|
||||
props: {
|
||||
confirmFunc: {
|
||||
type: Function,
|
||||
default: voidFunc,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'confirm',
|
||||
'cancel',
|
||||
],
|
||||
setup(props: ConfirmDialogProps, {emit}) {
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
|
||||
const onCancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
const onConfirm = async () => {
|
||||
const {confirmFunc} = props;
|
||||
confirmLoading.value = true;
|
||||
await confirmFunc();
|
||||
confirmLoading.value = false;
|
||||
visible.value = false;
|
||||
emit('confirm');
|
||||
};
|
||||
|
||||
return {
|
||||
visible,
|
||||
confirmLoading,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<div class="create-dialog-content-batch">
|
||||
<el-form class="control-panel" inline>
|
||||
<el-form-item>
|
||||
<Button type="primary" @click="onAdd">
|
||||
<font-awesome-icon :icon="['fa', 'plus']"/>
|
||||
Add
|
||||
</Button>
|
||||
</el-form-item>
|
||||
<el-form-item label="Edit All">
|
||||
<Switch v-model="editAll"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<FormTable
|
||||
:data="data"
|
||||
:fields="fields"
|
||||
@add="onAdd"
|
||||
@clone="onClone"
|
||||
@delete="onDelete"
|
||||
@field-change="onFieldChange"
|
||||
@field-register="onFieldRegister"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, inject, PropType, Ref, ref} from 'vue';
|
||||
import FormTable from '@/components/form/FormTable.vue';
|
||||
import {emptyArrayFunc} from '@/utils/func';
|
||||
import Switch from '@/components/switch/Switch.vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CreateDialogContentBatch',
|
||||
components: {
|
||||
Button,
|
||||
Switch,
|
||||
FormTable,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<TableData>,
|
||||
required: true,
|
||||
default: emptyArrayFunc,
|
||||
},
|
||||
fields: {
|
||||
type: Array as PropType<FormTableField[]>,
|
||||
required: true,
|
||||
default: emptyArrayFunc,
|
||||
}
|
||||
},
|
||||
setup(props: CreateDialogContentBatchProps) {
|
||||
const editAll = ref<boolean>(false);
|
||||
|
||||
const actionFunctions = inject('action-functions') as CreateEditDialogActionFunctions;
|
||||
|
||||
const onAdd = (rowIndex: number) => {
|
||||
actionFunctions?.onAdd?.(rowIndex);
|
||||
};
|
||||
|
||||
const onClone = (rowIndex: number) => {
|
||||
actionFunctions?.onClone?.(rowIndex);
|
||||
};
|
||||
|
||||
const onDelete = (rowIndex: number) => {
|
||||
actionFunctions?.onDelete?.(rowIndex);
|
||||
};
|
||||
|
||||
const onFieldChange = (rowIndex: number, prop: string, value: any) => {
|
||||
if (editAll.value) {
|
||||
// edit all rows
|
||||
rowIndex = -1;
|
||||
}
|
||||
|
||||
actionFunctions?.onFieldChange?.(rowIndex, prop, value);
|
||||
};
|
||||
|
||||
const onFieldRegister = (rowIndex: number, prop: string, formRef: Ref) => {
|
||||
actionFunctions?.onFieldRegister(rowIndex, prop, formRef);
|
||||
};
|
||||
|
||||
return {
|
||||
editAll,
|
||||
onAdd,
|
||||
onClone,
|
||||
onDelete,
|
||||
onFieldChange,
|
||||
onFieldRegister,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.create-dialog-content-batch {
|
||||
.control-panel {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.el-form-item {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,151 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:title="computedTitle"
|
||||
:visible="visible"
|
||||
:width="width"
|
||||
:confirm-loading="confirmLoading"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
@close="onClose"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<el-tabs
|
||||
v-model="internalTabName"
|
||||
:class="[type, visible ? 'visible' : '']"
|
||||
class="create-edit-dialog-tabs"
|
||||
@tab-click="onTabChange"
|
||||
>
|
||||
<el-tab-pane label="Single" name="single">
|
||||
<slot/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="!noBatch" label="Batch" name="batch">
|
||||
<CreateDialogContentBatch
|
||||
:data="batchFormData"
|
||||
:fields="batchFormFields"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType, provide, ref, SetupContext, watch} from 'vue';
|
||||
import CreateDialogContentBatch from '@/components/dialog/CreateDialogContentBatch.vue';
|
||||
import Dialog from '@/components/dialog/Dialog.vue';
|
||||
import {emptyArrayFunc, emptyObjectFunc} from '@/utils/func';
|
||||
import {Pane} from 'element-plus/lib/el-tabs/src/tabs.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CreateEditDialog',
|
||||
components: {
|
||||
Dialog,
|
||||
CreateDialogContentBatch,
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<CreateEditDialogType>,
|
||||
default: 'create',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '80vw',
|
||||
},
|
||||
batchFormData: {
|
||||
type: Array as PropType<TableData>,
|
||||
default: emptyArrayFunc,
|
||||
},
|
||||
batchFormFields: {
|
||||
type: Array as PropType<FormTableField[]>,
|
||||
default: emptyArrayFunc,
|
||||
},
|
||||
confirmDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actionFunctions: {
|
||||
type: Object as PropType<CreateEditDialogActionFunctions>,
|
||||
default: emptyObjectFunc,
|
||||
},
|
||||
tabName: {
|
||||
type: String as PropType<CreateEditTabName>,
|
||||
default: 'single',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
noBatch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
formRules: {
|
||||
type: Array as PropType<FormRuleItem[]>,
|
||||
default: emptyArrayFunc,
|
||||
},
|
||||
},
|
||||
setup(props: CreateEditDialogProps, ctx: SetupContext) {
|
||||
const computedTitle = computed<string>(() => {
|
||||
const {visible, type, title} = props;
|
||||
if (title) return title;
|
||||
if (!visible) return '';
|
||||
switch (type) {
|
||||
case 'create':
|
||||
return 'Create';
|
||||
case 'edit':
|
||||
return 'Edit';
|
||||
default:
|
||||
return 'Dialog';
|
||||
}
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
const {actionFunctions} = props;
|
||||
actionFunctions?.onClose?.();
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
const {actionFunctions} = props;
|
||||
actionFunctions?.onConfirm?.();
|
||||
};
|
||||
|
||||
const internalTabName = ref<CreateEditTabName>('single');
|
||||
const onTabChange = (tab: Pane) => {
|
||||
const tabName = tab.paneName as unknown as CreateEditTabName;
|
||||
const {actionFunctions} = props;
|
||||
actionFunctions?.onTabChange?.(tabName);
|
||||
};
|
||||
watch(() => props.tabName, () => {
|
||||
internalTabName.value = props.tabName as CreateEditTabName;
|
||||
});
|
||||
|
||||
provide<CreateEditDialogActionFunctions | undefined>('action-functions', props.actionFunctions);
|
||||
provide<FormRuleItem[] | undefined>('form-rules', props.formRules);
|
||||
|
||||
return {
|
||||
computedTitle,
|
||||
onClose,
|
||||
onConfirm,
|
||||
internalTabName,
|
||||
onTabChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.create-edit-dialog-tabs {
|
||||
&.edit,
|
||||
&:not(.visible) {
|
||||
.el-tabs__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:modal-class="modalClass"
|
||||
:before-close="onClose"
|
||||
:model-value="visible"
|
||||
:title="title"
|
||||
:top="top"
|
||||
:width="width"
|
||||
:z-index="zIndex"
|
||||
>
|
||||
<slot/>
|
||||
<template #footer>
|
||||
<slot name="prefix"/>
|
||||
<Button plain size="mini" type="info" @click="onClose">Cancel</Button>
|
||||
<Button
|
||||
:disabled="confirmDisabled"
|
||||
:loading="confirmLoading"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="onConfirm"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<slot name="suffix"/>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Dialog',
|
||||
components: {Button},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
modalClass: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
top: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
confirmDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'close',
|
||||
'confirm',
|
||||
],
|
||||
setup(props: DialogProps, {emit}) {
|
||||
const onClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
emit('confirm');
|
||||
};
|
||||
|
||||
return {
|
||||
onClose,
|
||||
onConfirm,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="classes"
|
||||
:draggable="true"
|
||||
class="draggable-item"
|
||||
@dragstart="$emit('d-start', item)"
|
||||
@dragend="$emit('d-end', item)"
|
||||
@dragenter="$emit('d-enter', item)"
|
||||
@dragleave="$emit('d-leave', item)"
|
||||
>
|
||||
<DraggableItemContent :item="item"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import DraggableItemContent from '@/components/drag/DraggableItemContent.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DraggableItem',
|
||||
components: {DraggableItemContent},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
dragging: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'd-start',
|
||||
'd-end',
|
||||
'd-enter',
|
||||
'd-leave',
|
||||
],
|
||||
setup(props) {
|
||||
const dragging = computed(() => {
|
||||
const {item} = props as DraggableItemProps;
|
||||
return item.dragging;
|
||||
});
|
||||
|
||||
const classes = computed(() => {
|
||||
const cls = [];
|
||||
if (dragging.value) cls.push('dragging');
|
||||
return cls;
|
||||
});
|
||||
|
||||
return {
|
||||
classes,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.draggable-item {
|
||||
position: relative;
|
||||
|
||||
&.dragging {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&.dragging * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {defineComponent, inject} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DraggableItemContent',
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => {
|
||||
const {item} = props;
|
||||
const content = inject<DraggableListContext>('list');
|
||||
if (!content || !content.ctx || !content.ctx.slots || !content.ctx.slots.default) return '';
|
||||
return content.ctx.slots.default({item});
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,111 +0,0 @@
|
||||
<template>
|
||||
<div class="draggable-list">
|
||||
<DraggableItem
|
||||
v-for="(item, $index) in orderedItems"
|
||||
:key="item[itemKey] === undefined ? $index : item[itemKey]"
|
||||
:item="item"
|
||||
@d-end="onTabDragEnd"
|
||||
@d-enter="onTabDragEnter"
|
||||
@d-leave="onTabDragLeave"
|
||||
@d-start="onTabDragStart"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, provide, reactive, ref} from 'vue';
|
||||
import DraggableItem from '@/components/drag/DraggableItem.vue';
|
||||
import {plainClone} from '@/utils/object';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DraggableList',
|
||||
components: {DraggableItem},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
itemKey: {
|
||||
type: String,
|
||||
default: 'key',
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'd-end',
|
||||
],
|
||||
setup(props, ctx) {
|
||||
const {emit} = ctx;
|
||||
const internalItems = reactive<DraggableListInternalItems>({});
|
||||
const isDragging = ref(false);
|
||||
|
||||
const orderedItems = computed(() => {
|
||||
const {items, itemKey} = props as DraggableListProps;
|
||||
const {draggingItem, targetItem} = internalItems;
|
||||
if (!draggingItem || !targetItem) return items;
|
||||
const orderedItems = plainClone(items) as DraggableItemData[];
|
||||
const draggingIdx = orderedItems.map(t => t[itemKey]).indexOf(draggingItem[itemKey]);
|
||||
const targetIdx = orderedItems.map(t => t[itemKey]).indexOf(targetItem[itemKey]);
|
||||
if (draggingIdx === -1 || targetIdx === -1) return items;
|
||||
orderedItems.splice(draggingIdx, 1);
|
||||
orderedItems.splice(targetIdx, 0, plainClone(draggingItem));
|
||||
return orderedItems;
|
||||
});
|
||||
|
||||
const onTabDragStart = (item: DraggableItemData) => {
|
||||
internalItems.draggingItem = plainClone(item) as DraggableItemData;
|
||||
internalItems.draggingItem.dragging = true;
|
||||
isDragging.value = true;
|
||||
};
|
||||
|
||||
const onTabDragEnd = () => {
|
||||
const items = orderedItems.value.map(d => {
|
||||
d.dragging = false;
|
||||
return d;
|
||||
});
|
||||
isDragging.value = false;
|
||||
internalItems.draggingItem = undefined;
|
||||
internalItems.targetItem = undefined;
|
||||
emit('d-end', items);
|
||||
};
|
||||
|
||||
const onTabDragEnter = (item: DraggableItemData) => {
|
||||
const {itemKey} = props as DraggableListProps;
|
||||
const {draggingItem} = internalItems;
|
||||
if (!draggingItem || (draggingItem ? draggingItem[itemKey] : undefined) === item[itemKey]) return;
|
||||
const _item = plainClone(item) as DraggableItemData;
|
||||
_item.dragging = true;
|
||||
internalItems.targetItem = _item;
|
||||
};
|
||||
|
||||
const onTabDragLeave = (item: DraggableItemData) => {
|
||||
const {itemKey} = props as DraggableListProps;
|
||||
const {draggingItem, targetItem} = internalItems;
|
||||
if (!!targetItem || !draggingItem || (draggingItem ? draggingItem[itemKey] : undefined) === item[itemKey]) return;
|
||||
internalItems.targetItem = undefined;
|
||||
};
|
||||
|
||||
provide('list', {
|
||||
ctx,
|
||||
props,
|
||||
} as DraggableListContext);
|
||||
|
||||
return {
|
||||
orderedItems,
|
||||
onTabDragStart,
|
||||
onTabDragEnd,
|
||||
onTabDragEnter,
|
||||
onTabDragLeave,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.draggable-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
3
frontend/src/components/element/index.d.ts
vendored
3
frontend/src/components/element/index.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
type BasicType = 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
type BasicEffect = 'dark' | 'light' | 'plain';
|
||||
type BasicSize = 'mini' | 'small' | 'medium' | 'large';
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div class="empty">
|
||||
<ImgEmpty/>
|
||||
<div class="description">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import ImgEmpty from '@/components/empty/ImgEmpty.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Empty',
|
||||
components: {ImgEmpty},
|
||||
props: {
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'No Data Available'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty {
|
||||
max-height: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.description {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
class="img-empty"
|
||||
version="1.1"
|
||||
viewBox="0 0 79 86"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<title>Group 2</title>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="linearGradient-1"
|
||||
x1="38.8503086%"
|
||||
x2="61.1496914%"
|
||||
y1="0%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stop-color="#FCFCFD"/>
|
||||
<stop offset="100%" stop-color="#EEEFF3"/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient-2"
|
||||
x1="0%"
|
||||
x2="100%"
|
||||
y1="9.5%"
|
||||
y2="90.5%"
|
||||
>
|
||||
<stop offset="0%" stop-color="#FCFCFD"/>
|
||||
<stop offset="100%" stop-color="#E9EBEF"/>
|
||||
</linearGradient>
|
||||
<rect
|
||||
id="path-3"
|
||||
height="36"
|
||||
width="17"
|
||||
x="0"
|
||||
y="0"
|
||||
/>
|
||||
</defs>
|
||||
<g
|
||||
id="Illustrations"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
>
|
||||
<g id="B-type" transform="translate(-1268.000000, -535.000000)">
|
||||
<g id="Group-2" transform="translate(1268.000000, 535.000000)">
|
||||
<path
|
||||
id="Oval-Copy-2"
|
||||
d="M39.5,86 C61.3152476,86 79,83.9106622 79,81.3333333 C79,78.7560045 57.3152476,78 35.5,78 C13.6847524,78 0,78.7560045 0,81.3333333 C0,83.9106622 17.6847524,86 39.5,86 Z"
|
||||
fill="#F7F8FC"
|
||||
/>
|
||||
<polygon
|
||||
id="Rectangle-Copy-14"
|
||||
fill="#E5E7E9"
|
||||
points="13 58 53 58 42 45 2 45"
|
||||
transform="translate(27.500000, 51.500000) scale(1, -1) translate(-27.500000, -51.500000) "
|
||||
/>
|
||||
<g id="Group-Copy"
|
||||
transform="translate(34.500000, 31.500000) scale(-1, 1) rotate(-25.000000) translate(-34.500000, -31.500000) translate(7.000000, 10.000000)">
|
||||
<polygon
|
||||
id="Rectangle-Copy-10"
|
||||
fill="#E5E7E9"
|
||||
points="2.84078316e-14 3 18 3 23 7 5 7"
|
||||
transform="translate(11.500000, 5.000000) scale(1, -1) translate(-11.500000, -5.000000) "
|
||||
/>
|
||||
<polygon id="Rectangle-Copy-11" fill="#EDEEF2" points="-3.69149156e-15 7 38 7 38 43 -3.69149156e-15 43"/>
|
||||
<rect
|
||||
id="Rectangle-Copy-12"
|
||||
fill="url(#linearGradient-1)"
|
||||
height="36"
|
||||
transform="translate(46.500000, 25.000000) scale(-1, 1) translate(-46.500000, -25.000000) "
|
||||
width="17"
|
||||
x="38"
|
||||
y="7"
|
||||
/>
|
||||
<polygon
|
||||
id="Rectangle-Copy-13"
|
||||
fill="#F8F9FB"
|
||||
points="24 7 41 7 55 -3.63806207e-12 38 -3.63806207e-12"
|
||||
transform="translate(39.500000, 3.500000) scale(-1, 1) translate(-39.500000, -3.500000) "
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
id="Rectangle-Copy-15"
|
||||
fill="url(#linearGradient-2)"
|
||||
height="36"
|
||||
width="40"
|
||||
x="13"
|
||||
y="45"
|
||||
/>
|
||||
<g id="Rectangle-Copy-17" transform="translate(53.000000, 45.000000)">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"/>
|
||||
</mask>
|
||||
<use
|
||||
id="Mask"
|
||||
fill="#E0E3E9"
|
||||
transform="translate(8.500000, 18.000000) scale(-1, 1) translate(-8.500000, -18.000000) "
|
||||
xlink:href="#path-3"
|
||||
/>
|
||||
<polygon
|
||||
id="Rectangle-Copy"
|
||||
fill="#D5D7DE"
|
||||
mask="url(#mask-4)"
|
||||
points="7 0 24 0 20 18 -1.70530257e-13 16"
|
||||
transform="translate(12.000000, 9.000000) scale(-1, 1) translate(-12.000000, -9.000000) "
|
||||
/>
|
||||
</g>
|
||||
<polygon
|
||||
id="Rectangle-Copy-18"
|
||||
fill="#F8F9FB"
|
||||
points="62 45 79 45 70 58 53 58"
|
||||
transform="translate(66.000000, 51.500000) scale(-1, 1) translate(-66.000000, -51.500000) "
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ImgEmpty',
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.img-empty {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,823 +0,0 @@
|
||||
<template>
|
||||
<div ref="fileEditor" class="file-editor">
|
||||
<div :class="navMenuCollapsed ? 'collapsed' : ''" class="nav-menu">
|
||||
<div
|
||||
:style="{
|
||||
backgroundColor: style.backgroundColorGutters,
|
||||
color: style.color,
|
||||
}"
|
||||
class="nav-menu-top-bar"
|
||||
>
|
||||
<div class="left">
|
||||
<el-input
|
||||
v-model="fileSearchString"
|
||||
:style="{
|
||||
color: style.color,
|
||||
}"
|
||||
class="search"
|
||||
clearable
|
||||
placeholder="Search files..."
|
||||
prefix-icon="fa fa-search"
|
||||
size="mini"
|
||||
/>
|
||||
</div>
|
||||
<div class="right">
|
||||
<el-tooltip content="Settings">
|
||||
<span class="action-icon" @click="showSettings = true">
|
||||
<div class="background"/>
|
||||
<font-awesome-icon :icon="['fa', 'cog']"/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="Hide files">
|
||||
<span class="action-icon" @click="onToggleNavMenu">
|
||||
<div class="background"/>
|
||||
<font-awesome-icon :icon="['fa', 'minus']"/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<FileEditorNavMenu
|
||||
:active-item="activeFileItem"
|
||||
:default-expand-all="!!fileSearchString"
|
||||
:items="files"
|
||||
:style="style"
|
||||
@node-click="onNavItemClick"
|
||||
@node-db-click="onNavItemDbClick"
|
||||
@node-drop="onNavItemDrop"
|
||||
@ctx-menu-new-file="onContextMenuNewFile"
|
||||
@ctx-menu-new-directory="onContextMenuNewDirectory"
|
||||
@ctx-menu-rename="onContextMenuRename"
|
||||
@ctx-menu-clone="onContextMenuClone"
|
||||
@ctx-menu-delete="onContextMenuDelete"
|
||||
@drop-files="onDropFiles"
|
||||
/>
|
||||
</div>
|
||||
<div class="file-editor-content">
|
||||
<FileEditorNavTabs
|
||||
ref="navTabs"
|
||||
:active-tab="activeFileItem"
|
||||
:tabs="tabs"
|
||||
:style="style"
|
||||
@tab-click="onTabClick"
|
||||
@tab-close="onTabClose"
|
||||
@tab-close-others="onTabCloseOthers"
|
||||
@tab-close-all="onTabCloseAll"
|
||||
@tab-dragend="onTabDragEnd"
|
||||
>
|
||||
<template v-if="navMenuCollapsed" #prefix>
|
||||
<el-tooltip content="Show files">
|
||||
<span class="action-icon expand-files" @click="onToggleNavMenu">
|
||||
<div class="background"/>
|
||||
<font-awesome-icon :icon="['fa', 'bars']"/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</FileEditorNavTabs>
|
||||
<div
|
||||
ref="codeMirrorEditor"
|
||||
:class="showCodeMirrorEditor ? '' : 'hidden'"
|
||||
:style="{
|
||||
scrollbar: style.backgroundColorGutters,
|
||||
}"
|
||||
class="code-mirror-editor"
|
||||
/>
|
||||
<div
|
||||
v-show="!showCodeMirrorEditor"
|
||||
:style="{
|
||||
backgroundColor: style.backgroundColor,
|
||||
color: style.color,
|
||||
}"
|
||||
class="empty-content"
|
||||
>
|
||||
You can edit or view a file by double-clicking one of the files on the left.
|
||||
</div>
|
||||
<template v-if="navTabs && navTabs.showMoreVisible">
|
||||
<FileEditorNavTabsShowMoreContextMenu
|
||||
:tabs="tabs"
|
||||
:visible="showMoreContextMenuVisible"
|
||||
@hide="onShowMoreHide"
|
||||
@tab-click="onClickShowMoreContextMenuItem"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
background: style.backgroundColor,
|
||||
color: style.color,
|
||||
}"
|
||||
class="nav-tabs-suffix"
|
||||
>
|
||||
<el-tooltip content="Show more">
|
||||
<span class="action-icon" @click.prevent="onShowMoreShow">
|
||||
<div class="background"/>
|
||||
<font-awesome-icon :icon="['fa', 'angle-down']"/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</FileEditorNavTabsShowMoreContextMenu>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="codeMirrorTemplate" class="code-mirror-template"/>
|
||||
<div ref="styleRef" v-html="extraStyle"/>
|
||||
<FileEditorSettingsDialog/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, onUnmounted, PropType, ref, watch} from 'vue';
|
||||
import CodeMirror, {Editor, EditorConfiguration, KeyMap} from 'codemirror';
|
||||
import {MimeType} from 'codemirror/mode/meta';
|
||||
import {useStore} from 'vuex';
|
||||
import {getCodemirrorEditor, getCodeMirrorTemplate, initTheme} from '@/utils/codemirror';
|
||||
import variables from '@/styles/variables.scss';
|
||||
import {FILE_ROOT} from '@/constants/file';
|
||||
|
||||
// codemirror css
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
|
||||
// codemirror mode
|
||||
import 'codemirror/mode/meta';
|
||||
|
||||
// codemirror utils
|
||||
import '@/utils/codemirror';
|
||||
|
||||
// components
|
||||
import FileEditorNavMenu from '@/components/file/FileEditorNavMenu.vue';
|
||||
import FileEditorNavTabs from '@/components/file/FileEditorNavTabs.vue';
|
||||
import FileEditorSettingsDialog from '@/components/file/FileEditorSettingsDialog.vue';
|
||||
import FileEditorNavTabsShowMoreContextMenu from '@/components/file/FileEditorNavTabsShowMoreContextMenu.vue';
|
||||
|
||||
// codemirror mode import cache
|
||||
const codeMirrorModeCache = new Set<string>();
|
||||
|
||||
// codemirror tab content cache
|
||||
const codeMirrorTabContentCache = new Map<string, string>();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditor',
|
||||
components: {
|
||||
FileEditorSettingsDialog,
|
||||
FileEditorNavTabs,
|
||||
FileEditorNavMenu,
|
||||
FileEditorNavTabsShowMoreContextMenu,
|
||||
},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
activeNavItem: {
|
||||
type: Object as PropType<FileNavItem>,
|
||||
required: false,
|
||||
},
|
||||
navItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'content-change',
|
||||
'tab-click',
|
||||
'node-click',
|
||||
'node-db-click',
|
||||
'node-drop',
|
||||
'save-file',
|
||||
'ctx-menu-new-file',
|
||||
'ctx-menu-new-directory',
|
||||
'ctx-menu-rename',
|
||||
'ctx-menu-clone',
|
||||
'ctx-menu-delete',
|
||||
'drop-files',
|
||||
],
|
||||
setup(props, {emit}) {
|
||||
const ns = 'spider';
|
||||
const store = useStore();
|
||||
const {file} = store.state as RootStoreState;
|
||||
|
||||
const fileEditor = ref<HTMLDivElement>();
|
||||
|
||||
const codeMirrorEditor = ref<HTMLDivElement>();
|
||||
|
||||
const tabs = ref<FileNavItem[]>([]);
|
||||
|
||||
const activeFileItem = computed<FileNavItem | undefined>(() => props.activeNavItem);
|
||||
|
||||
const style = ref<FileEditorStyle>({});
|
||||
|
||||
const fileSearchString = ref<string>('');
|
||||
|
||||
const navMenuCollapsed = ref<boolean>(false);
|
||||
|
||||
const showSettings = ref<boolean>(false);
|
||||
|
||||
const styleRef = ref<HTMLDivElement>();
|
||||
|
||||
let editor: Editor | null = null;
|
||||
|
||||
let codeMirrorTemplateEditor: Editor | null = null;
|
||||
|
||||
const codeMirrorTemplate = ref<HTMLDivElement>();
|
||||
|
||||
let codeMirrorEditorSearchLabel: HTMLSpanElement | undefined;
|
||||
|
||||
let codeMirrorEditorSearchInput: HTMLInputElement | undefined;
|
||||
|
||||
const navTabs = ref<typeof FileEditorNavTabs>();
|
||||
|
||||
const showMoreContextMenuVisible = ref<boolean>(false);
|
||||
|
||||
const showCodeMirrorEditor = computed<boolean>(() => {
|
||||
return !!activeFileItem.value;
|
||||
});
|
||||
|
||||
const language = computed<MimeType | undefined>(() => {
|
||||
const fileName = activeFileItem.value?.name;
|
||||
if (!fileName) return;
|
||||
return CodeMirror.findModeByFileName(fileName);
|
||||
});
|
||||
|
||||
const languageMime = computed<string | undefined>(() => language.value?.mime);
|
||||
|
||||
const options = computed<FileEditorConfiguration>(() => {
|
||||
const {editorOptions} = file as FileStoreState;
|
||||
return {
|
||||
mode: languageMime.value || 'text',
|
||||
...editorOptions,
|
||||
};
|
||||
});
|
||||
|
||||
const content = computed<string>(() => {
|
||||
const {content} = props as FileEditorProps;
|
||||
return content || '';
|
||||
});
|
||||
|
||||
const extraStyle = computed<string>(() => {
|
||||
return `<style>
|
||||
.file-editor .file-editor-nav-menu::-webkit-scrollbar {
|
||||
background-color: ${style.value.backgroundColor};
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.file-editor .file-editor-nav-menu::-webkit-scrollbar-thumb {
|
||||
background-color: ${variables.primaryColor};
|
||||
border-radius: 4px;
|
||||
}
|
||||
.file-editor .file-editor-content .code-mirror-editor .CodeMirror-vscrollbar::-webkit-scrollbar {
|
||||
background-color: ${style.value.backgroundColor};
|
||||
width: 8px;
|
||||
}
|
||||
.file-editor .file-editor-content .code-mirror-editor .CodeMirror-hscrollbar::-webkit-scrollbar {
|
||||
background-color: ${style.value.backgroundColor};
|
||||
height: 8px;
|
||||
}
|
||||
.file-editor .file-editor-content .code-mirror-editor .CodeMirror-vscrollbar::-webkit-scrollbar-thumb,
|
||||
.file-editor .file-editor-content .code-mirror-editor .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: ${variables.primaryColor};
|
||||
border-radius: 4px;
|
||||
}
|
||||
.file-editor .file-editor-nav-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>`;
|
||||
});
|
||||
|
||||
const codeMirrorTemplateContent = computed<string>(() => {
|
||||
return getCodeMirrorTemplate();
|
||||
});
|
||||
|
||||
const updateEditorOptions = () => {
|
||||
for (const k in options.value) {
|
||||
const key = k as keyof EditorConfiguration;
|
||||
const value = options.value[key];
|
||||
editor?.setOption(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
const updateEditorContent = () => {
|
||||
editor?.setValue(content.value || '');
|
||||
};
|
||||
|
||||
const updateStyle = () => {
|
||||
// codemirror style: background color / color / height
|
||||
const el = codeMirrorEditor.value as HTMLElement;
|
||||
const cm = el.querySelector('.CodeMirror');
|
||||
if (!cm) return;
|
||||
const computedStyle = window.getComputedStyle(cm);
|
||||
style.value = {
|
||||
backgroundColor: computedStyle.backgroundColor,
|
||||
color: computedStyle.color,
|
||||
height: computedStyle.height,
|
||||
};
|
||||
|
||||
// gutter
|
||||
const cmGutters = el.querySelector('.CodeMirror-gutters');
|
||||
if (!cmGutters) return;
|
||||
const computedStyleGutters = window.getComputedStyle(cmGutters);
|
||||
style.value.backgroundColorGutters = computedStyleGutters.backgroundColor;
|
||||
};
|
||||
|
||||
const updateTheme = async () => {
|
||||
await initTheme(options.value.theme);
|
||||
};
|
||||
|
||||
const updateMode = async () => {
|
||||
const mode = language.value?.mode;
|
||||
if (!mode || codeMirrorModeCache.has(mode)) return;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
await import(`codemirror/mode/${mode}/${mode}.js`);
|
||||
codeMirrorModeCache.add(mode);
|
||||
};
|
||||
|
||||
const updateSearchInput = () => {
|
||||
codeMirrorEditorSearchLabel = codeMirrorEditor.value?.querySelector<HTMLSpanElement>('.CodeMirror-search-label') || undefined;
|
||||
codeMirrorEditorSearchInput = codeMirrorEditor.value?.querySelector<HTMLInputElement>('.CodeMirror-search-field') || undefined;
|
||||
if (!codeMirrorEditorSearchInput) return;
|
||||
codeMirrorEditorSearchInput.onblur = () => {
|
||||
if (codeMirrorEditorSearchLabel?.textContent?.includes('Search')) {
|
||||
setTimeout(() => {
|
||||
codeMirrorEditorSearchInput?.parentElement?.remove();
|
||||
editor?.focus();
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getContentCache = (tab: FileNavItem) => {
|
||||
if (!tab.path) return;
|
||||
const key = tab.path;
|
||||
const content = codeMirrorTabContentCache.get(key);
|
||||
emit('content-change', content as string);
|
||||
setTimeout(updateEditorContent, 0);
|
||||
};
|
||||
|
||||
const updateContentCache = (tab: FileNavItem, content: string) => {
|
||||
if (!tab.path) return;
|
||||
const key = tab.path as string;
|
||||
codeMirrorTabContentCache.set(key, content as string);
|
||||
};
|
||||
|
||||
const deleteContentCache = (tab: FileNavItem) => {
|
||||
if (!tab.path) return;
|
||||
const key = tab.path;
|
||||
codeMirrorTabContentCache.delete(key);
|
||||
};
|
||||
|
||||
const deleteOtherContentCache = (tab: FileNavItem) => {
|
||||
if (!tab.path) return;
|
||||
const key = tab.path;
|
||||
const content = codeMirrorTabContentCache.get(key);
|
||||
codeMirrorTabContentCache.clear();
|
||||
codeMirrorTabContentCache.set(key, content as string);
|
||||
};
|
||||
|
||||
const clearContentCache = () => {
|
||||
codeMirrorTabContentCache.clear();
|
||||
};
|
||||
|
||||
const getFilteredFiles = (items: FileNavItem[]): FileNavItem[] => {
|
||||
return items
|
||||
.filter(d => {
|
||||
if (!d.is_dir) {
|
||||
return d.name?.toLowerCase().includes(fileSearchString.value.toLowerCase());
|
||||
}
|
||||
if (d.children) {
|
||||
const children = getFilteredFiles(d.children);
|
||||
if (children.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(d => {
|
||||
if (!d.is_dir) return d;
|
||||
d.children = getFilteredFiles(d.children || []);
|
||||
return d;
|
||||
});
|
||||
};
|
||||
|
||||
const files = computed<FileNavItem[]>(() => {
|
||||
const {navItems} = props as FileEditorProps;
|
||||
const root: FileNavItem = {
|
||||
path: FILE_ROOT,
|
||||
name: FILE_ROOT,
|
||||
is_dir: true,
|
||||
children: fileSearchString.value ? getFilteredFiles(navItems) : navItems,
|
||||
};
|
||||
return [root];
|
||||
});
|
||||
|
||||
const updateTabs = (item?: FileNavItem) => {
|
||||
// add tab
|
||||
if (item && !tabs.value.find(t => t.path === item.path)) {
|
||||
if (tabs.value.length === 0) {
|
||||
store.commit(`${ns}/setActiveFileNavItem`, item);
|
||||
editor?.focus();
|
||||
}
|
||||
tabs.value.push(item);
|
||||
getContentCache(item);
|
||||
}
|
||||
};
|
||||
|
||||
const onNavItemClick = (item: FileNavItem) => {
|
||||
emit('node-click', item);
|
||||
};
|
||||
|
||||
const onNavItemDbClick = (item: FileNavItem) => {
|
||||
store.commit(`${ns}/setActiveFileNavItem`, item);
|
||||
emit('node-db-click', item);
|
||||
|
||||
// update tabs
|
||||
updateTabs(item);
|
||||
};
|
||||
|
||||
const onNavItemDrop = (draggingItem: FileNavItem, dropItem: FileNavItem) => {
|
||||
emit('node-drop', draggingItem, dropItem);
|
||||
};
|
||||
|
||||
const onContextMenuNewFile = (item: FileNavItem, name: string) => {
|
||||
emit('ctx-menu-new-file', item, name);
|
||||
};
|
||||
|
||||
const onContextMenuNewDirectory = (item: FileNavItem, name: string) => {
|
||||
emit('ctx-menu-new-directory', item, name);
|
||||
};
|
||||
|
||||
const onContextMenuRename = (item: FileNavItem, name: string) => {
|
||||
emit('ctx-menu-rename', item, name);
|
||||
};
|
||||
|
||||
const onContextMenuClone = (item: FileNavItem, name: string) => {
|
||||
emit('ctx-menu-clone', item, name);
|
||||
};
|
||||
|
||||
const onContextMenuDelete = (item: FileNavItem) => {
|
||||
emit('ctx-menu-delete', item);
|
||||
};
|
||||
|
||||
const onContentChange = (cm: Editor) => {
|
||||
const content = cm.getValue();
|
||||
if (!activeFileItem.value) return;
|
||||
emit('content-change', content);
|
||||
|
||||
// update in cache
|
||||
updateContentCache(activeFileItem.value, content);
|
||||
};
|
||||
|
||||
const onTabClick = (tab: FileNavItem) => {
|
||||
store.commit(`${ns}/setActiveFileNavItem`, tab);
|
||||
emit('tab-click', tab);
|
||||
|
||||
// get from cache and update content
|
||||
getContentCache(tab);
|
||||
};
|
||||
|
||||
const closeTab = (tab: FileNavItem) => {
|
||||
const idx = tabs.value.findIndex(t => t.path === tab.path);
|
||||
if (idx !== -1) {
|
||||
tabs.value.splice(idx, 1);
|
||||
}
|
||||
if (activeFileItem.value) {
|
||||
if (activeFileItem.value.path === tab.path) {
|
||||
if (idx === 0) {
|
||||
store.commit(`${ns}/setActiveFileNavItem`, tabs.value[0]);
|
||||
} else {
|
||||
store.commit(`${ns}/setActiveFileNavItem`, tabs.value[idx - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// get from cache
|
||||
setTimeout(() => {
|
||||
if (activeFileItem.value) {
|
||||
getContentCache(activeFileItem.value);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// delete in cache
|
||||
deleteContentCache(tab);
|
||||
};
|
||||
|
||||
const onTabClose = (tab: FileNavItem) => {
|
||||
closeTab(tab);
|
||||
};
|
||||
|
||||
const onTabCloseOthers = (tab: FileNavItem) => {
|
||||
tabs.value = [tab];
|
||||
store.commit(`${ns}/setActiveFileNavItem`, tab);
|
||||
|
||||
// clear cache and update current tab content
|
||||
deleteOtherContentCache(tab);
|
||||
};
|
||||
|
||||
const onTabCloseAll = () => {
|
||||
tabs.value = [];
|
||||
store.commit(`${ns}/resetActiveFileNavItem`);
|
||||
|
||||
// clear cache
|
||||
clearContentCache();
|
||||
};
|
||||
|
||||
const onTabDragEnd = (newTabs: FileNavItem[]) => {
|
||||
tabs.value = newTabs;
|
||||
};
|
||||
|
||||
const onShowMoreShow = () => {
|
||||
showMoreContextMenuVisible.value = true;
|
||||
};
|
||||
|
||||
const onShowMoreHide = () => {
|
||||
showMoreContextMenuVisible.value = false;
|
||||
};
|
||||
|
||||
const onClickShowMoreContextMenuItem = (tab: FileNavItem) => {
|
||||
store.commit(`${ns}/setActiveFileNavItem`, tab);
|
||||
emit('tab-click', tab);
|
||||
};
|
||||
|
||||
const keyMapSave = () => {
|
||||
if (!activeFileItem.value) return;
|
||||
emit('save-file', activeFileItem.value);
|
||||
};
|
||||
|
||||
const keyMapClose = () => {
|
||||
if (!activeFileItem.value) return;
|
||||
closeTab(activeFileItem.value);
|
||||
};
|
||||
|
||||
const addSaveKeyMap = (cm: Editor) => {
|
||||
const map = {
|
||||
'Cmd-S': keyMapSave,
|
||||
'Ctrl-S': keyMapSave,
|
||||
// 'Cmd-W': keyMapClose,
|
||||
'Ctrl-W': keyMapClose,
|
||||
} as KeyMap;
|
||||
cm.addKeyMap(map);
|
||||
};
|
||||
|
||||
const onToggleNavMenu = () => {
|
||||
navMenuCollapsed.value = !navMenuCollapsed.value;
|
||||
};
|
||||
|
||||
const listenToKeyboardEvents = () => {
|
||||
editor?.on('blur', () => {
|
||||
updateSearchInput();
|
||||
});
|
||||
};
|
||||
|
||||
const unlistenToKeyboardEvents = () => {
|
||||
document.onkeydown = null;
|
||||
};
|
||||
|
||||
watch(options, async () => {
|
||||
await Promise.all([
|
||||
updateMode(),
|
||||
updateTheme(),
|
||||
]);
|
||||
updateEditorOptions();
|
||||
updateStyle();
|
||||
});
|
||||
|
||||
const onDropFiles = (files: InputFile[]) => {
|
||||
emit('drop-files', files);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// init codemirror editor
|
||||
const el = codeMirrorEditor.value as HTMLElement;
|
||||
editor = getCodemirrorEditor(el, options.value);
|
||||
|
||||
// add save key map
|
||||
addSaveKeyMap(editor);
|
||||
|
||||
// on editor change
|
||||
editor.on('change', onContentChange);
|
||||
|
||||
// update editor options
|
||||
updateEditorOptions();
|
||||
|
||||
// update editor content
|
||||
updateEditorContent();
|
||||
|
||||
// update editor theme
|
||||
await updateTheme();
|
||||
|
||||
// update styles
|
||||
updateStyle();
|
||||
|
||||
// listen to keyboard events key
|
||||
listenToKeyboardEvents();
|
||||
|
||||
// init codemirror template
|
||||
const elTemplate = codeMirrorTemplate.value as HTMLElement;
|
||||
codeMirrorTemplateEditor = getCodemirrorEditor(elTemplate, options.value);
|
||||
codeMirrorTemplateEditor.setValue(codeMirrorTemplateContent.value);
|
||||
codeMirrorTemplateEditor.setOption('mode', 'text/x-python');
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// turnoff listening to keyboard events
|
||||
unlistenToKeyboardEvents();
|
||||
});
|
||||
|
||||
return {
|
||||
fileEditor,
|
||||
codeMirrorEditor,
|
||||
tabs,
|
||||
activeFileItem,
|
||||
fileSearchString,
|
||||
navMenuCollapsed,
|
||||
styleRef,
|
||||
codeMirrorTemplate,
|
||||
showSettings,
|
||||
showCodeMirrorEditor,
|
||||
navTabs,
|
||||
showMoreContextMenuVisible,
|
||||
languageMime,
|
||||
options,
|
||||
style,
|
||||
files,
|
||||
extraStyle,
|
||||
variables,
|
||||
onNavItemClick,
|
||||
onNavItemDbClick,
|
||||
onNavItemDrop,
|
||||
onContextMenuNewFile,
|
||||
onContextMenuNewDirectory,
|
||||
onContextMenuRename,
|
||||
onContextMenuClone,
|
||||
onContextMenuDelete,
|
||||
onContentChange,
|
||||
onTabClick,
|
||||
onTabClose,
|
||||
onTabCloseOthers,
|
||||
onTabCloseAll,
|
||||
onTabDragEnd,
|
||||
onToggleNavMenu,
|
||||
onShowMoreShow,
|
||||
onShowMoreHide,
|
||||
onClickShowMoreContextMenuItem,
|
||||
updateTabs,
|
||||
updateEditorContent,
|
||||
updateContentCache,
|
||||
onDropFiles,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.file-editor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
.nav-menu {
|
||||
flex-basis: $fileEditorNavMenuWidth;
|
||||
min-width: $fileEditorNavMenuWidth;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all $fileEditorNavMenuCollapseTransitionDuration;
|
||||
|
||||
&.collapsed {
|
||||
min-width: 0;
|
||||
flex-basis: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-menu-top-bar {
|
||||
flex-basis: $fileEditorNavMenuTopBarHeight;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
padding: 0 10px 0 0;
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-editor-content {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-width: calc(100% - #{$fileEditorNavMenuWidth});
|
||||
flex-direction: column;
|
||||
|
||||
.code-mirror-editor {
|
||||
flex: 1;
|
||||
|
||||
&.hidden {
|
||||
position: fixed;
|
||||
top: -100vh;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-tabs-suffix {
|
||||
width: 30px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: $fileEditorNavTabsHeight;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
.background {
|
||||
background-color: $fileEditorMaskBg;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.expand-files {
|
||||
width: 29px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-mirror-template {
|
||||
position: fixed;
|
||||
top: -100vh;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.file-editor .nav-menu .nav-menu-top-bar >>> .search.el-input .el-input__inner {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.file-editor .file-editor-content .code-mirror-editor >>> .CodeMirror {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.file-editor .file-editor-content .code-mirror-editor >>> .CodeMirror.dialog-opened {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-editor .file-editor-content .code-mirror-editor >>> .CodeMirror-dialog {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-editor .file-editor-content .code-mirror-editor >>> .CodeMirror-dialog.CodeMirror-dialog-top {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-editor .file-editor-content .code-mirror-editor >>> .CodeMirror-dialog.CodeMirror-dialog-bottom {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-editor .file-editor-content .code-mirror-editor >>> .CodeMirror-search-field {
|
||||
background-color: transparent;
|
||||
border: 1px solid;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,479 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:style="{
|
||||
backgroundColor: style.backgroundColorGutters,
|
||||
borderRight: `1px solid ${style.backgroundColor}`
|
||||
}"
|
||||
ref="fileEditorNavMenu"
|
||||
class="file-editor-nav-menu"
|
||||
>
|
||||
<el-tree
|
||||
ref="tree"
|
||||
:render-after-expand="defaultExpandAll"
|
||||
:data="items"
|
||||
:expand-on-click-node="false"
|
||||
:highlight-current="false"
|
||||
:allow-drop="allowDrop"
|
||||
empty-text="No files available"
|
||||
icon-class="fa fa-angle-right"
|
||||
:style="{
|
||||
backgroundColor: style.backgroundColorGutters,
|
||||
color: style.color,
|
||||
}"
|
||||
node-key="path"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
draggable
|
||||
@node-drag-enter="onNodeDragEnter"
|
||||
@node-drag-leave="onNodeDragLeave"
|
||||
@node-drag-end="onNodeDragEnd"
|
||||
@node-drop="onNodeDrop"
|
||||
@node-click="onNodeClick"
|
||||
@node-contextmenu="onNodeContextMenuShow"
|
||||
@node-expand="onNodeExpand"
|
||||
@node-collapse="onNodeCollapse"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<FileEditorNavMenuContextMenu
|
||||
:clicking="contextMenuClicking"
|
||||
:visible="isShowContextMenu(data)"
|
||||
@hide="onNodeContextMenuHide"
|
||||
@clone="onNodeContextMenuClone(data)"
|
||||
@delete="onNodeContextMenuDelete(data)"
|
||||
@rename="onNodeContextMenuRename(data)"
|
||||
@new-file="onNodeContextMenuNewFile(data)"
|
||||
@new-directory="onNodeContextMenuNewDirectory(data)"
|
||||
>
|
||||
<div
|
||||
v-bind="getBindDir(data)"
|
||||
:class="getItemClass(data)"
|
||||
class="nav-item-wrapper"
|
||||
>
|
||||
<div class="background"/>
|
||||
<div class="nav-item">
|
||||
<span class="icon">
|
||||
<atom-material-icon :is-dir="data.is_dir" :name="data.name"/>
|
||||
</span>
|
||||
<span class="title">
|
||||
{{ data.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</FileEditorNavMenuContextMenu>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, onUnmounted, reactive, ref} from 'vue';
|
||||
import {ClickOutside} from 'element-plus/lib/directives';
|
||||
import Node from 'element-plus/lib/el-tree/src/model/node';
|
||||
import {DropType} from 'element-plus/lib/el-tree/src/tree.type';
|
||||
import AtomMaterialIcon from '@/components/icon/AtomMaterialIcon.vue';
|
||||
import {KEY_CONTROL, KEY_META} from '@/constants/keyboard';
|
||||
import FileEditorNavMenuContextMenu from '@/components/file/FileEditorNavMenuContextMenu.vue';
|
||||
import {ElMessageBox, ElTree} from 'element-plus';
|
||||
import {useDropzone} from 'vue3-dropzone';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditorNavMenu',
|
||||
components: {
|
||||
FileEditorNavMenuContextMenu,
|
||||
AtomMaterialIcon
|
||||
},
|
||||
directives: {
|
||||
ClickOutside,
|
||||
},
|
||||
props: {
|
||||
activeItem: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
defaultExpandAll: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'node-click',
|
||||
'node-db-click',
|
||||
'node-drop',
|
||||
'ctx-menu-new-file',
|
||||
'ctx-menu-new-directory',
|
||||
'ctx-menu-rename',
|
||||
'ctx-menu-clone',
|
||||
'ctx-menu-delete',
|
||||
'drop-files',
|
||||
],
|
||||
setup(props, ctx) {
|
||||
const {emit} = ctx;
|
||||
|
||||
const tree = ref<typeof ElTree>();
|
||||
|
||||
const fileEditorNavMenu = ref<HTMLDivElement>();
|
||||
|
||||
const clickStatus = reactive<FileEditorNavMenuClickStatus>({
|
||||
clicked: false,
|
||||
item: undefined,
|
||||
});
|
||||
|
||||
const selectedCache = reactive<FileEditorNavMenuCache<boolean>>({});
|
||||
|
||||
const dragCache = reactive<FileEditorNavMenuCache<boolean>>({});
|
||||
|
||||
const isCtrlKeyPressed = ref<boolean>(false);
|
||||
|
||||
const activeContextMenuItem = ref<FileNavItem>();
|
||||
|
||||
const contextMenuClicking = ref<boolean>(false);
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
const defaultExpandedKeys = computed<string[]>(() => {
|
||||
return ['~'].concat(expandedKeys.value);
|
||||
});
|
||||
|
||||
const addDefaultExpandedKey = (key: string) => {
|
||||
if (!expandedKeys.value.includes(key)) expandedKeys.value.push(key);
|
||||
};
|
||||
|
||||
const removeDefaultExpandedKey = (key: string) => {
|
||||
if (!expandedKeys.value.includes(key)) return;
|
||||
const idx = expandedKeys.value.indexOf(key);
|
||||
expandedKeys.value.splice(idx, 1);
|
||||
};
|
||||
|
||||
const resetDefaultExpandedKeys = () => {
|
||||
expandedKeys.value = [];
|
||||
};
|
||||
|
||||
const resetClickStatus = () => {
|
||||
clickStatus.clicked = false;
|
||||
clickStatus.item = undefined;
|
||||
activeContextMenuItem.value = undefined;
|
||||
};
|
||||
|
||||
const updateSelectedMap = (item: FileNavItem) => {
|
||||
const key = item.path;
|
||||
if (!key) {
|
||||
console.warn('No path specified for FileNavItem');
|
||||
return;
|
||||
}
|
||||
if (!selectedCache[key]) {
|
||||
selectedCache[key] = false;
|
||||
}
|
||||
selectedCache[key] = !selectedCache[key];
|
||||
|
||||
// if Ctrl key is not pressed, clear other selection
|
||||
if (!isCtrlKeyPressed.value) {
|
||||
Object.keys(selectedCache).filter(k => k !== key).forEach(k => {
|
||||
selectedCache[k] = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onNodeClick = (item: FileNavItem) => {
|
||||
if (clickStatus.clicked && clickStatus.item?.path === item.path) {
|
||||
emit('node-db-click', item);
|
||||
updateSelectedMap(item);
|
||||
resetClickStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
clickStatus.item = item;
|
||||
clickStatus.clicked = true;
|
||||
setTimeout(() => {
|
||||
if (clickStatus.clicked) {
|
||||
emit('node-click', item);
|
||||
updateSelectedMap(item);
|
||||
}
|
||||
resetClickStatus();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onNodeContextMenuShow = (ev: Event, item: FileNavItem) => {
|
||||
contextMenuClicking.value = true;
|
||||
activeContextMenuItem.value = item;
|
||||
setTimeout(() => {
|
||||
contextMenuClicking.value = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onNodeContextMenuHide = () => {
|
||||
activeContextMenuItem.value = undefined;
|
||||
};
|
||||
|
||||
const onNodeContextMenuNewFile = async (item: FileNavItem) => {
|
||||
const res = await ElMessageBox.prompt('Please enter the name of the new file', 'New File');
|
||||
emit('ctx-menu-new-file', item, res.value);
|
||||
};
|
||||
|
||||
const onNodeContextMenuNewDirectory = async (item: FileNavItem) => {
|
||||
const res = await ElMessageBox.prompt('Please enter the name of the new directory', 'New Directory');
|
||||
emit('ctx-menu-new-directory', item, res.value);
|
||||
};
|
||||
|
||||
const onNodeContextMenuRename = async (item: FileNavItem) => {
|
||||
const res = await ElMessageBox.prompt('Please enter the new name', 'Rename');
|
||||
emit('ctx-menu-rename', item, res.value);
|
||||
};
|
||||
|
||||
const onNodeContextMenuClone = async (item: FileNavItem) => {
|
||||
const res = await ElMessageBox.prompt('Please enter the new name', 'Clone');
|
||||
emit('ctx-menu-clone', item, res.value);
|
||||
};
|
||||
|
||||
const onNodeContextMenuDelete = async (item: FileNavItem) => {
|
||||
await ElMessageBox.confirm('Are you sure to delete?', 'Delete');
|
||||
emit('ctx-menu-delete', item);
|
||||
};
|
||||
|
||||
const onNodeDragEnter = (draggingNode: Node, dropNode: Node) => {
|
||||
const item = dropNode.data as FileNavItem;
|
||||
if (!item.path) return;
|
||||
dragCache[item.path] = true;
|
||||
};
|
||||
|
||||
const onNodeDragLeave = (draggingNode: Node, dropNode: Node) => {
|
||||
const item = dropNode.data as FileNavItem;
|
||||
if (!item.path) return;
|
||||
dragCache[item.path] = false;
|
||||
};
|
||||
|
||||
const onNodeDragEnd = () => {
|
||||
for (const key in dragCache) {
|
||||
dragCache[key] = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onNodeDrop = (draggingNode: Node, dropNode: Node) => {
|
||||
const draggingItem = draggingNode.data as FileNavItem;
|
||||
const dropItem = dropNode.data as FileNavItem;
|
||||
emit('node-drop', draggingItem, dropItem);
|
||||
};
|
||||
|
||||
const onNodeExpand = (data: FileNavItem) => {
|
||||
addDefaultExpandedKey(data.path as string);
|
||||
};
|
||||
|
||||
const onNodeCollapse = (data: FileNavItem) => {
|
||||
removeDefaultExpandedKey(data.path as string);
|
||||
};
|
||||
|
||||
const isSelected = (item: FileNavItem): boolean => {
|
||||
if (!item.path) return false;
|
||||
return selectedCache[item.path] || false;
|
||||
};
|
||||
|
||||
const isDroppable = (item: FileNavItem): boolean => {
|
||||
if (!item.path) return false;
|
||||
return dragCache[item.path] || false;
|
||||
};
|
||||
|
||||
const isShowContextMenu = (item: FileNavItem) => {
|
||||
return activeContextMenuItem.value?.path === item.path;
|
||||
};
|
||||
|
||||
const allowDrop = (draggingNode: Node, dropNode: Node, type: DropType) => {
|
||||
if (type !== 'inner') return false;
|
||||
if (draggingNode.data?.path === dropNode.data?.path) return false;
|
||||
if (draggingNode.parent?.data?.path === dropNode.data?.path) return false;
|
||||
const item = dropNode.data as FileNavItem;
|
||||
return item.is_dir;
|
||||
};
|
||||
|
||||
const getItemClass = (item: FileNavItem): string[] => {
|
||||
const cls = [];
|
||||
if (isSelected(item)) cls.push('selected');
|
||||
if (isDroppable(item)) cls.push('droppable');
|
||||
return cls;
|
||||
};
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
} = useDropzone({
|
||||
onDrop: (files: InputFile[]) => {
|
||||
emit('drop-files', files);
|
||||
},
|
||||
});
|
||||
|
||||
const getBindDir = (item: FileNavItem) => getRootProps({
|
||||
onDragEnter: (ev: DragEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!item.is_dir || !item.path) return;
|
||||
dragCache[item.path] = true;
|
||||
},
|
||||
onDragLeave: (ev: DragEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!item.is_dir || !item.path) return;
|
||||
dragCache[item.path] = false;
|
||||
},
|
||||
onDrop: () => {
|
||||
for (const key in dragCache) {
|
||||
dragCache[key] = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// listen to keyboard events
|
||||
document.onkeydown = (ev: KeyboardEvent) => {
|
||||
if (!ev) return;
|
||||
if (ev.key === KEY_CONTROL || ev.key === KEY_META) {
|
||||
isCtrlKeyPressed.value = true;
|
||||
}
|
||||
};
|
||||
document.onkeyup = (ev: KeyboardEvent) => {
|
||||
if (!ev) return;
|
||||
if (ev.key === KEY_CONTROL || ev.key === KEY_META) {
|
||||
isCtrlKeyPressed.value = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// turnoff listening to keyboard events
|
||||
document.onkeydown = null;
|
||||
document.onkeyup = null;
|
||||
});
|
||||
|
||||
return {
|
||||
tree,
|
||||
activeContextMenuItem,
|
||||
fileEditorNavMenu,
|
||||
contextMenuClicking,
|
||||
defaultExpandedKeys,
|
||||
onNodeClick,
|
||||
onNodeContextMenuShow,
|
||||
onNodeContextMenuHide,
|
||||
onNodeContextMenuNewFile,
|
||||
onNodeContextMenuNewDirectory,
|
||||
onNodeContextMenuRename,
|
||||
onNodeContextMenuClone,
|
||||
onNodeContextMenuDelete,
|
||||
onNodeDragEnter,
|
||||
onNodeDragLeave,
|
||||
onNodeDragEnd,
|
||||
onNodeDrop,
|
||||
onNodeExpand,
|
||||
onNodeCollapse,
|
||||
isSelected,
|
||||
isDroppable,
|
||||
isShowContextMenu,
|
||||
allowDrop,
|
||||
getItemClass,
|
||||
resetDefaultExpandedKeys,
|
||||
addDefaultExpandedKey,
|
||||
removeDefaultExpandedKey,
|
||||
getBindDir,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.file-editor-nav-menu {
|
||||
flex: 1;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.el-tree {
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
max-width: fit-content;
|
||||
|
||||
.el-tree-node {
|
||||
.nav-item-wrapper {
|
||||
z-index: 2;
|
||||
|
||||
& * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.background {
|
||||
background-color: $fileEditorMaskBg;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
color: $fileEditorNavMenuItemSelectedColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.droppable {
|
||||
& * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
border: 1px dashed $fileEditorNavMenuItemDragTargetBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item:hover,
|
||||
.background:hover + .nav-item {
|
||||
color: $fileEditorNavMenuItemSelectedColor;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.file-editor-nav-menu >>> .el-tree .el-tree-node > .el-tree-node__content {
|
||||
background-color: inherit;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.file-editor-nav-menu >>> .el-tree .el-tree-node > .el-tree-node__content .el-tree-node__expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.file-editor-nav-menu >>> .el-tree .el-tree-node * {
|
||||
transition: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<ContextMenu :clicking="clicking" :placement="placement" :visible="visible" @hide="$emit('hide')">
|
||||
<template #default>
|
||||
<ContextMenuList :items="items" @hide="$emit('hide')"/>
|
||||
</template>
|
||||
<template #reference>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, readonly} from 'vue';
|
||||
import ContextMenu, {contextMenuDefaultProps} from '@/components/context-menu/ContextMenu.vue';
|
||||
import ContextMenuList from '@/components/context-menu/ContextMenuList.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditorNavMenuContextMenu',
|
||||
components: {ContextMenuList, ContextMenu},
|
||||
props: contextMenuDefaultProps,
|
||||
emits: [
|
||||
'hide',
|
||||
'new-file',
|
||||
'new-directory',
|
||||
'rename',
|
||||
'clone',
|
||||
'delete',
|
||||
],
|
||||
setup(props, {emit}) {
|
||||
const items = readonly<ContextMenuItem[]>([
|
||||
{title: 'New File', icon: ['fa', 'file-alt'], action: () => emit('new-file')},
|
||||
{title: 'New Directory', icon: ['fa', 'folder-plus'], action: () => emit('new-directory')},
|
||||
{title: 'Rename', icon: ['fa', 'edit'], action: () => emit('rename')},
|
||||
{title: 'Duplicate', icon: ['fa', 'clone'], action: () => emit('clone')},
|
||||
{title: 'Delete', icon: ['fa', 'trash'], action: () => emit('delete')},
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="navTabs"
|
||||
:style="{
|
||||
backgroundColor: style.backgroundColorGutters,
|
||||
color: style.color,
|
||||
}"
|
||||
class="file-editor-nav-tabs"
|
||||
>
|
||||
<slot name="prefix"></slot>
|
||||
<DraggableList
|
||||
item-key="path"
|
||||
:items="tabs"
|
||||
@d-end="onDragEnd"
|
||||
>
|
||||
<template v-slot="{item}">
|
||||
<FileEditorNavTabsContextMenu
|
||||
:clicking="contextMenuClicking"
|
||||
:visible="isShowContextMenu(item)"
|
||||
@close="onClose(item)"
|
||||
@hide="onContextMenuHide"
|
||||
@close-others="onCloseOthers(item)"
|
||||
@close-all="onCloseAll"
|
||||
>
|
||||
<div
|
||||
:class="activeTab && activeTab.path === item.path ? 'active' : ''"
|
||||
:style="{
|
||||
backgroundColor: style.backgroundColor,
|
||||
}"
|
||||
class="file-editor-nav-tab"
|
||||
@click="onClick(item)"
|
||||
@contextmenu.prevent="onContextMenuShow(item)"
|
||||
>
|
||||
<span class="icon">
|
||||
<atom-material-icon :is-dir="item.is_dir" :name="item.name"/>
|
||||
</span>
|
||||
<el-tooltip :content="getTitle(item)" :show-after="500">
|
||||
<span class="title">
|
||||
{{ getTitle(item) }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span class="close-btn" @click.stop="onClose(item)">
|
||||
<i class="el-icon-close"></i>
|
||||
</span>
|
||||
<div class="background"/>
|
||||
</div>
|
||||
</FileEditorNavTabsContextMenu>
|
||||
</template>
|
||||
</DraggableList>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, ref, watch} from 'vue';
|
||||
import DraggableList from '@/components/drag/DraggableList.vue';
|
||||
import AtomMaterialIcon from '@/components/icon/AtomMaterialIcon.vue';
|
||||
import FileEditorNavTabsContextMenu from '@/components/file/FileEditorNavTabsContextMenu.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditorNavTabs',
|
||||
components: {FileEditorNavTabsContextMenu, AtomMaterialIcon, DraggableList},
|
||||
props: {
|
||||
activeTab: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'tab-click',
|
||||
'tab-close',
|
||||
'tab-close-others',
|
||||
'tab-close-all',
|
||||
'tab-dragend',
|
||||
'show-more',
|
||||
],
|
||||
setup(props, {emit}) {
|
||||
const activeContextMenuItem = ref<FileNavItem>();
|
||||
|
||||
const navTabs = ref<HTMLDivElement>();
|
||||
|
||||
const navTabsWidth = ref<number>();
|
||||
|
||||
const navTabsOverflowWidth = ref<number>();
|
||||
|
||||
const showMoreVisible = computed<boolean>(() => {
|
||||
if (navTabsWidth.value === undefined || navTabsOverflowWidth.value === undefined) return false;
|
||||
return navTabsOverflowWidth.value > navTabsWidth.value;
|
||||
});
|
||||
|
||||
const contextMenuClicking = ref<boolean>(false);
|
||||
|
||||
const tabs = computed<FileNavItem[]>(() => {
|
||||
const {tabs} = props as FileEditorNavTabsProps;
|
||||
return tabs;
|
||||
});
|
||||
|
||||
const getTitle = (item: FileNavItem) => {
|
||||
return item.name;
|
||||
};
|
||||
|
||||
const onClick = (item: FileNavItem) => {
|
||||
emit('tab-click', item);
|
||||
};
|
||||
|
||||
const onClose = (item: FileNavItem) => {
|
||||
emit('tab-close', item);
|
||||
};
|
||||
|
||||
const onCloseOthers = (item: FileNavItem) => {
|
||||
emit('tab-close-others', item);
|
||||
};
|
||||
|
||||
const onCloseAll = () => {
|
||||
emit('tab-close-all');
|
||||
};
|
||||
|
||||
const onDragEnd = (items: FileNavItem[]) => {
|
||||
emit('tab-dragend', items);
|
||||
};
|
||||
|
||||
const onContextMenuShow = (item: FileNavItem) => {
|
||||
contextMenuClicking.value = true;
|
||||
activeContextMenuItem.value = item;
|
||||
|
||||
setTimeout(() => {
|
||||
contextMenuClicking.value = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onContextMenuHide = () => {
|
||||
activeContextMenuItem.value = undefined;
|
||||
};
|
||||
|
||||
const isShowContextMenu = (item: FileNavItem) => {
|
||||
return activeContextMenuItem.value?.path === item.path;
|
||||
};
|
||||
|
||||
const updateWidths = () => {
|
||||
if (!navTabs.value) return;
|
||||
|
||||
// width
|
||||
navTabsWidth.value = Number(getComputedStyle(navTabs.value).width.replace('px', ''));
|
||||
|
||||
// overflow width
|
||||
const el = navTabs.value.querySelector('.draggable-list');
|
||||
if (el) {
|
||||
navTabsOverflowWidth.value = Number(getComputedStyle(el).width.replace('px', ''));
|
||||
}
|
||||
};
|
||||
|
||||
watch(tabs.value, () => {
|
||||
setTimeout(updateWidths, 100);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// update tabs widths
|
||||
updateWidths();
|
||||
});
|
||||
|
||||
return {
|
||||
activeContextMenuItem,
|
||||
navTabs,
|
||||
navTabsWidth,
|
||||
navTabsOverflowWidth,
|
||||
showMoreVisible,
|
||||
contextMenuClicking,
|
||||
getTitle,
|
||||
onClick,
|
||||
onClose,
|
||||
onCloseOthers,
|
||||
onCloseAll,
|
||||
onDragEnd,
|
||||
onContextMenuShow,
|
||||
onContextMenuHide,
|
||||
isShowContextMenu,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.file-editor-nav-tabs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
height: $fileEditorNavTabsHeight;
|
||||
|
||||
.file-editor-nav-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
height: $fileEditorNavTabsHeight;
|
||||
max-width: $fileEditorNavTabsItemMaxWidth;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
.background {
|
||||
background-color: $fileEditorMaskBg;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-bottom: 2px solid $primaryColor;
|
||||
|
||||
.title {
|
||||
color: $fileEditorNavTabsItemActiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: $fileEditorNavTabsItemColor;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
margin-left: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.suffix {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<ContextMenu :clicking="clicking" :placement="placement" :visible="visible" @hide="$emit('hide')">
|
||||
<template #default>
|
||||
<ContextMenuList :items="items" @hide="$emit('hide')"/>
|
||||
</template>
|
||||
<template #reference>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, readonly} from 'vue';
|
||||
import ContextMenu, {contextMenuDefaultProps} from '@/components/context-menu/ContextMenu.vue';
|
||||
import ContextMenuList from '@/components/context-menu/ContextMenuList.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditorNavTabsContextMenu',
|
||||
components: {ContextMenuList, ContextMenu},
|
||||
props: contextMenuDefaultProps,
|
||||
emits: [
|
||||
'hide',
|
||||
'close',
|
||||
'close-others',
|
||||
'close-all',
|
||||
],
|
||||
setup(props, {emit}) {
|
||||
const items = readonly<ContextMenuItem[]>([
|
||||
{title: 'Close', icon: ['fa', 'times'], action: () => emit('close')},
|
||||
{title: 'Close Others', action: () => emit('close-others')},
|
||||
{title: 'Close All', action: () => emit('close-all')},
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<ContextMenu :placement="placement" :visible="visible" @hide="$emit('hide')">
|
||||
<template #default>
|
||||
<ContextMenuList :items="items" @hide="$emit('hide')"/>
|
||||
</template>
|
||||
<template #reference>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import ContextMenu, {contextMenuDefaultProps} from '@/components/context-menu/ContextMenu.vue';
|
||||
import ContextMenuList from '@/components/context-menu/ContextMenuList.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditorNavTabsShowMoreContextMenu',
|
||||
components: {ContextMenuList, ContextMenu},
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
...contextMenuDefaultProps,
|
||||
},
|
||||
emits: [
|
||||
'tab-click',
|
||||
],
|
||||
setup(props, {emit}) {
|
||||
const items = computed<ContextMenuItem[]>(() => {
|
||||
const {tabs} = props as FileEditorNavTabsShowMoreContextMenuProps;
|
||||
const contextMenuItems: ContextMenuItem[] = tabs.map(t => {
|
||||
return {
|
||||
title: t.path || '',
|
||||
icon: t.name || '',
|
||||
action: () => emit('tab-click', t),
|
||||
};
|
||||
});
|
||||
return contextMenuItems;
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,198 +0,0 @@
|
||||
<template>
|
||||
<div class="file-editor-settings-dialog">
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="File Editor Settings"
|
||||
@close="onClose"
|
||||
>
|
||||
<el-menu :default-active="activeTabName" class="nav-menu" mode="horizontal" @select="onTabChange">
|
||||
<el-menu-item v-for="tab in tabs" :key="tab.name" :index="tab.name">
|
||||
{{ tab.title }}
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<el-form
|
||||
:label-width="variables.fileEditorSettingsDialogLabelWidth"
|
||||
class="form"
|
||||
size="small"
|
||||
>
|
||||
<el-form-item
|
||||
v-for="name in optionNames[activeTabName]"
|
||||
:key="name"
|
||||
>
|
||||
<template #label>
|
||||
<el-tooltip :content="getDefinitionDescription(name)" popper-class="help-tooltip" trigger="click">
|
||||
<font-awesome-icon :icon="['far', 'question-circle']" class="icon" size="sm"/>
|
||||
</el-tooltip>
|
||||
{{ getDefinitionTitle(name) }}
|
||||
</template>
|
||||
<FileEditorSettingsFormItem v-model="options[name]" :name="name"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button plain size="small" type="info" @click="onClose">Cancel</el-button>
|
||||
<el-button size="small" type="primary" @click="onConfirm">Save</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onBeforeMount, readonly, ref} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import {plainClone} from '@/utils/object';
|
||||
import variables from '@/styles/variables.scss';
|
||||
import {getOptionDefinition, getThemes} from '@/utils/codemirror';
|
||||
import FileEditorSettingsFormItem from '@/components/file/FileEditorSettingsFormItem.vue';
|
||||
import {onBeforeRouteLeave} from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditorSettingsDialog',
|
||||
components: {FileEditorSettingsFormItem},
|
||||
setup() {
|
||||
const storeNamespace = 'file';
|
||||
const store = useStore();
|
||||
const {file} = store.state as RootStoreState;
|
||||
|
||||
const options = ref<FileEditorConfiguration>({});
|
||||
|
||||
const tabs = readonly([
|
||||
{name: 'general', title: 'General'},
|
||||
{name: 'edit', title: 'Edit'},
|
||||
{name: 'indentation', title: 'Indentation'},
|
||||
{name: 'cursor', title: 'Cursor'},
|
||||
]);
|
||||
|
||||
const optionNames = readonly({
|
||||
general: [
|
||||
'theme',
|
||||
'keyMap',
|
||||
'lineWrapping',
|
||||
'lineNumbers',
|
||||
'maxHighlightLength',
|
||||
'spellcheck',
|
||||
'autocorrect',
|
||||
'autocapitalize',
|
||||
],
|
||||
edit: [
|
||||
'lineWiseCopyCut',
|
||||
'pasteLinesPerSelection',
|
||||
'undoDepth',
|
||||
],
|
||||
indentation: [
|
||||
'indentUnit',
|
||||
'smartIndent',
|
||||
'tabSize',
|
||||
'indentWithTabs',
|
||||
'electricChars',
|
||||
],
|
||||
cursor: [
|
||||
'showCursorWhenSelecting',
|
||||
'cursorBlinkRate',
|
||||
'cursorScrollMargin',
|
||||
'cursorHeight',
|
||||
],
|
||||
});
|
||||
|
||||
const activeTabName = ref<string>(tabs[0].name);
|
||||
|
||||
const visible = computed<boolean>(() => {
|
||||
const {editorSettingsDialogVisible} = file;
|
||||
return editorSettingsDialogVisible;
|
||||
});
|
||||
|
||||
const themes = computed<string[]>(() => {
|
||||
return getThemes();
|
||||
});
|
||||
|
||||
const resetOptions = () => {
|
||||
const {editorOptions} = file;
|
||||
options.value = plainClone(editorOptions);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
store.commit(`${storeNamespace}/setEditorSettingsDialogVisible`, false);
|
||||
resetOptions();
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
store.commit(`${storeNamespace}/setEditorOptions`, options.value);
|
||||
store.commit(`${storeNamespace}/setEditorSettingsDialogVisible`, false);
|
||||
resetOptions();
|
||||
};
|
||||
|
||||
const onTabChange = (tabName: string) => {
|
||||
activeTabName.value = tabName;
|
||||
};
|
||||
|
||||
const getDefinitionDescription = (name: string) => {
|
||||
return getOptionDefinition(name)?.description;
|
||||
};
|
||||
|
||||
const getDefinitionTitle = (name: string) => {
|
||||
return getOptionDefinition(name)?.title;
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
resetOptions();
|
||||
});
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
store.commit(`${storeNamespace}/setEditorSettingsDialogVisible`, false);
|
||||
});
|
||||
|
||||
return {
|
||||
variables,
|
||||
options,
|
||||
activeTabName,
|
||||
tabs,
|
||||
optionNames,
|
||||
visible,
|
||||
themes,
|
||||
onClose,
|
||||
onConfirm,
|
||||
onTabChange,
|
||||
getDefinitionDescription,
|
||||
getDefinitionTitle,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-editor-settings-dialog {
|
||||
.nav-menu {
|
||||
.el-menu-item {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.file-editor-settings-dialog >>> .el-dialog .el-dialog__body {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.file-editor-settings-dialog >>> .el-form-item > .el-form-item__label .icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-editor-settings-dialog >>> .el-form-item > .el-form-item__content {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.file-editor-settings-dialog >>> .el-form-item > .el-form-item__content .el-input,
|
||||
.file-editor-settings-dialog >>> .el-form-item > .el-form-item__content .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.help-tooltip {
|
||||
max-width: 240px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<el-select v-if="type === 'select'" :value="value" @change="onChange">
|
||||
<el-option v-for="op in data.options" :key="op" :label="op" :value="op"/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-else-if="type === 'input-number'"
|
||||
:min="data.min !== undefined ? data.min : 0"
|
||||
:step="data.step !== undefined ? data.step : 1"
|
||||
:value="value"
|
||||
@change="onChange"
|
||||
/>
|
||||
<el-switch v-else-if="type === 'switch'" :value="value" @change="onChange"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import {getOptionDefinition} from '@/utils/codemirror';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEditorSettingsFormItem',
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
const def = computed<FileEditorOptionDefinition | undefined>(() => {
|
||||
const {name} = props;
|
||||
return getOptionDefinition(name);
|
||||
});
|
||||
const type = computed<FileEditorOptionDefinitionType | undefined>(() => def.value?.type);
|
||||
const data = computed<any>(() => def.value?.data);
|
||||
|
||||
const onChange = (value: any) => {
|
||||
emit('input', value);
|
||||
};
|
||||
|
||||
return {
|
||||
type,
|
||||
data,
|
||||
onChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,210 +0,0 @@
|
||||
<template>
|
||||
<div class="file-upload">
|
||||
<div class="mode-select">
|
||||
<el-radio-group v-model="internalMode" @change="onModeChange">
|
||||
<el-radio
|
||||
v-for="{value, label} in modeOptions"
|
||||
:key="value"
|
||||
:label="value"
|
||||
>
|
||||
{{ label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<template v-if="mode === FILE_UPLOAD_MODE_FILES">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:on-change="onFileChange"
|
||||
:http-request="() => {}"
|
||||
drag
|
||||
multiple
|
||||
:show-file-list="false"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">Drag files here, or <em>click to upload</em></div>
|
||||
</el-upload>
|
||||
<input v-bind="getInputProps()" multiple>
|
||||
</template>
|
||||
<template v-else-if="mode === FILE_UPLOAD_MODE_DIR">
|
||||
<div class="folder-upload">
|
||||
<Button size="large" @click="open">
|
||||
<i class="fa fa-folder"></i>
|
||||
Click to Select Folder to Upload
|
||||
</Button>
|
||||
<template v-if="!!dirInfo?.dirName && dirInfo?.fileCount">
|
||||
<Tag
|
||||
type="primary"
|
||||
class="info-tag"
|
||||
:label="dirInfo?.dirName"
|
||||
:icon="['fa', 'folder']"
|
||||
tooltip="Folder Name"
|
||||
/>
|
||||
<Tag
|
||||
type="success"
|
||||
class="info-tag"
|
||||
:label="dirInfo?.fileCount"
|
||||
:icon="['fa', 'hashtag']"
|
||||
tooltip="Files Count"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<input v-bind="getInputProps()" webkitdirectory multiple>
|
||||
</template>
|
||||
<div v-if="dirInfo?.filePaths?.length > 0" class="file-list-wrapper">
|
||||
<h4 class="title">Files to Upload</h4>
|
||||
<ul class="file-list">
|
||||
<li v-for="(path, $index) in dirInfo?.filePaths" :key="$index" class="file-item">
|
||||
{{ path }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onBeforeMount, ref, watch} from 'vue';
|
||||
import {FILE_UPLOAD_MODE_DIR, FILE_UPLOAD_MODE_FILES} from '@/constants/file';
|
||||
import {ElUpload} from 'element-plus/lib/el-upload/src/upload.type';
|
||||
import {UploadFile} from 'element-plus/packages/upload/src/upload.type';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
import Tag from '@/components/tag/Tag.vue';
|
||||
import {plainClone} from '@/utils/object';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileUpload',
|
||||
components: {
|
||||
Tag,
|
||||
Button,
|
||||
},
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
},
|
||||
getInputProps: {
|
||||
type: Function,
|
||||
},
|
||||
open: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'mode-change',
|
||||
'files-change',
|
||||
],
|
||||
setup(props: FileUploadProps, {emit}) {
|
||||
const modeOptions: FileUploadModeOption[] = [
|
||||
{
|
||||
label: 'Folder',
|
||||
value: FILE_UPLOAD_MODE_DIR,
|
||||
},
|
||||
{
|
||||
label: 'Files',
|
||||
value: FILE_UPLOAD_MODE_FILES,
|
||||
},
|
||||
];
|
||||
const internalMode = ref<string>();
|
||||
|
||||
const uploadRef = ref<ElUpload>();
|
||||
|
||||
const dirPath = ref<string>();
|
||||
|
||||
watch(() => props.mode, () => {
|
||||
internalMode.value = props.mode;
|
||||
uploadRef.value?.clearFiles();
|
||||
});
|
||||
|
||||
const onFileChange = (file: UploadFile, fileList: UploadFile[]) => {
|
||||
emit('files-change', fileList.map(f => f.raw));
|
||||
};
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles();
|
||||
};
|
||||
|
||||
const onModeChange = (mode: string) => {
|
||||
emit('mode-change', mode);
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
const {mode} = props;
|
||||
internalMode.value = mode;
|
||||
});
|
||||
|
||||
const dirInfo = ref<FileUploadInfo>();
|
||||
|
||||
const setInfo = (info: FileUploadInfo) => {
|
||||
dirInfo.value = plainClone(info);
|
||||
}
|
||||
|
||||
const resetInfo = (info: FileUploadInfo) => {
|
||||
dirInfo.value = undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
uploadRef,
|
||||
FILE_UPLOAD_MODE_FILES,
|
||||
FILE_UPLOAD_MODE_DIR,
|
||||
modeOptions,
|
||||
internalMode,
|
||||
dirPath,
|
||||
onFileChange,
|
||||
clearFiles,
|
||||
onModeChange,
|
||||
dirInfo,
|
||||
setInfo,
|
||||
resetInfo,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/variables";
|
||||
|
||||
.file-upload {
|
||||
.mode-select {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.folder-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.file-list-wrapper {
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style: none;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
border: 1px solid $infoPlainColor;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
.file-item {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.file-upload >>> .el-upload,
|
||||
.file-upload >>> .el-upload .el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-upload >>> .folder-upload .info-tag {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +0,0 @@
|
||||
import {useDropzone} from 'vue3-dropzone';
|
||||
|
||||
const useFileEditorDropZone = () => {
|
||||
const onDrop = (acceptedFiles: InputFile[], rejectReasons: FileRejectReason[], event: Event) => {
|
||||
console.log(acceptedFiles);
|
||||
};
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
open,
|
||||
} = useDropzone({
|
||||
onDrop,
|
||||
});
|
||||
|
||||
return {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
open,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFileEditorDropZone;
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<div class="filter-condition">
|
||||
<el-select
|
||||
:model-value="condition.type"
|
||||
:popper-append-to-body="false"
|
||||
class="filter-condition-type"
|
||||
size="mini"
|
||||
@change="onTypeChange"
|
||||
>
|
||||
<el-option v-for="op in conditionTypesOptions" :key="op.value" :label="op.label" :value="op.value"/>
|
||||
</el-select>
|
||||
<el-input
|
||||
:model-value="condition.value"
|
||||
class="filter-condition-value"
|
||||
:class="isInvalidValue ? 'invalid' : ''"
|
||||
size="mini"
|
||||
placeholder="Value"
|
||||
:disabled="condition.type === FILTER_OP_NOT_SET"
|
||||
@input="onValueChange"
|
||||
/>
|
||||
<el-tooltip content="Delete Condition">
|
||||
<el-icon class="icon" name="circle-close" @click="onDelete"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import {
|
||||
FILTER_OP_CONTAINS,
|
||||
FILTER_OP_EQUAL,
|
||||
FILTER_OP_GREATER_THAN,
|
||||
FILTER_OP_GREATER_THAN_EQUAL,
|
||||
FILTER_OP_LESS_THAN,
|
||||
FILTER_OP_LESS_THAN_EQUAL,
|
||||
FILTER_OP_NOT_CONTAINS,
|
||||
FILTER_OP_NOT_EQUAL,
|
||||
FILTER_OP_NOT_SET,
|
||||
FILTER_OP_REGEX,
|
||||
} from '@/constants/filter';
|
||||
import {plainClone} from '@/utils/object';
|
||||
|
||||
export const defaultFilterCondition: FilterConditionData = {
|
||||
op: FILTER_OP_NOT_SET,
|
||||
value: '',
|
||||
};
|
||||
|
||||
export const getDefaultFilterCondition = () => {
|
||||
return plainClone(defaultFilterCondition);
|
||||
};
|
||||
|
||||
export const conditionTypesOptions: SelectOption[] = [
|
||||
{value: FILTER_OP_NOT_SET, label: 'Not Set'},
|
||||
{value: FILTER_OP_CONTAINS, label: 'Contains'},
|
||||
{value: FILTER_OP_NOT_CONTAINS, label: 'Not Contains'},
|
||||
{value: FILTER_OP_REGEX, label: 'Regex'},
|
||||
{value: FILTER_OP_EQUAL, label: 'Equal to'},
|
||||
{value: FILTER_OP_NOT_EQUAL, label: 'Not Equal to'},
|
||||
{value: FILTER_OP_GREATER_THAN, label: 'Greater than'},
|
||||
{value: FILTER_OP_LESS_THAN, label: 'Less than'},
|
||||
{value: FILTER_OP_GREATER_THAN_EQUAL, label: 'Greater than or Equal to'},
|
||||
{value: FILTER_OP_LESS_THAN_EQUAL, label: 'Less than or Equal to'},
|
||||
];
|
||||
|
||||
export const conditionTypesMap: { [key: string]: string } = (() => {
|
||||
const map: { [key: string]: string } = {};
|
||||
conditionTypesOptions.forEach(d => {
|
||||
map[d.value] = d.label as string;
|
||||
});
|
||||
return map;
|
||||
})();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilterCondition',
|
||||
props: {
|
||||
condition: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'change',
|
||||
'delete',
|
||||
],
|
||||
setup(props, {emit}) {
|
||||
const isInvalidValue = computed<boolean>(() => {
|
||||
const {condition} = props as FilterConditionProps;
|
||||
if (condition?.op === FILTER_OP_NOT_SET) {
|
||||
return false;
|
||||
}
|
||||
return !condition?.value;
|
||||
});
|
||||
|
||||
const onTypeChange = (conditionType: string) => {
|
||||
const {condition} = props as FilterConditionProps;
|
||||
if (condition) {
|
||||
condition.op = conditionType;
|
||||
if (condition.op === FILTER_OP_NOT_SET) {
|
||||
condition.value = undefined;
|
||||
}
|
||||
}
|
||||
emit('change', condition);
|
||||
};
|
||||
|
||||
const onValueChange = (conditionValue: string) => {
|
||||
const {condition} = props as FilterConditionProps;
|
||||
if (condition) {
|
||||
condition.value = conditionValue;
|
||||
}
|
||||
emit('change', condition);
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
emit('delete');
|
||||
};
|
||||
|
||||
return {
|
||||
FILTER_OP_NOT_SET,
|
||||
conditionTypesOptions,
|
||||
isInvalidValue,
|
||||
onTypeChange,
|
||||
onValueChange,
|
||||
onDelete,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-condition {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.filter-condition-type {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.filter-condition-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.filter-condition >>> .filter-condition-type.el-select .el-input__inner {
|
||||
border-color: #DCDFE6 !important;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.filter-condition >>> .filter-condition-value.el-input .el-input__inner {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.filter-condition >>> .filter-condition-value.el-input.invalid .el-input__inner {
|
||||
border-color: #F56C6C;
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<ul class="filter-condition-list">
|
||||
<li
|
||||
v-for="(cond, $index) in conditions"
|
||||
:key="$index"
|
||||
class="filter-condition-item"
|
||||
>
|
||||
<FilterCondition :condition="cond" @change="onChange($index, $event)" @delete="onDelete($index)"/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import FilterCondition, {getDefaultFilterCondition} from '@/components/filter/FilterCondition.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilterConditionList',
|
||||
components: {
|
||||
FilterCondition,
|
||||
},
|
||||
props: {
|
||||
conditions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'change',
|
||||
],
|
||||
setup(props, {emit}) {
|
||||
const onChange = (index: number, condition: FilterConditionData) => {
|
||||
const {conditions} = props as FilterConditionListProps;
|
||||
conditions[index] = condition;
|
||||
emit('change', conditions);
|
||||
};
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
const {conditions} = props as FilterConditionListProps;
|
||||
conditions.splice(index, 1);
|
||||
if (conditions.length === 0) {
|
||||
conditions.push(getDefaultFilterCondition());
|
||||
}
|
||||
emit('change', conditions);
|
||||
};
|
||||
|
||||
return {
|
||||
onChange,
|
||||
onDelete,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-condition-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.filter-condition-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:inline="inline"
|
||||
:label-width="labelWidth"
|
||||
:size="size"
|
||||
:model="model"
|
||||
class="form"
|
||||
:rules="rules"
|
||||
hide-required-asterisk
|
||||
@validate="$emit('validate')"
|
||||
>
|
||||
<slot></slot>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType, provide, reactive, ref} from 'vue';
|
||||
import {ElForm} from 'element-plus';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Form',
|
||||
props: {
|
||||
model: {
|
||||
type: Object as PropType<FormModel>,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
labelWidth: {
|
||||
type: String,
|
||||
default: '150px',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'mini',
|
||||
},
|
||||
grid: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
rules: {
|
||||
type: Object as PropType<FormRules>,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'validate',
|
||||
],
|
||||
setup(props: FormProps, {emit}) {
|
||||
const form = computed<FormContext>(() => {
|
||||
const {labelWidth, size, grid} = props;
|
||||
return {labelWidth, size, grid};
|
||||
});
|
||||
|
||||
provide('form-context', reactive<FormContext>(form.value));
|
||||
|
||||
const formRef = ref<typeof ElForm>();
|
||||
|
||||
const validate = async () => {
|
||||
return await formRef.value?.validate();
|
||||
};
|
||||
|
||||
const resetFields = () => {
|
||||
return formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const clearValidate = () => {
|
||||
return formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
return {
|
||||
formRef,
|
||||
validate,
|
||||
resetFields,
|
||||
clearValidate,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,209 +0,0 @@
|
||||
<template>
|
||||
<div ref="formItem" :style="style" class="form-item">
|
||||
<el-form-item
|
||||
:prop="prop"
|
||||
:label="label"
|
||||
:required="isRequired"
|
||||
:rules="rules"
|
||||
:size="size || formContext?.size"
|
||||
>
|
||||
<template #label>
|
||||
<el-tooltip :content="labelTooltip" :disabled="!labelTooltip">
|
||||
<span class="form-item-label">
|
||||
<span :class="showRequiredAsterisk ? 'required' : ''" class="form-item-label-text">
|
||||
{{ label }}
|
||||
</span>
|
||||
<el-tooltip v-if="isSelectiveForm" :content="editableTooltip">
|
||||
<el-checkbox
|
||||
v-model="internalEditable"
|
||||
:disabled="notEditable"
|
||||
class="editable-checkbox"
|
||||
@change="onEditableChange"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="form-item-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, inject, onMounted, PropType, ref, watch} from 'vue';
|
||||
import {RuleItem} from 'async-validator';
|
||||
import {cloneArray} from '@/utils/object';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FormItem',
|
||||
props: {
|
||||
prop: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
labelTooltip: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
labelWidth: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
span: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1,
|
||||
},
|
||||
offset: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
rules: {
|
||||
type: [Object, Array] as PropType<RuleItem | RuleItem[]>,
|
||||
},
|
||||
notEditable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props: FormItemProps, {emit}) {
|
||||
const formItem = ref<HTMLDivElement>();
|
||||
|
||||
// form context
|
||||
const formContext = inject<FormContext>('form-context', {} as FormContext);
|
||||
|
||||
// store context
|
||||
const storeContext = inject<ListStoreContext<BaseModel>>('store-context');
|
||||
const ns = storeContext?.namespace;
|
||||
const store = storeContext?.store;
|
||||
const state = storeContext?.state;
|
||||
const isSelectiveForm = computed<boolean | undefined>(() => state?.isSelectiveForm);
|
||||
const selectedFormFields = computed<string[] | undefined>(() => state?.selectedFormFields);
|
||||
const isBatchForm = computed<boolean>(() => store?.getters[`${ns}/isBatchForm`]);
|
||||
const activeDialogKey = computed<DialogKey | undefined>(() => state?.activeDialogKey);
|
||||
|
||||
const style = computed<Partial<CSSStyleDeclaration>>(() => {
|
||||
const {span, offset} = props;
|
||||
return {
|
||||
flexBasis: `calc(100% / ${formContext.grid} * ${span})`,
|
||||
marginRight: `calc(100% / ${formContext.grid} * ${offset})`,
|
||||
};
|
||||
});
|
||||
|
||||
const internalEditable = ref<boolean>(false);
|
||||
watch(() => state?.activeDialogKey, () => internalEditable.value = false);
|
||||
|
||||
const editableTooltip = computed<string>(() => {
|
||||
const {notEditable} = props;
|
||||
if (notEditable) return 'Unable to edit';
|
||||
return internalEditable.value ? 'Uncheck to disable editing' : 'Check to enable editing';
|
||||
});
|
||||
|
||||
const onEditableChange = (value: boolean) => {
|
||||
const {prop} = props;
|
||||
if (!selectedFormFields.value || !prop) return;
|
||||
const fields = cloneArray<string>(selectedFormFields.value);
|
||||
if (value) {
|
||||
if (!fields.includes(prop)) {
|
||||
fields.push(prop);
|
||||
}
|
||||
} else {
|
||||
if (fields.includes(prop)) {
|
||||
const idx = fields.findIndex(field => field === prop);
|
||||
fields.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
store?.commit(`${ns}/setSelectedFormFields`, fields);
|
||||
};
|
||||
|
||||
const isRequired = computed<boolean>(() => {
|
||||
switch (activeDialogKey.value) {
|
||||
case 'edit':
|
||||
if (isBatchForm.value) {
|
||||
if (!internalEditable.value) return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const {required} = props;
|
||||
return required;
|
||||
});
|
||||
|
||||
const showRequiredAsterisk = computed<boolean>(() => {
|
||||
if (isSelectiveForm.value) return false;
|
||||
const {required} = props;
|
||||
return required;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (formItem.value) {
|
||||
const {labelWidth} = formContext;
|
||||
const el = formItem.value?.querySelector('.el-form-item__content') as HTMLDivElement;
|
||||
if (labelWidth && el.style) {
|
||||
el.style.width = `calc(100% - ${labelWidth})`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
formItem,
|
||||
formContext,
|
||||
style,
|
||||
isSelectiveForm,
|
||||
internalEditable,
|
||||
editableTooltip,
|
||||
onEditableChange,
|
||||
isRequired,
|
||||
showRequiredAsterisk,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables";
|
||||
|
||||
.form-item {
|
||||
.el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
|
||||
.form-item-label {
|
||||
.editable-checkbox {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.form-item-label-text.required {
|
||||
&:before {
|
||||
content: "*";
|
||||
color: $red;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.form-item >>> .form-item-content > .el-select,
|
||||
.form-item >>> .form-item-content > .el-autocomplete,
|
||||
.form-item >>> .form-item-content > .el-input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="form-readonly-value">
|
||||
{{ value }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FormReadonlyValue',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props: FormReadonlyValueProps, {emit}) {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables";
|
||||
|
||||
.form-readonly-value {
|
||||
font-size: 14px;
|
||||
color: $infoColor;
|
||||
}
|
||||
</style>
|
||||
@@ -1,125 +0,0 @@
|
||||
<template>
|
||||
<div class="form-table">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
hide-footer
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, h, inject, PropType, Ref} from 'vue';
|
||||
import {emptyArrayFunc} from '@/utils/func';
|
||||
import Table from '@/components/table/Table.vue';
|
||||
import FormTableField from '@/components/form/FormTableField.vue';
|
||||
import {TABLE_COLUMN_NAME_ACTIONS} from '@/constants/table';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FormTable',
|
||||
components: {
|
||||
Table,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<TableData>,
|
||||
required: false,
|
||||
default: emptyArrayFunc,
|
||||
},
|
||||
fields: {
|
||||
type: Array as PropType<FormTableField[]>,
|
||||
required: false,
|
||||
default: emptyArrayFunc,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'add',
|
||||
'clone',
|
||||
'delete',
|
||||
'field-change',
|
||||
'field-register',
|
||||
],
|
||||
setup(props: FormTableProps, {emit}) {
|
||||
const columns = computed<TableColumns>(() => {
|
||||
const {fields} = props;
|
||||
const formRules = inject<FormRuleItem[]>('form-rules');
|
||||
const columns = fields.map(f => {
|
||||
const {
|
||||
prop,
|
||||
label,
|
||||
width,
|
||||
fieldType,
|
||||
options,
|
||||
required,
|
||||
placeholder,
|
||||
disabled,
|
||||
} = f;
|
||||
return {
|
||||
key: prop,
|
||||
label,
|
||||
width,
|
||||
required,
|
||||
value: (row: BaseModel, rowIndex: number) => h(FormTableField, {
|
||||
form: row,
|
||||
formRules,
|
||||
prop,
|
||||
fieldType,
|
||||
options,
|
||||
required,
|
||||
placeholder,
|
||||
disabled: typeof disabled === 'function' ? disabled(row) : disabled,
|
||||
onChange: (value: any) => {
|
||||
emit('field-change', rowIndex, prop, value);
|
||||
},
|
||||
onRegister: (formRef: Ref) => {
|
||||
if (rowIndex < 0) return;
|
||||
emit('field-register', rowIndex, prop, formRef);
|
||||
},
|
||||
} as FormTableFieldProps, emptyArrayFunc),
|
||||
} as TableColumn;
|
||||
}) as TableColumns;
|
||||
columns.push({
|
||||
key: TABLE_COLUMN_NAME_ACTIONS,
|
||||
label: 'Actions',
|
||||
width: '150',
|
||||
// fixed: 'right',
|
||||
buttons: [
|
||||
{
|
||||
type: 'primary',
|
||||
icon: ['fa', 'plus'],
|
||||
tooltip: 'Add',
|
||||
onClick: (_, rowIndex) => {
|
||||
emit('add', rowIndex);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
icon: ['fa', 'clone'],
|
||||
tooltip: 'Clone',
|
||||
onClick: (_, rowIndex) => {
|
||||
emit('clone', rowIndex);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
icon: ['fa', 'trash-alt'],
|
||||
tooltip: 'Delete',
|
||||
onClick: (_, rowIndex) => {
|
||||
emit('delete', rowIndex);
|
||||
},
|
||||
}
|
||||
]
|
||||
});
|
||||
return columns;
|
||||
});
|
||||
|
||||
return {
|
||||
columns,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,239 +0,0 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="form" :rules="computedFormRules" inline-message>
|
||||
<el-form-item ref="formItemRef" :prop="prop" :required="isRequired">
|
||||
<el-input
|
||||
v-if="fieldType === FORM_FIELD_TYPE_INPUT"
|
||||
v-model="internalValue"
|
||||
:placeholder="placeholder"
|
||||
size="mini"
|
||||
:disabled="disabled"
|
||||
@input="onInputChange"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="fieldType === FORM_FIELD_TYPE_INPUT_PASSWORD"
|
||||
v-model="internalValue"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
size="mini"
|
||||
type="password"
|
||||
@input="onInputChange"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="fieldType === FORM_FIELD_TYPE_INPUT_TEXTAREA"
|
||||
v-model="internalValue"
|
||||
:placeholder="placeholder"
|
||||
size="mini"
|
||||
type="textarea"
|
||||
:disabled="disabled"
|
||||
@input="onInputChange"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="fieldType === FORM_FIELD_TYPE_SELECT"
|
||||
v-model="internalValue"
|
||||
:placeholder="placeholder"
|
||||
size="mini"
|
||||
:disabled="disabled"
|
||||
@change="onInputChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="op in options"
|
||||
:key="op.value"
|
||||
:label="op.label"
|
||||
:value="op.value"
|
||||
/>
|
||||
</el-select>
|
||||
<InputWithButton
|
||||
v-else-if="fieldType === FORM_FIELD_TYPE_INPUT_WITH_BUTTON"
|
||||
v-model="internalValue"
|
||||
:placeholder="placeholder"
|
||||
button-label="Edit"
|
||||
size="mini"
|
||||
:disabled="disabled"
|
||||
@input="onInputChange"
|
||||
/>
|
||||
<TagInput
|
||||
v-else-if="fieldType === FORM_FIELD_TYPE_TAG_INPUT"
|
||||
v-model="internalValue"
|
||||
:disabled="disabled"
|
||||
@change="onInputChange"
|
||||
/>
|
||||
<Switch
|
||||
v-else-if="fieldType === FORM_FIELD_TYPE_SWITCH"
|
||||
v-model="internalValue"
|
||||
:disabled="disabled"
|
||||
@change="onInputChange"
|
||||
/>
|
||||
<!-- TODO: implement more field types -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
inject,
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
PropType,
|
||||
Ref,
|
||||
ref,
|
||||
SetupContext,
|
||||
watch
|
||||
} from 'vue';
|
||||
import {
|
||||
FORM_FIELD_TYPE_CHECK_TAG_GROUP,
|
||||
FORM_FIELD_TYPE_INPUT,
|
||||
FORM_FIELD_TYPE_INPUT_PASSWORD,
|
||||
FORM_FIELD_TYPE_INPUT_TEXTAREA,
|
||||
FORM_FIELD_TYPE_INPUT_WITH_BUTTON,
|
||||
FORM_FIELD_TYPE_SELECT,
|
||||
FORM_FIELD_TYPE_SWITCH,
|
||||
FORM_FIELD_TYPE_TAG_INPUT,
|
||||
FORM_FIELD_TYPE_TAG_SELECT,
|
||||
} from '@/constants/form';
|
||||
import TagInput from '@/components/input/TagInput.vue';
|
||||
import {emptyArrayFunc, voidFunc} from '@/utils/func';
|
||||
import {ElForm, ElFormItem} from 'element-plus';
|
||||
import InputWithButton from '@/components/input/InputWithButton.vue';
|
||||
import Switch from '@/components/switch/Switch.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FormTableField',
|
||||
components: {
|
||||
Switch,
|
||||
InputWithButton,
|
||||
TagInput,
|
||||
},
|
||||
props: {
|
||||
form: {
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
formRules: {
|
||||
type: Object as PropType<FormRuleItem[]>,
|
||||
required: false,
|
||||
},
|
||||
prop: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fieldType: {
|
||||
type: String as PropType<FormFieldType>,
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<SelectOption[]>,
|
||||
default: emptyArrayFunc,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Please enter value',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onChange: {
|
||||
type: Function as PropType<(value: any) => void>,
|
||||
default: voidFunc,
|
||||
},
|
||||
onRegister: {
|
||||
type: Function as PropType<(formRef: Ref) => void>,
|
||||
default: voidFunc,
|
||||
}
|
||||
},
|
||||
setup(props: FormTableFieldProps, {emit}: SetupContext) {
|
||||
// form ref
|
||||
const formRef = ref<typeof ElForm>();
|
||||
|
||||
// form item ref
|
||||
const formItemRef = ref<typeof ElFormItem>();
|
||||
|
||||
// internal value
|
||||
const internalValue = ref<any>();
|
||||
|
||||
// computed field value
|
||||
const fieldValue = computed(() => {
|
||||
const {form, prop} = props;
|
||||
return form[prop];
|
||||
});
|
||||
watch(() => fieldValue.value, () => {
|
||||
if (internalValue.value !== fieldValue.value) {
|
||||
internalValue.value = fieldValue.value;
|
||||
}
|
||||
});
|
||||
|
||||
const onInputChange = (value: any) => {
|
||||
const {onChange} = props;
|
||||
onChange?.(value);
|
||||
};
|
||||
|
||||
const isEmptyForm = inject('fn:isEmptyForm') as (d: any) => boolean;
|
||||
|
||||
const isRequired = computed<boolean>(() => {
|
||||
const {form, required} = props;
|
||||
if (isEmptyForm(form)) return false;
|
||||
return required || false;
|
||||
});
|
||||
|
||||
const isErrorMessageVisible = computed<boolean>(() => !!formItemRef.value?.validateMessage);
|
||||
|
||||
const computedFormRules = computed<FormRuleItem[]>(() => {
|
||||
const {form, formRules} = props;
|
||||
if (isEmptyForm(form)) {
|
||||
return [];
|
||||
} else {
|
||||
return formRules || [];
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
const {form, prop} = props;
|
||||
|
||||
// initialize internal value
|
||||
internalValue.value = form[prop];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const {onRegister} = props;
|
||||
|
||||
// register form ref
|
||||
onRegister?.(formRef);
|
||||
});
|
||||
|
||||
return {
|
||||
FORM_FIELD_TYPE_INPUT,
|
||||
FORM_FIELD_TYPE_INPUT_PASSWORD,
|
||||
FORM_FIELD_TYPE_INPUT_TEXTAREA,
|
||||
FORM_FIELD_TYPE_INPUT_WITH_BUTTON,
|
||||
FORM_FIELD_TYPE_SELECT,
|
||||
FORM_FIELD_TYPE_TAG_INPUT,
|
||||
FORM_FIELD_TYPE_TAG_SELECT,
|
||||
FORM_FIELD_TYPE_CHECK_TAG_GROUP,
|
||||
FORM_FIELD_TYPE_SWITCH,
|
||||
formRef,
|
||||
formItemRef,
|
||||
internalValue,
|
||||
onInputChange,
|
||||
isRequired,
|
||||
isErrorMessageVisible,
|
||||
computedFormRules,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-form {
|
||||
margin: 0;
|
||||
|
||||
.el-form-item {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,269 +0,0 @@
|
||||
import {computed, provide} from 'vue';
|
||||
import {Store} from 'vuex';
|
||||
import useFormTable from '@/components/form/formTable';
|
||||
import {EMPTY_OBJECT_ID} from '@/utils/mongo';
|
||||
|
||||
const useForm = (ns: ListStoreNamespace, store: Store<RootStoreState>, services: Services<BaseModel>, data: FormComponentData<BaseModel>) => {
|
||||
const {
|
||||
form: newForm,
|
||||
formRef,
|
||||
formTableFieldRefsMap,
|
||||
} = data;
|
||||
|
||||
const getNewForm = () => {
|
||||
return {...newForm.value};
|
||||
};
|
||||
|
||||
const getNewFormList = () => {
|
||||
const list = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
list.push(getNewForm());
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
// store state
|
||||
const state = store.state[ns];
|
||||
|
||||
// form
|
||||
const form = computed<BaseModel>(() => state.form);
|
||||
|
||||
// form list
|
||||
const formList = computed<BaseModel[]>(() => state.formList);
|
||||
|
||||
// active dialog key
|
||||
const activeDialogKey = computed<DialogKey | undefined>(() => state.activeDialogKey);
|
||||
|
||||
// is selective form
|
||||
const isSelectiveForm = computed<boolean>(() => state.isSelectiveForm);
|
||||
|
||||
// selected form fields
|
||||
const selectedFormFields = computed<string[]>(() => state.selectedFormFields);
|
||||
|
||||
// readonly form fields
|
||||
const readonlyFormFields = computed<string[]>(() => state.readonlyFormFields);
|
||||
|
||||
// is batch form getters
|
||||
const isBatchForm = computed<boolean>(() => store.getters[`${ns}/isBatchForm`]);
|
||||
|
||||
// form list ids getters
|
||||
const formListIds = computed<string[]>(() => store.getters[`${ns}/formListIds`]);
|
||||
|
||||
const validateForm = async () => {
|
||||
if (isBatchForm.value && activeDialogKey.value === 'create') {
|
||||
let valid = true;
|
||||
for (const formRef of formTableFieldRefsMap.value.values()) {
|
||||
try {
|
||||
await formRef.value?.validate?.();
|
||||
} catch (e) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
} else {
|
||||
return await formRef.value?.validate();
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
if (activeDialogKey.value) {
|
||||
switch (activeDialogKey.value) {
|
||||
case 'create':
|
||||
store.commit(`${ns}/setForm`, getNewForm());
|
||||
store.commit(`${ns}/setFormList`, getNewFormList());
|
||||
break;
|
||||
case 'edit':
|
||||
// store.commit(`${ns}/setForm`, plainClone(state.form))
|
||||
formRef.value?.clearValidate();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields();
|
||||
formTableFieldRefsMap.value = new Map();
|
||||
}
|
||||
};
|
||||
|
||||
// whether form item is disabled
|
||||
const isFormItemDisabled = (prop: string) => {
|
||||
if (readonlyFormFields.value.includes(prop)) {
|
||||
return true;
|
||||
}
|
||||
if (!isSelectiveForm.value) return false;
|
||||
if (!prop) return false;
|
||||
return !selectedFormFields.value.includes(prop);
|
||||
};
|
||||
|
||||
// whether the form is empty
|
||||
const isEmptyForm = (d: any): boolean => {
|
||||
return JSON.stringify(d) === JSON.stringify(getNewForm());
|
||||
};
|
||||
provide<(d: any) => boolean>('fn:isEmptyForm', isEmptyForm);
|
||||
|
||||
// all list select options
|
||||
const allListSelectOptions = computed<SelectOption[]>(() => store.getters[`${ns}/allListSelectOptions`]);
|
||||
|
||||
// all list select options with empty
|
||||
const allListSelectOptionsWithEmpty = computed<SelectOption[]>(() => allListSelectOptions.value.concat({
|
||||
label: 'Unassigned',
|
||||
value: EMPTY_OBJECT_ID,
|
||||
}));
|
||||
|
||||
// all dict
|
||||
const allDict = computed<Map<string, BaseModel>>(() => store.getters[`${ns}/allDict`]);
|
||||
|
||||
// all tags
|
||||
const allTags = computed<string[]>(() => store.getters[`${ns}/allTags`]);
|
||||
|
||||
const {
|
||||
getList,
|
||||
create,
|
||||
updateById,
|
||||
createList,
|
||||
updateList,
|
||||
} = services;
|
||||
|
||||
// dialog create edit
|
||||
const createEditDialogVisible = computed<boolean>(() => {
|
||||
const {activeDialogKey} = state;
|
||||
if (!activeDialogKey) return false;
|
||||
return ['create', 'edit'].includes(activeDialogKey);
|
||||
});
|
||||
|
||||
// dialog create edit tab name
|
||||
const createEditDialogTabName = computed<CreateEditTabName>(() => state.createEditDialogTabName);
|
||||
|
||||
// dialog confirm
|
||||
const confirmDisabled = computed<boolean>(() => {
|
||||
return isSelectiveForm.value &&
|
||||
selectedFormFields.value.length === 0;
|
||||
});
|
||||
const confirmLoading = computed<boolean>(() => state.confirmLoading);
|
||||
const setConfirmLoading = (value: boolean) => store.commit(`${ns}/setConfirmLoading`, value);
|
||||
const onConfirm = async () => {
|
||||
// validate
|
||||
try {
|
||||
const valid = await validateForm();
|
||||
if (!valid) return;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
return;
|
||||
}
|
||||
if (!form.value) {
|
||||
console.error(new Error('form is undefined'));
|
||||
return;
|
||||
}
|
||||
|
||||
// flag of request finished
|
||||
let isRequestFinished = false;
|
||||
|
||||
// start loading
|
||||
setTimeout(() => {
|
||||
if (isRequestFinished) return;
|
||||
setConfirmLoading(true);
|
||||
}, 50);
|
||||
|
||||
// request
|
||||
try {
|
||||
let res: HttpResponse;
|
||||
switch (activeDialogKey.value) {
|
||||
case 'create':
|
||||
if (isBatchForm.value) {
|
||||
const changedFormList = formList.value.filter(d => !isEmptyForm(d));
|
||||
res = await createList(changedFormList);
|
||||
} else {
|
||||
res = await create(form.value);
|
||||
}
|
||||
break;
|
||||
case 'edit':
|
||||
if (isBatchForm.value) {
|
||||
res = await updateList(formListIds.value, form.value, selectedFormFields.value);
|
||||
} else {
|
||||
res = await updateById(form.value._id as string, form.value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error(`activeDialogKey "${activeDialogKey.value}" is invalid`);
|
||||
return;
|
||||
}
|
||||
if (res.error) {
|
||||
console.error(res.error);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
// flag request finished as true
|
||||
isRequestFinished = true;
|
||||
|
||||
// stop loading
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
|
||||
// close
|
||||
store.commit(`${ns}/hideDialog`);
|
||||
|
||||
// request list
|
||||
await getList();
|
||||
};
|
||||
|
||||
// dialog close
|
||||
const onClose = () => {
|
||||
store.commit(`${ns}/hideDialog`);
|
||||
};
|
||||
|
||||
// dialog tab change
|
||||
const onTabChange = (tabName: CreateEditTabName) => {
|
||||
// if (tabName === 'batch') {
|
||||
// store.commit(`${ns}/setFormList`, getNewFormList());
|
||||
// }
|
||||
store.commit(`${ns}/setCreateEditDialogTabName`, tabName);
|
||||
};
|
||||
|
||||
// use form table
|
||||
const formTable = useFormTable(ns, store, services, data);
|
||||
const {
|
||||
onAdd,
|
||||
onClone,
|
||||
onDelete,
|
||||
onFieldChange,
|
||||
onFieldRegister,
|
||||
} = formTable;
|
||||
|
||||
// action functions
|
||||
const actionFunctions = {
|
||||
onClose,
|
||||
onConfirm,
|
||||
onTabChange,
|
||||
onAdd,
|
||||
onClone,
|
||||
onDelete,
|
||||
onFieldChange,
|
||||
onFieldRegister,
|
||||
} as CreateEditDialogActionFunctions;
|
||||
|
||||
return {
|
||||
...formTable,
|
||||
getNewForm,
|
||||
getNewFormList,
|
||||
form,
|
||||
formRef,
|
||||
isSelectiveForm,
|
||||
selectedFormFields,
|
||||
formList,
|
||||
isBatchForm,
|
||||
validateForm,
|
||||
resetForm,
|
||||
isFormItemDisabled,
|
||||
activeDialogKey,
|
||||
createEditDialogTabName,
|
||||
createEditDialogVisible,
|
||||
allListSelectOptions,
|
||||
allListSelectOptionsWithEmpty,
|
||||
allDict,
|
||||
allTags,
|
||||
confirmDisabled,
|
||||
confirmLoading,
|
||||
setConfirmLoading,
|
||||
actionFunctions,
|
||||
};
|
||||
};
|
||||
|
||||
export default useForm;
|
||||
@@ -1,67 +0,0 @@
|
||||
import {Store} from 'vuex';
|
||||
import {plainClone} from '@/utils/object';
|
||||
import {computed, Ref} from 'vue';
|
||||
|
||||
const useFormTable = (ns: ListStoreNamespace, store: Store<RootStoreState>, services: Services<BaseModel>, data: FormComponentData<BaseModel>) => {
|
||||
const {
|
||||
form,
|
||||
formTableFieldRefsMap,
|
||||
} = data;
|
||||
|
||||
// state
|
||||
const state = store.state[ns];
|
||||
|
||||
// form list
|
||||
const formList = computed(() => state.formList);
|
||||
|
||||
const getNewForm = () => {
|
||||
return {...form.value};
|
||||
};
|
||||
|
||||
const onAdd = (index: number) => {
|
||||
formList.value.splice(index, 0, getNewForm());
|
||||
};
|
||||
|
||||
const onClone = (index: number) => {
|
||||
const form = plainClone(formList.value[index]);
|
||||
formList.value.splice(index, 0, form);
|
||||
};
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
formList.value.splice(index, 1);
|
||||
for (const key of formTableFieldRefsMap.value.keys()) {
|
||||
const rowIndex = key[0];
|
||||
if (rowIndex === index) {
|
||||
formTableFieldRefsMap.value.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onFieldChange = (rowIndex: number, prop: string, value: any) => {
|
||||
if (rowIndex !== -1) {
|
||||
// one row change
|
||||
const item = formList.value[rowIndex] as BaseModel;
|
||||
item[prop] = value;
|
||||
} else {
|
||||
// all rows change
|
||||
for (let i = 0; i < formList.value.length; i++) {
|
||||
onFieldChange(i, prop, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onFieldRegister = (rowIndex: number, prop: string, formRef: Ref) => {
|
||||
const key = [rowIndex, prop] as FormTableFieldRefsMapKey;
|
||||
formTableFieldRefsMap.value.set(key, formRef);
|
||||
};
|
||||
|
||||
return {
|
||||
onAdd,
|
||||
onClone,
|
||||
onDelete,
|
||||
onFieldChange,
|
||||
onFieldRegister,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFormTable;
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<span class="atom-material-icon" v-html="html"/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import {getFileIconFromName, getFolderIconFromName} from 'atom-material-icons';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AtomMaterialIcon',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDir: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
setup(props: AtomMaterialIconProps) {
|
||||
const html = computed<string>(() => {
|
||||
const {name, isDir} = props;
|
||||
const icon = isDir ? getFolderIconFromName(name) : getFileIconFromName(name);
|
||||
return icon.default;
|
||||
});
|
||||
|
||||
return {
|
||||
html,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<template v-if="icon">
|
||||
<font-awesome-icon
|
||||
v-if="isFaIcon"
|
||||
:class="spinning ? 'fa-spin' : ''"
|
||||
:icon="icon"
|
||||
:style="{fontSize}"
|
||||
class="icon"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
:class="[spinning ? 'fa-spin' : '', icon, 'icon']"
|
||||
:style="{fontSize}"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from 'vue';
|
||||
import useIcon from '@/components/icon/icon';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
props: {
|
||||
icon: {
|
||||
type: [String, Array] as PropType<Icon>,
|
||||
},
|
||||
spinning: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<IconSize>,
|
||||
default: 'mini',
|
||||
}
|
||||
},
|
||||
setup(props: IconProps, {emit}) {
|
||||
const {
|
||||
isFaIcon: _isFaIcon,
|
||||
getFontSize,
|
||||
} = useIcon();
|
||||
|
||||
const fontSize = computed(() => {
|
||||
const {size} = props;
|
||||
return getFontSize(size);
|
||||
});
|
||||
|
||||
const isFaIcon = computed<boolean>(() => {
|
||||
const {icon} = props;
|
||||
if (!icon) return false;
|
||||
return _isFaIcon(icon);
|
||||
});
|
||||
|
||||
return {
|
||||
isFaIcon,
|
||||
fontSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<template v-if="!item || !item.icon">
|
||||
<i :style="{'font-size': fontSize}" class="menu-item-icon fa fa-circle-o"></i>
|
||||
</template>
|
||||
<template v-else-if="Array.isArray(item.icon)">
|
||||
<font-awesome-icon
|
||||
:icon="item.icon"
|
||||
:style="{'font-size': fontSize}"
|
||||
class="menu-item-icon"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i :class="item.icon" :style="{'font-size': fontSize}" class="menu-item-icon"></i>
|
||||
</template>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from 'vue';
|
||||
import useIcon from '@/components/icon/icon';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MenuItemIcon',
|
||||
props: {
|
||||
item: {
|
||||
type: Object as PropType<MenuItem>,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<IconSize>,
|
||||
default: 'mini',
|
||||
}
|
||||
},
|
||||
setup(props: MenuItemIconProps) {
|
||||
const {
|
||||
getFontSize,
|
||||
} = useIcon();
|
||||
|
||||
const fontSize = computed(() => {
|
||||
const {size} = props as MenuItemIconProps;
|
||||
return getFontSize(size);
|
||||
});
|
||||
|
||||
return {
|
||||
fontSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.menu-item-icon {
|
||||
width: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +0,0 @@
|
||||
const useIcon = () => {
|
||||
// implementation
|
||||
const isFaIcon = (icon: Icon) => {
|
||||
if (Array.isArray(icon)) {
|
||||
return icon.length > 0 && icon[0].substr(0, 2) === 'fa';
|
||||
} else {
|
||||
return icon?.substr(0, 2) === 'fa';
|
||||
}
|
||||
};
|
||||
|
||||
const getFontSize = (size: IconSize) => {
|
||||
switch (size) {
|
||||
case 'large':
|
||||
return '24px';
|
||||
case 'normal':
|
||||
return '16px';
|
||||
case 'small':
|
||||
return '12px';
|
||||
case 'mini':
|
||||
return '10px';
|
||||
default:
|
||||
return size || '16px';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// public variables and methods
|
||||
isFaIcon,
|
||||
getFontSize,
|
||||
};
|
||||
};
|
||||
|
||||
export default useIcon;
|
||||
@@ -1,188 +0,0 @@
|
||||
<template>
|
||||
<div class="input-with-button">
|
||||
<!-- Input -->
|
||||
<el-input
|
||||
v-model="internalValue"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
class="input"
|
||||
:disabled="disabled"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@keyup.enter="onBlur"
|
||||
/>
|
||||
<!-- ./Input -->
|
||||
|
||||
<!-- Button -->
|
||||
<Button
|
||||
v-if="buttonLabel"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:type="buttonType"
|
||||
class="button"
|
||||
no-margin
|
||||
@click="onClick"
|
||||
>
|
||||
<Icon v-if="buttonIcon" :icon="buttonIcon" />
|
||||
{{ buttonLabel }}
|
||||
</Button>
|
||||
<template v-else-if="buttonIcon">
|
||||
<FaIconButton
|
||||
v-if="isFaIcon"
|
||||
:disabled="disabled"
|
||||
:icon="buttonIcon"
|
||||
:size="size"
|
||||
:type="buttonType"
|
||||
class="button"
|
||||
@click="onClick"
|
||||
/>
|
||||
<IconButton
|
||||
v-else
|
||||
:disabled="disabled"
|
||||
:icon="buttonIcon"
|
||||
:size="size"
|
||||
:type="buttonType"
|
||||
class="button"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
<!-- ./Button -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onMounted, PropType, ref, watch} from 'vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
import Icon from '@/components/icon/Icon.vue';
|
||||
import FaIconButton from '@/components/button/FaIconButton.vue';
|
||||
import useIcon from '@/components/icon/icon';
|
||||
import IconButton from '@/components/button/IconButton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InputWithButton',
|
||||
components: {
|
||||
IconButton,
|
||||
FaIconButton,
|
||||
Icon,
|
||||
Button,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'mini',
|
||||
},
|
||||
buttonType: {
|
||||
type: String as PropType<BasicType>,
|
||||
default: 'primary',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: 'Click',
|
||||
},
|
||||
buttonIcon: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:model-value',
|
||||
'input',
|
||||
'click',
|
||||
'blur',
|
||||
'focus',
|
||||
'keyup.enter',
|
||||
],
|
||||
setup(props: InputWithButtonProps, {emit}) {
|
||||
const internalValue = ref<string>();
|
||||
|
||||
const {
|
||||
isFaIcon: _isFaIcon,
|
||||
} = useIcon();
|
||||
|
||||
const isFaIcon = () => {
|
||||
const {buttonIcon} = props;
|
||||
if (!buttonIcon) return false;
|
||||
return _isFaIcon(buttonIcon);
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
internalValue.value = props.modelValue;
|
||||
});
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:model-value', value);
|
||||
emit('input', value);
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
emit('focus');
|
||||
};
|
||||
|
||||
const onKeyUpEnter = () => {
|
||||
emit('keyup.enter');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const {modelValue} = props;
|
||||
internalValue.value = modelValue;
|
||||
});
|
||||
|
||||
return {
|
||||
internalValue,
|
||||
isFaIcon,
|
||||
onClick,
|
||||
onInput,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onKeyUpEnter,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-with-button {
|
||||
display: inline-table;
|
||||
vertical-align: middle;
|
||||
//align-items: start;
|
||||
|
||||
.input {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.input-with-button >>> .input.el-input .el-input__inner {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-with-button >>> .button .el-button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
height: 28px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,228 +0,0 @@
|
||||
<template>
|
||||
<div class="tag-input">
|
||||
<template v-for="(item, $index) in selectedValue" :key="$index">
|
||||
<TagInputItem
|
||||
v-if="item.isEdit"
|
||||
ref="inputItemRef"
|
||||
v-model="selectedValue[$index]"
|
||||
:disabled="disabled"
|
||||
placeholder="Tag Name"
|
||||
size="mini"
|
||||
@blur="onBlur($index, $event)"
|
||||
@check="onCheck($index, $event)"
|
||||
@close="onClose($index, $event)"
|
||||
@delete="onDelete($index, $event)"
|
||||
@focus="onFocus($index, $event)"
|
||||
/>
|
||||
<Tag
|
||||
v-else
|
||||
:closable="!disabled"
|
||||
:color="item.color"
|
||||
:disabled="disabled"
|
||||
:label="item.name"
|
||||
clickable
|
||||
size="small"
|
||||
type="plain"
|
||||
@click="onEdit($index, $event)"
|
||||
@close="onDelete($index, $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<el-tooltip :content="addButtonTooltip" :disabled="!addButtonTooltip">
|
||||
<Tab
|
||||
:icon="['fa', 'plus']"
|
||||
:show-close="false"
|
||||
:show-title="false"
|
||||
class="add-btn"
|
||||
:class="disabled ? 'disabled' : ''"
|
||||
@click="onAdd"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType, ref, watch} from 'vue';
|
||||
import TagComp from '@/components/tag/Tag.vue';
|
||||
import Tab from '@/components/tab/Tab.vue';
|
||||
import TagInputItem from '@/components/input/TagInputItem.vue';
|
||||
import {cloneArray} from '@/utils/object';
|
||||
import {getNewTag} from '@/components/tag/tag';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TagInput',
|
||||
components: {
|
||||
TagInputItem,
|
||||
Tag: TagComp,
|
||||
Tab,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Tag[]>,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'change',
|
||||
'update:model-value',
|
||||
],
|
||||
setup(props: TagInputProps, {emit}) {
|
||||
const activeIndex = ref<number>(-1);
|
||||
const inputItemRef = ref<typeof TagInputItem>();
|
||||
|
||||
const selectedValue = ref<TagInputOption[]>([]);
|
||||
|
||||
const emitValue = () => {
|
||||
emit('change', selectedValue.value);
|
||||
emit('update:model-value', selectedValue.value.map(d => {
|
||||
return {
|
||||
_id: d._id,
|
||||
name: d.name,
|
||||
color: d.color,
|
||||
} as Tag;
|
||||
}));
|
||||
};
|
||||
|
||||
const disabled = computed<boolean>(() => props.disabled);
|
||||
|
||||
const addButtonTooltip = computed<string>(() => disabled.value ? '' : 'Add Tag');
|
||||
|
||||
const onEdit = (index: number, ev?: Event) => {
|
||||
// check disabled
|
||||
if (disabled.value) return;
|
||||
|
||||
ev?.stopPropagation();
|
||||
const item = selectedValue.value[index];
|
||||
item.isEdit = true;
|
||||
|
||||
// auto focus
|
||||
setTimeout(() => inputItemRef.value?.focus(), 0);
|
||||
};
|
||||
|
||||
const onDelete = (index: number, ev?: Event) => {
|
||||
// check disabled
|
||||
if (disabled.value) return;
|
||||
|
||||
ev?.stopPropagation();
|
||||
selectedValue.value.splice(index, 1);
|
||||
|
||||
// commit change
|
||||
emitValue();
|
||||
};
|
||||
|
||||
const onFocus = (index: number, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
activeIndex.value = index;
|
||||
};
|
||||
|
||||
const onBlur = (index: number, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
activeIndex.value = -1;
|
||||
};
|
||||
|
||||
const onCheck = (index: number, value?: Tag, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
const item = selectedValue.value[index];
|
||||
if (!item) return;
|
||||
item.isEdit = false;
|
||||
if (!value) return;
|
||||
const {name, hex} = value;
|
||||
item.name = name;
|
||||
item.hex = hex;
|
||||
|
||||
// commit change
|
||||
emitValue();
|
||||
};
|
||||
|
||||
const onClose = (index: number, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
const item = selectedValue.value[index];
|
||||
if (!item) return;
|
||||
item.isEdit = false;
|
||||
if (!item.name) {
|
||||
selectedValue.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
// check disabled
|
||||
if (disabled.value) return;
|
||||
|
||||
// add value to array
|
||||
selectedValue.value.push({
|
||||
...getNewTag(),
|
||||
isEdit: true,
|
||||
});
|
||||
|
||||
// auto focus
|
||||
setTimeout(() => inputItemRef.value?.focus(), 0);
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
const modelValue = props.modelValue || [];
|
||||
selectedValue.value = cloneArray(modelValue);
|
||||
});
|
||||
|
||||
return {
|
||||
inputItemRef,
|
||||
selectedValue,
|
||||
addButtonTooltip,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCheck,
|
||||
onClose,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.tag-input {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
|
||||
.tag-input-item {
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:not(.disabled) {
|
||||
background-color: $white;
|
||||
color: $infoMediumColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.tag-input >>> .tag {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,353 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
isFocus ? 'is-focus' : '',
|
||||
isNew ? 'is-new' : '',
|
||||
]"
|
||||
class="tag-input-item"
|
||||
>
|
||||
<!-- Input -->
|
||||
<div class="input-wrapper">
|
||||
<el-autocomplete
|
||||
ref="inputRef"
|
||||
v-model="internalValue.name"
|
||||
:disabled="disabled"
|
||||
:fetch-suggestions="fetchSuggestions"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
popper-class="tag-input-item-popper"
|
||||
class="input"
|
||||
value-key="name"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@select="onSelect"
|
||||
@keyup.enter="onCheck"
|
||||
/>
|
||||
<div class="actions">
|
||||
<font-awesome-icon
|
||||
:class="[isDisabled('check') ? 'disabled' : '']"
|
||||
:icon="['fa', 'check']"
|
||||
class="action-btn check"
|
||||
@click="onCheck"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
:class="[isDisabled('close') ? 'disabled' : '']"
|
||||
:icon="['fa', 'times']"
|
||||
class="action-btn close"
|
||||
@click="onClose"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
:class="[isDisabled('delete') ? 'disabled' : '']"
|
||||
:icon="['fa', 'trash']"
|
||||
class="action-btn delete"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ./Input -->
|
||||
|
||||
<!-- Color Picker -->
|
||||
<ColorPicker
|
||||
v-model="internalValue.color"
|
||||
:disabled="!isNew"
|
||||
:predefine="predefinedColors"
|
||||
class="color-picker"
|
||||
show-alpha
|
||||
/>
|
||||
<!-- <el-color-picker-->
|
||||
<!-- v-model="internalValue.color"-->
|
||||
<!-- :disabled="!isNew"-->
|
||||
<!-- :predefine="predefinedColors"-->
|
||||
<!-- class="color-picker"-->
|
||||
<!-- show-alpha-->
|
||||
<!-- />-->
|
||||
<!-- ./Color Picker -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, inject, onMounted, PropType, readonly, ref, watch} from 'vue';
|
||||
import {ElInput} from 'element-plus';
|
||||
import {plainClone} from '@/utils/object';
|
||||
import useTagService from '@/services/tag/tagService';
|
||||
import {useStore} from 'vuex';
|
||||
import {FILTER_OP_CONTAINS, FILTER_OP_EQUAL} from '@/constants/filter';
|
||||
import {getNewTag} from '@/components/tag/tag';
|
||||
import {getPredefinedColors} from '@/utils/color';
|
||||
import ColorPicker from '@/components/color/ColorPicker.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TagInputItem',
|
||||
components: {
|
||||
ColorPicker,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as PropType<Tag>,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<BasicSize>,
|
||||
default: 'mini',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:model-value',
|
||||
'input',
|
||||
'click',
|
||||
'blur',
|
||||
'focus',
|
||||
'keyup.enter',
|
||||
'close',
|
||||
'check',
|
||||
'delete',
|
||||
],
|
||||
setup(props: TagInputItemProps, {emit}) {
|
||||
const store = useStore();
|
||||
|
||||
const internalValue = ref<Tag>(getNewTag());
|
||||
|
||||
const isFocus = ref<boolean>(false);
|
||||
|
||||
const inputRef = ref<typeof ElInput>();
|
||||
|
||||
const isNew = computed<boolean>(() => !internalValue.value._id);
|
||||
|
||||
// predefined colors
|
||||
const predefinedColors = readonly<string[]>(getPredefinedColors());
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
if (!props.modelValue) {
|
||||
internalValue.value = getNewTag();
|
||||
} else {
|
||||
internalValue.value = plainClone(props.modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
const isDisabled = (key: string) => {
|
||||
switch (key) {
|
||||
case 'check':
|
||||
return !internalValue.value.name;
|
||||
case 'close':
|
||||
return false;
|
||||
case 'delete':
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onInput = (name: string) => {
|
||||
const value = {...props.modelValue, name};
|
||||
emit('input', value);
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
isFocus.value = false;
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
isFocus.value = true;
|
||||
emit('focus');
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
inputRef.value?.focus();
|
||||
};
|
||||
|
||||
const onSelect = (value: Tag) => {
|
||||
internalValue.value = value;
|
||||
};
|
||||
|
||||
const onCheck = () => {
|
||||
if (isDisabled('check')) return;
|
||||
emit('update:model-value', internalValue.value);
|
||||
emit('check', internalValue.value);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
if (isDisabled('close')) return;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
if (isDisabled('delete')) return;
|
||||
emit('delete');
|
||||
};
|
||||
|
||||
const ctx = inject<ListStoreContext<BaseModel>>('store-context');
|
||||
|
||||
const fetchSuggestions = async (queryString: string, callback: (data: Tag[]) => void) => {
|
||||
const {
|
||||
getList,
|
||||
} = useTagService(store);
|
||||
const params = {
|
||||
page: 1,
|
||||
size: 50,
|
||||
conditions: [
|
||||
{key: 'col', op: FILTER_OP_EQUAL, value: `${ctx?.namespace}s`}
|
||||
]
|
||||
} as ListRequestParams;
|
||||
if (queryString) {
|
||||
const conditions = params.conditions as FilterConditionData[];
|
||||
conditions.push({key: 'name', op: FILTER_OP_CONTAINS, value: queryString});
|
||||
}
|
||||
try {
|
||||
const res = await getList(params);
|
||||
return callback(res.data || []);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
callback([]);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.modelValue) {
|
||||
internalValue.value = getNewTag();
|
||||
} else {
|
||||
internalValue.value = plainClone(props.modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
predefinedColors,
|
||||
internalValue,
|
||||
isFocus,
|
||||
inputRef,
|
||||
isNew,
|
||||
onClick,
|
||||
onInput,
|
||||
onBlur,
|
||||
onFocus,
|
||||
focus,
|
||||
onCheck,
|
||||
onClose,
|
||||
onDelete,
|
||||
onSelect,
|
||||
isDisabled,
|
||||
fetchSuggestions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.tag-input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
//height: 28px;
|
||||
|
||||
.input-wrapper {
|
||||
display: inherit;
|
||||
border: none;
|
||||
position: relative;
|
||||
height: 28px;
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
|
||||
.action-btn {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
padding: 3px;
|
||||
color: $infoMediumColor;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
&.check {
|
||||
color: $successColor;
|
||||
}
|
||||
|
||||
&.close {
|
||||
color: $infoColor;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
color: $dangerColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $infoMediumLightColor;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.tag-input-item >>> .input,
|
||||
.tag-input-item >>> .actions,
|
||||
.tag-input-item >>> .color-picker,
|
||||
.tag-input-item >>> .color-picker .el-color-picker {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .input {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .input .el-input__inner {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .color-picker .el-color-picker__trigger {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: none;
|
||||
border-top: 1px solid #DCDFE6;
|
||||
border-right: 1px solid #DCDFE6;
|
||||
border-bottom: 1px solid #DCDFE6;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tag-input-item.is-focus >>> .color-picker .el-color-picker__trigger {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .color-picker .el-color-picker__color {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .color-picker .el-color-picker__mask {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
left: 0;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .el-autocomplete-suggestion__list > li {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.tag-input-item-popper >>> .el-autocomplete-suggestion__list > li {
|
||||
height: 28px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<NavActionGroup>
|
||||
<NavActionButton
|
||||
:button-type="buttonType"
|
||||
:disabled="disabled"
|
||||
:icon="icon"
|
||||
:label="label"
|
||||
:tooltip="tooltip"
|
||||
type="primary"
|
||||
@click="() => $emit('click')"
|
||||
/>
|
||||
</NavActionGroup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
import NavActionGroup from '@/components/nav/NavActionGroup.vue';
|
||||
import NavActionButton from '@/components/nav/NavActionButton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavActionBack',
|
||||
components: {NavActionButton, NavActionGroup},
|
||||
props: {
|
||||
buttonType: {
|
||||
type: String as PropType<ButtonType>,
|
||||
default: 'label',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Back'
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array] as PropType<Icon>,
|
||||
default: () => {
|
||||
return ['fa', 'undo'];
|
||||
}
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<BasicType>,
|
||||
default: 'primary'
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<BasicSize>,
|
||||
default: 'mini'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<LabelButton
|
||||
v-if="buttonType === 'label'"
|
||||
:disabled="disabled"
|
||||
:icon="icon"
|
||||
:label="label"
|
||||
:size="size"
|
||||
:tooltip="tooltip"
|
||||
:type="type"
|
||||
@click="onClick"
|
||||
/>
|
||||
<FaIconButton
|
||||
v-else-if="buttonType === 'fa-icon'"
|
||||
:disabled="disabled"
|
||||
:icon="icon"
|
||||
:size="size"
|
||||
:tooltip="tooltip"
|
||||
:type="type"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
import {buttonProps} from '@/components/button/Button.vue';
|
||||
import LabelButton from '@/components/button/LabelButton.vue';
|
||||
import FaIconButton from '@/components/button/FaIconButton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavActionButton',
|
||||
components: {
|
||||
LabelButton,
|
||||
FaIconButton,
|
||||
},
|
||||
props: {
|
||||
buttonType: {
|
||||
type: String as PropType<ButtonType>,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array] as PropType<Icon>
|
||||
},
|
||||
onClick: {
|
||||
type: Function as PropType<() => void>
|
||||
},
|
||||
...buttonProps,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<NavActionItem class="nav-action-fa-icon" is-label>
|
||||
<el-tooltip :content="tooltip" :disabled="!tooltip">
|
||||
<font-awesome-icon :icon="icon" class="title"/>
|
||||
</el-tooltip>
|
||||
</NavActionItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
import NavActionItem from '@/components/nav/NavActionItem.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavActionFaIcon',
|
||||
components: {
|
||||
NavActionItem,
|
||||
},
|
||||
props: {
|
||||
tooltip: {
|
||||
type: String,
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array] as PropType<Icon>,
|
||||
},
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<div class="nav-action-group">
|
||||
<div class="border"/>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavActionGroup',
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.nav-action-group {
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
& + .nav-action-group {
|
||||
//padding-left: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
.border {
|
||||
margin-left: -10px;
|
||||
margin-right: 10px;
|
||||
border-left: 1px solid $navActionsGroupBorderColor;
|
||||
height: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.nav-action-group >>> .nav-action-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<NavActionGroup>
|
||||
<NavActionButton
|
||||
:disabled="disabled"
|
||||
:icon="['fa', 'undo']"
|
||||
button-type="label"
|
||||
label="Back"
|
||||
type="primary"
|
||||
@click="() => $emit('back')"
|
||||
/>
|
||||
<NavActionButton
|
||||
:disabled="disabled"
|
||||
:icon="['fa', 'save']"
|
||||
button-type="label"
|
||||
label="Save"
|
||||
type="success"
|
||||
@click="() => $emit('save')"
|
||||
/>
|
||||
</NavActionGroup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import NavActionGroup from '@/components/nav/NavActionGroup.vue';
|
||||
import NavActionButton from '@/components/nav/NavActionButton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavActionGroupDetailCommon',
|
||||
components: {NavActionButton, NavActionGroup},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'back',
|
||||
],
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div :class="[isLabel ? 'is-label' : '']" class="nav-action-item">
|
||||
<span v-if="label" class="nav-action-item-label">
|
||||
{{ label }}
|
||||
</span>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavActionItem',
|
||||
props: {
|
||||
isLabel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.nav-action-item {
|
||||
margin: 10px 0;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $navActionsItemColor;
|
||||
|
||||
& + .nav-action-item {
|
||||
//margin-left: 10px;
|
||||
}
|
||||
|
||||
&.is-label {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.nav-action-item >>> .title {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.nav-action-item >>> .label {
|
||||
color: inherit;
|
||||
font-size: 14px;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.nav-action-item >>> .el-button.el-button--small {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.nav-action-item >>> .el-button:not(.is-circle) .fa {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.nav-action-item >>> .el-button .icon + span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.nav-action-item >>> .nav-action-item-label {
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="navActions"
|
||||
:class="classes"
|
||||
:style="style"
|
||||
class="nav-actions"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, ref, watch} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavActions',
|
||||
props: {
|
||||
collapsed: Boolean,
|
||||
minHeight: String,
|
||||
},
|
||||
setup(props) {
|
||||
const originalHeight = ref<string | null>(null);
|
||||
const height = ref<string | null>(null);
|
||||
|
||||
const navActions = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const unmounted = ref<boolean>(true);
|
||||
|
||||
const collapsed = computed<boolean>(() => {
|
||||
const {collapsed} = props as NavActionsProps;
|
||||
return collapsed || false;
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
return {
|
||||
height: height.value,
|
||||
};
|
||||
});
|
||||
|
||||
const classes = computed<string[]>(() => {
|
||||
const cls = [];
|
||||
if (collapsed.value) cls.push('collapsed');
|
||||
if (unmounted.value) cls.push('unmounted');
|
||||
return cls;
|
||||
});
|
||||
|
||||
const updateHeight = () => {
|
||||
if (!collapsed.value) {
|
||||
if (originalHeight.value === null) {
|
||||
if (!navActions.value) return;
|
||||
originalHeight.value = `calc(${window.getComputedStyle(navActions.value).height} - 1px)`;
|
||||
}
|
||||
height.value = originalHeight.value;
|
||||
} else {
|
||||
height.value = '0';
|
||||
}
|
||||
};
|
||||
|
||||
const getHeight = () => {
|
||||
return height.value;
|
||||
};
|
||||
|
||||
watch(collapsed, () => updateHeight());
|
||||
|
||||
onMounted(() => {
|
||||
updateHeight();
|
||||
unmounted.value = false;
|
||||
});
|
||||
|
||||
return {
|
||||
navActions,
|
||||
style,
|
||||
classes,
|
||||
getHeight,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.nav-actions {
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: fit-content;
|
||||
border-bottom: 1px solid $infoBorderColor;
|
||||
transition: all $navActionsCollapseTransitionDuration;
|
||||
overflow-y: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.collapsed {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.unmounted {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div class="nav-link" @click="onClick">
|
||||
<Icon :icon="icon" class="icon"/>
|
||||
<span class="title">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
import Icon from '@/components/icon/Icon.vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavLink',
|
||||
components: {Icon},
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array] as PropType<Icon>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'click',
|
||||
],
|
||||
setup(props: NavLinkProps, {emit}) {
|
||||
const router = useRouter();
|
||||
|
||||
const onClick = () => {
|
||||
const {path} = props;
|
||||
if (path) {
|
||||
router.push(path);
|
||||
}
|
||||
emit('click');
|
||||
};
|
||||
|
||||
return {
|
||||
onClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/color";
|
||||
|
||||
.nav-link {
|
||||
cursor: pointer;
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div class="nav-sidebar" :class="classes">
|
||||
<div class="search">
|
||||
<el-input
|
||||
v-model="searchString"
|
||||
class="search-input"
|
||||
:prefix-icon="collapsed ? '' : 'fa fa-search'"
|
||||
placeholder="Search..."
|
||||
:clearable="true"
|
||||
/>
|
||||
<div v-if="!collapsed" class="search-suffix" @click.stop="onToggle">
|
||||
<el-tooltip
|
||||
v-model="toggleTooltipValue"
|
||||
content="Collapse sidebar"
|
||||
>
|
||||
<font-awesome-icon :icon="['fa', 'outdent']" class="icon"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu
|
||||
ref="navMenu"
|
||||
v-if="filteredItems && filteredItems.length > 0"
|
||||
class="nav-menu"
|
||||
:default-active="activeKey"
|
||||
@select="onSelect"
|
||||
>
|
||||
<el-menu-item v-for="item in filteredItems" :key="item.id" :index="item.id" class="nav-menu-item">
|
||||
<span class="title">{{ item.title }}</span>
|
||||
<!-- TODO: implement -->
|
||||
<span v-if="false" class="actions">
|
||||
<font-awesome-icon class="icon" :icon="['far', 'star']" @click="onStar(item.id)"/>
|
||||
<font-awesome-icon class="icon" :icon="['far', 'clone']" @click="onDuplicate(item.id)"/>
|
||||
<font-awesome-icon class="icon" :icon="['fa', 'trash-alt']" @click="onRemove(item.id)"/>
|
||||
</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<Empty
|
||||
v-else
|
||||
description="No Items"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, ref} from 'vue';
|
||||
import {ElMenu} from 'element-plus';
|
||||
import variables from '@/styles/variables.scss';
|
||||
import Empty from '@/components/empty/Empty.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavSidebar',
|
||||
components: {Empty},
|
||||
props: {
|
||||
items: Array,
|
||||
activeKey: String,
|
||||
collapsed: Boolean,
|
||||
showActions: Boolean,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
const toggling = ref(false);
|
||||
const searchString = ref('');
|
||||
const navMenu = ref<typeof ElMenu | null>(null);
|
||||
const toggleTooltipValue = ref(false);
|
||||
|
||||
const filteredItems = computed<NavItem[]>(() => {
|
||||
const items = props.items as NavItem[];
|
||||
if (!searchString.value) return items;
|
||||
return items.filter(d => d.title?.toLocaleLowerCase().includes(searchString.value.toLocaleLowerCase()));
|
||||
});
|
||||
|
||||
const classes = computed(() => {
|
||||
const {collapsed} = props as NavSidebarProps;
|
||||
const cls = [];
|
||||
if (collapsed) cls.push('collapsed');
|
||||
// if (toggling.value) cls.push('toggling');
|
||||
return cls;
|
||||
});
|
||||
|
||||
const onSelect = (index: string) => {
|
||||
emit('select', index);
|
||||
};
|
||||
|
||||
const onStar = (index: string) => {
|
||||
emit('star', index);
|
||||
};
|
||||
|
||||
const onDuplicate = (index: string) => {
|
||||
emit('duplicate', index);
|
||||
};
|
||||
|
||||
const onRemove = (index: string) => {
|
||||
emit('remove', index);
|
||||
};
|
||||
|
||||
const onToggle = () => {
|
||||
const {collapsed} = props as NavSidebarProps;
|
||||
emit('toggle', !collapsed);
|
||||
toggleTooltipValue.value = false;
|
||||
};
|
||||
|
||||
const scroll = (id: string) => {
|
||||
const idx = filteredItems.value.findIndex(d => d.id === id);
|
||||
if (idx === -1) return;
|
||||
const {navSidebarItemHeight} = variables;
|
||||
const navSidebarItemHeightNumber = Number(navSidebarItemHeight.replace('px', ''));
|
||||
if (!navMenu.value) return;
|
||||
const $el = navMenu.value.$el as HTMLDivElement;
|
||||
$el.scrollTo({
|
||||
top: navSidebarItemHeightNumber * idx,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
toggling,
|
||||
searchString,
|
||||
navMenu,
|
||||
toggleTooltipValue,
|
||||
filteredItems,
|
||||
classes,
|
||||
onSelect,
|
||||
onStar,
|
||||
onDuplicate,
|
||||
onRemove,
|
||||
onToggle,
|
||||
scroll,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.nav-sidebar {
|
||||
position: relative;
|
||||
//margin: 10px;
|
||||
width: $navSidebarWidth;
|
||||
border-right: 1px solid $navSidebarBorderColor;
|
||||
background-color: $navSidebarBg;
|
||||
height: calc(100vh - #{$headerHeight} - #{$tabsViewHeight} - 1px);
|
||||
transition: width $navSidebarCollapseTransitionDuration;
|
||||
|
||||
&.collapsed {
|
||||
margin: 10px 0;
|
||||
width: 0;
|
||||
border: none;
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
height: $navSidebarSearchHeight;
|
||||
box-sizing: content-box;
|
||||
border-bottom: 1px solid $navSidebarBorderColor;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-suffix {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
width: 25px;
|
||||
color: $navSidebarItemActionColor;
|
||||
cursor: pointer;
|
||||
//transition: right $navSidebarCollapseTransitionDuration;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
max-height: calc(100% - #{$navSidebarSearchHeight});
|
||||
overflow-y: auto;
|
||||
color: $navSidebarColor;
|
||||
|
||||
&.empty {
|
||||
height: $navSidebarItemHeight;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-menu-item {
|
||||
position: relative;
|
||||
height: $navSidebarItemHeight;
|
||||
line-height: $navSidebarItemHeight;
|
||||
|
||||
&:hover {
|
||||
.actions {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
|
||||
.icon {
|
||||
color: $navSidebarItemActionColor;
|
||||
margin-left: 3px;
|
||||
|
||||
&:hover {
|
||||
color: $primaryColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-expand {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: $infoPlainColor;
|
||||
border: 1px solid $infoColor;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border-left: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.nav-sidebar > .search >>> .el-input__inner {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-sidebar.collapsed > .search >>> .el-input__inner {
|
||||
padding: 0;
|
||||
width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div class="nav-tabs">
|
||||
<el-menu
|
||||
:default-active="activeKey"
|
||||
mode="horizontal"
|
||||
@select="onSelect"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:class="item.emphasis ? 'emphasis' : ''"
|
||||
:index="item.id"
|
||||
:style="item.style"
|
||||
>
|
||||
<el-tooltip :content="item.tooltip" :disabled="!item.tooltip">
|
||||
<template v-if="!!item.icon">
|
||||
<font-awesome-icon :icon="item.icon"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
</el-tooltip>
|
||||
</el-menu-item>
|
||||
<div class="extra">
|
||||
<slot name="extra">
|
||||
</slot>
|
||||
</div>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavTabs',
|
||||
props: {
|
||||
items: Array,
|
||||
activeKey: String,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
const onSelect = (index: string) => {
|
||||
emit('select', index);
|
||||
};
|
||||
|
||||
return {
|
||||
onSelect,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.nav-tabs {
|
||||
.el-menu {
|
||||
height: calc(#{$navTabsHeight} + 1px);
|
||||
|
||||
.el-menu-item {
|
||||
height: $navTabsHeight;
|
||||
line-height: $navTabsHeight;
|
||||
|
||||
&.emphasis {
|
||||
color: $infoColor;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.extra {
|
||||
float: right;
|
||||
height: $navTabsHeight;
|
||||
line-height: $navTabsHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<CreateEditDialog
|
||||
:action-functions="actionFunctions"
|
||||
:batch-form-data="formList"
|
||||
:batch-form-fields="batchFormFields"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
:confirm-loading="confirmLoading"
|
||||
:tab-name="createEditDialogTabName"
|
||||
:type="activeDialogKey"
|
||||
:visible="createEditDialogVisible"
|
||||
:form-rules="formRules"
|
||||
>
|
||||
<template #default>
|
||||
<NodeForm/>
|
||||
</template>
|
||||
</CreateEditDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import CreateEditDialog from '@/components/dialog/CreateEditDialog.vue';
|
||||
import NodeForm from '@/components/node/NodeForm.vue';
|
||||
import useNode from '@/components/node/node';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CreateEditProjectDialog',
|
||||
components: {
|
||||
CreateEditDialog,
|
||||
NodeForm,
|
||||
},
|
||||
setup() {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...useNode(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<el-tag :type="type" class="node-active" size="mini">
|
||||
<font-awesome-icon :icon="icon" class="icon"/>
|
||||
<span>{{ label }}</span>
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeActive',
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
setup(props: NodeActiveProps, {emit}) {
|
||||
const type = computed<string>(() => {
|
||||
const {active} = props;
|
||||
return active ? 'success' : 'info';
|
||||
});
|
||||
|
||||
const label = computed<string>(() => {
|
||||
const {active} = props;
|
||||
return active ? 'Online' : 'Offline';
|
||||
});
|
||||
|
||||
const icon = computed<string[]>(() => {
|
||||
const {active} = props;
|
||||
return active ? ['fa', 'check-circle'] : ['fa', 'times-circle'];
|
||||
});
|
||||
|
||||
return {
|
||||
type,
|
||||
label,
|
||||
icon,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node-active {
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<Form
|
||||
v-if="form"
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:selective="isSelectiveForm"
|
||||
>
|
||||
<!--Row-->
|
||||
<FormItem v-if="readonly" :offset="2" :span="2" label="Key" not-editable prop="key">
|
||||
<el-input :value="form.key" disabled/>
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="2" label="Name" not-editable prop="name" required>
|
||||
<el-input v-model="form.name" :disabled="isFormItemDisabled('name')" placeholder="Name"/>
|
||||
</FormItem>
|
||||
<FormItem :span="2" label="Tags" prop="tags">
|
||||
<TagInput v-model="form.tags" :disabled="isFormItemDisabled('tags')"/>
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="2" label="Type" not-editable prop="type">
|
||||
<NodeType :is-master="form.is_master"/>
|
||||
</FormItem>
|
||||
<FormItem :span="2" label="IP" prop="ip">
|
||||
<el-input v-model="form.ip" :disabled="isFormItemDisabled('ip')" placeholder="IP"/>
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="2" label="MAC Address" prop="mac">
|
||||
<el-input v-model="form.mac" :disabled="isFormItemDisabled('mac')" placeholder="MAC Address"/>
|
||||
</FormItem>
|
||||
<FormItem :span="2" label="Hostname" prop="hostname">
|
||||
<el-input v-model="form.hostname" :disabled="isFormItemDisabled('hostname')" placeholder="Hostname"/>
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="2" label="Enabled" prop="enabled">
|
||||
<Switch v-model="form.enabled" :disabled="isFormItemDisabled('enabled')"/>
|
||||
</FormItem>
|
||||
<FormItem :span="2" label="Max Runners" prop="max_runners">
|
||||
<el-input-number
|
||||
v-model="form.max_runners"
|
||||
:disabled="isFormItemDisabled('max_runners')"
|
||||
:min="0"
|
||||
placeholder="Max Runners"
|
||||
/>
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="4" label="Description" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
:disabled="isFormItemDisabled('description')"
|
||||
placeholder="Description"
|
||||
type="textarea"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<!--./Row-->
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import useNode from '@/components/node/node';
|
||||
import TagInput from '@/components/input/TagInput.vue';
|
||||
import Form from '@/components/form/Form.vue';
|
||||
import FormItem from '@/components/form/FormItem.vue';
|
||||
import NodeType from '@/components/node/NodeType.vue';
|
||||
import Switch from '@/components/switch/Switch.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeForm',
|
||||
props: {
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Switch,
|
||||
NodeType,
|
||||
Form,
|
||||
FormItem,
|
||||
TagInput,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...useNode(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<Tag
|
||||
:key="data"
|
||||
:icon="data.icon"
|
||||
:label="data.label"
|
||||
:size="size"
|
||||
:spinning="data.spinning"
|
||||
:tooltip="data.tooltip"
|
||||
:type="data.type"
|
||||
@click="$emit('click')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from 'vue';
|
||||
import Tag from '@/components/tag/Tag.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeRunners',
|
||||
components: {
|
||||
Tag,
|
||||
},
|
||||
props: {
|
||||
available: {
|
||||
type: Number as PropType<number | undefined>,
|
||||
required: false,
|
||||
},
|
||||
max: {
|
||||
type: Number as PropType<number | undefined>,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<BasicSize>,
|
||||
required: false,
|
||||
default: 'mini',
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props: NodeRunnersProps, {emit}) {
|
||||
const running = computed<number>(() => {
|
||||
const {available, max} = props;
|
||||
if (available === undefined ||
|
||||
max === undefined ||
|
||||
isNaN(available) ||
|
||||
isNaN(max)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
return (max - available) as number;
|
||||
});
|
||||
|
||||
const label = computed<string>(() => {
|
||||
const {max} = props;
|
||||
return `${running.value} / ${max}`;
|
||||
});
|
||||
|
||||
const data = computed<TagData>(() => {
|
||||
const max = props.max as number;
|
||||
if (running.value === max) {
|
||||
return {
|
||||
label: label.value,
|
||||
tooltip: 'No runners available at this moment',
|
||||
type: 'danger',
|
||||
icon: ['fa', 'ban'],
|
||||
};
|
||||
} else if (running.value > 0) {
|
||||
return {
|
||||
label: label.value,
|
||||
tooltip: `${running.value} out of ${max} runners are running`,
|
||||
type: 'warning',
|
||||
icon: ['far', 'check-square'],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: label.value,
|
||||
tooltip: `All runners available`,
|
||||
type: 'success',
|
||||
icon: ['far', 'check-square'],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
label,
|
||||
data,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node-runners {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<Tag
|
||||
:key="data"
|
||||
:icon="data.icon"
|
||||
:label="data.label"
|
||||
:size="size"
|
||||
:spinning="data.spinning"
|
||||
:tooltip="data.tooltip"
|
||||
:type="data.type"
|
||||
@click="$emit('click')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from 'vue';
|
||||
import Tag from '@/components/tag/Tag.vue';
|
||||
import {
|
||||
NODE_STATUS_OFFLINE,
|
||||
NODE_STATUS_ONLINE,
|
||||
NODE_STATUS_REGISTERED,
|
||||
NODE_STATUS_UNREGISTERED
|
||||
} from '@/constants/node';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeStatus',
|
||||
components: {
|
||||
Tag,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: String as PropType<TaskStatus>,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<BasicSize>,
|
||||
required: false,
|
||||
default: 'mini',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props: NodeStatusProps, {emit}) {
|
||||
const data = computed<TagData>(() => {
|
||||
const {status} = props;
|
||||
switch (status) {
|
||||
case NODE_STATUS_UNREGISTERED:
|
||||
return {
|
||||
label: 'Unregistered',
|
||||
tooltip: 'Node is waiting to be registered',
|
||||
type: 'danger',
|
||||
icon: ['fa', 'exclamation'],
|
||||
};
|
||||
case NODE_STATUS_REGISTERED:
|
||||
return {
|
||||
label: 'Registered',
|
||||
tooltip: 'Node is registered and wait to be online',
|
||||
type: 'warning',
|
||||
icon: ['far', 'check-square'],
|
||||
};
|
||||
case NODE_STATUS_ONLINE:
|
||||
return {
|
||||
label: 'Online',
|
||||
tooltip: 'Node is currently online',
|
||||
type: 'success',
|
||||
icon: ['fa', 'check'],
|
||||
};
|
||||
case NODE_STATUS_OFFLINE:
|
||||
return {
|
||||
label: 'Offline',
|
||||
tooltip: 'Node is currently offline',
|
||||
type: 'info',
|
||||
icon: ['fa', 'times'],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: 'Unknown',
|
||||
tooltip: 'Unknown node status',
|
||||
type: 'info',
|
||||
icon: ['fa', 'question'],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.task-status {
|
||||
width: 80px;
|
||||
cursor: default;
|
||||
|
||||
.icon {
|
||||
width: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<el-tag :type="type" class="node-type" size="mini">
|
||||
<font-awesome-icon :icon="icon" class="icon"/>
|
||||
<span>{{ computedLabel }}</span>
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeType',
|
||||
props: {
|
||||
isMaster: {
|
||||
type: Boolean,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props: NodeTypeProps, {emit}) {
|
||||
const type = computed<string>(() => {
|
||||
const {isMaster} = props;
|
||||
return isMaster ? 'primary' : 'warning';
|
||||
});
|
||||
|
||||
const computedLabel = computed<string>(() => {
|
||||
const {isMaster, label} = props;
|
||||
if (label) return label;
|
||||
return isMaster ? 'Master' : 'Worker';
|
||||
});
|
||||
|
||||
const icon = computed<string[]>(() => {
|
||||
const {isMaster} = props;
|
||||
return isMaster ? ['fa', 'home'] : ['fa', 'server'];
|
||||
});
|
||||
|
||||
return {
|
||||
type,
|
||||
computedLabel,
|
||||
icon,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node-type {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
import {readonly} from 'vue';
|
||||
import {Store} from 'vuex';
|
||||
import useForm from '@/components/form/form';
|
||||
import useNodeService from '@/services/node/nodeService';
|
||||
import {getDefaultFormComponentData} from '@/utils/form';
|
||||
import {FORM_FIELD_TYPE_INPUT, FORM_FIELD_TYPE_INPUT_TEXTAREA, FORM_FIELD_TYPE_SWITCH} from '@/constants/form';
|
||||
|
||||
type Node = CNode;
|
||||
|
||||
// get new node
|
||||
export const getNewNode = (): Node => {
|
||||
return {
|
||||
tags: [],
|
||||
max_runners: 8,
|
||||
enabled: true,
|
||||
};
|
||||
};
|
||||
|
||||
// form component data
|
||||
const formComponentData = getDefaultFormComponentData<Node>(getNewNode);
|
||||
|
||||
const useNode = (store: Store<RootStoreState>) => {
|
||||
// store
|
||||
const ns = 'node';
|
||||
const {node: state} = store.state as RootStoreState;
|
||||
|
||||
// batch form fields
|
||||
const batchFormFields: FormTableField[] = [
|
||||
{
|
||||
prop: 'name',
|
||||
label: 'Name',
|
||||
width: '150',
|
||||
fieldType: FORM_FIELD_TYPE_INPUT,
|
||||
required: true,
|
||||
placeholder: 'Name',
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: 'Enabled',
|
||||
width: '120',
|
||||
fieldType: FORM_FIELD_TYPE_SWITCH,
|
||||
},
|
||||
{
|
||||
prop: 'description',
|
||||
label: 'Description',
|
||||
width: '200',
|
||||
fieldType: FORM_FIELD_TYPE_INPUT_TEXTAREA,
|
||||
},
|
||||
];
|
||||
|
||||
// form rules
|
||||
const formRules = readonly<FormRules>({});
|
||||
|
||||
return {
|
||||
...useForm(ns, store, useNodeService(store), formComponentData),
|
||||
batchFormFields,
|
||||
formRules,
|
||||
};
|
||||
};
|
||||
|
||||
export default useNode;
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<CreateEditDialog
|
||||
:action-functions="actionFunctions"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
:confirm-loading="confirmLoading"
|
||||
:tab-name="createEditDialogTabName"
|
||||
:type="activeDialogKey"
|
||||
:visible="createEditDialogVisible"
|
||||
:form-rules="formRules"
|
||||
no-batch
|
||||
>
|
||||
<template #default>
|
||||
<PluginForm/>
|
||||
</template>
|
||||
</CreateEditDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import CreateEditDialog from '@/components/dialog/CreateEditDialog.vue';
|
||||
import PluginForm from '@/components/plugin/PluginForm.vue';
|
||||
import usePlugin from '@/components/plugin/plugin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CreateEditProjectDialog',
|
||||
components: {
|
||||
CreateEditDialog,
|
||||
PluginForm,
|
||||
},
|
||||
setup() {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...usePlugin(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<Form
|
||||
v-if="form"
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:selective="isSelectiveForm"
|
||||
>
|
||||
<!--Row-->
|
||||
<FormItem :span="2" :offset="2" label="Name" not-editable prop="name" required>
|
||||
<el-input v-model="form.name" disabled placeholder="Name" />
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="2" label="Execute Command" prop="cmd">
|
||||
<el-input
|
||||
v-model="form.cmd"
|
||||
disabled
|
||||
placeholder="cmd"
|
||||
/>
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="4" label="Description" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
disabled
|
||||
placeholder="Description"
|
||||
type="textarea"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<!--./Row-->
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import usePlugin from '@/components/plugin/plugin';
|
||||
import Form from '@/components/form/Form.vue';
|
||||
import FormItem from '@/components/form/FormItem.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PluginForm',
|
||||
props: {
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Form,
|
||||
FormItem,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...usePlugin(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
import {readonly} from 'vue';
|
||||
import {Store} from 'vuex';
|
||||
import useForm from '@/components/form/form';
|
||||
import usePluginService from '@/services/plugin/pluginService';
|
||||
import {getDefaultFormComponentData} from '@/utils/form';
|
||||
|
||||
type Plugin = CPlugin;
|
||||
|
||||
// get new plugin
|
||||
export const getNewPlugin = (): Plugin => {
|
||||
return {};
|
||||
};
|
||||
|
||||
// form component data
|
||||
const formComponentData = getDefaultFormComponentData<Plugin>(getNewPlugin);
|
||||
|
||||
const usePlugin = (store: Store<RootStoreState>) => {
|
||||
// store
|
||||
const ns = 'plugin';
|
||||
|
||||
// form rules
|
||||
const formRules = readonly<FormRules>({});
|
||||
|
||||
return {
|
||||
...useForm(ns, store, usePluginService(store), formComponentData),
|
||||
formRules,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePlugin;
|
||||
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<CreateEditDialog
|
||||
:type="activeDialogKey"
|
||||
:tab-name="createEditDialogTabName"
|
||||
:visible="createEditDialogVisible"
|
||||
:action-functions="actionFunctions"
|
||||
:batch-form-data="formList"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
:confirm-loading="confirmLoading"
|
||||
:batch-form-fields="batchFormFields"
|
||||
:form-rules="formRules"
|
||||
>
|
||||
<template #default>
|
||||
<ProjectForm/>
|
||||
</template>
|
||||
</CreateEditDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import CreateEditDialog from '@/components/dialog/CreateEditDialog.vue';
|
||||
import ProjectForm from '@/components/project/ProjectForm.vue';
|
||||
import useProject from '@/components/project/project';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CreateEditProjectDialog',
|
||||
components: {
|
||||
CreateEditDialog,
|
||||
ProjectForm,
|
||||
},
|
||||
setup() {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...useProject(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<Form
|
||||
v-if="form"
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
:selective="isSelectiveForm"
|
||||
>
|
||||
<FormItem :span="2" label="Name" not-editable prop="name" required>
|
||||
<el-input v-model="form.name" :disabled="isFormItemDisabled('name')" placeholder="Name"/>
|
||||
</FormItem>
|
||||
<FormItem :span="2" label="Tags" prop="tags">
|
||||
<TagInput v-model="form.tags" :disabled="isFormItemDisabled('tags')"/>
|
||||
</FormItem>
|
||||
<FormItem :span="4" label="Description" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
:disabled="isFormItemDisabled('description')"
|
||||
placeholder="Description"
|
||||
type="textarea"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import useProject from '@/components/project/project';
|
||||
import Form from '@/components/form/Form.vue';
|
||||
import FormItem from '@/components/form/FormItem.vue';
|
||||
import TagInput from '@/components/input/TagInput.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProjectForm',
|
||||
components: {TagInput, FormItem, Form},
|
||||
setup() {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...useProject(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<el-tag :color="color" :type="type" class="project-tag" size="mini">
|
||||
<span>{{ label }}</span>
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProjectTag',
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<BasicType>,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
setup(props: ProjectTagProps, {emit}) {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.project-tag {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user