mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-21 17:21:09 +01:00
121
.gitignore
vendored
121
.gitignore
vendored
@@ -1,128 +1,9 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
|
||||
# egg-info
|
||||
*.egg-info
|
||||
|
||||
# .DS_Store
|
||||
.DS_Store
|
||||
|
||||
.docks
|
||||
.docs
|
||||
|
||||
node_modules/
|
||||
logs/
|
||||
tmp/
|
||||
_book/
|
||||
.idea
|
||||
*.lock
|
||||
|
||||
backend/spiders
|
||||
spiders/*.zip
|
||||
|
||||
vendor/
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -16,9 +16,6 @@ WORKDIR /app
|
||||
RUN rm /app/.npmrc
|
||||
|
||||
# install frontend
|
||||
#RUN npm config set unsafe-perm true
|
||||
#RUN npm install -g yarn && yarn install
|
||||
|
||||
RUN yarn install && yarn run build:docker
|
||||
|
||||
# images
|
||||
@@ -33,18 +30,26 @@ ENV CRAWLAB_IS_DOCKER Y
|
||||
# install packages
|
||||
RUN chmod 777 /tmp \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y curl git net-tools iputils-ping ntp ntpdate python3 python3-pip nginx wget dumb-init cloc \
|
||||
&& apt-get install -y curl git net-tools iputils-ping ntp ntpdate nginx wget dumb-init cloc
|
||||
|
||||
# install python
|
||||
RUN apt-get install -y python3 python3-pip \
|
||||
&& ln -s /usr/bin/pip3 /usr/local/bin/pip \
|
||||
&& ln -s /usr/bin/python3 /usr/local/bin/python
|
||||
|
||||
# install golang
|
||||
RUN curl -OL https://golang.org/dl/go1.16.7.linux-amd64.tar.gz \
|
||||
&& tar -C /usr/local -xvf go1.16.7.linux-amd64.tar.gz \
|
||||
&& ln -s /usr/local/go/bin/go /usr/local/bin/go
|
||||
|
||||
# install seaweedfs
|
||||
RUN wget https://github.com/chrislusf/seaweedfs/releases/download/2.59/linux_amd64.tar.gz \
|
||||
RUN wget https://github.com/chrislusf/seaweedfs/releases/download/2.76/linux_amd64.tar.gz \
|
||||
&& tar -zxf linux_amd64.tar.gz \
|
||||
&& cp weed /usr/local/bin
|
||||
|
||||
# install backend
|
||||
RUN pip install scrapy pymongo bs4 requests
|
||||
RUN pip install crawlab-sdk==0.6.b20210729-1634
|
||||
RUN pip install crawlab-sdk==0.6.b20211024-1207
|
||||
|
||||
# add files
|
||||
COPY ./backend/conf /app/backend/conf
|
||||
|
||||
@@ -16,9 +16,6 @@ WORKDIR /app
|
||||
#RUN rm /app/.npmrc
|
||||
|
||||
# install frontend
|
||||
#RUN npm config set unsafe-perm true
|
||||
#RUN npm install -g yarn && yarn install
|
||||
|
||||
RUN yarn install && yarn run build:docker
|
||||
|
||||
# images
|
||||
@@ -33,18 +30,26 @@ ENV CRAWLAB_IS_DOCKER Y
|
||||
# install packages
|
||||
RUN chmod 777 /tmp \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y curl git net-tools iputils-ping ntp ntpdate python3 python3-pip nginx wget dumb-init cloc \
|
||||
&& apt-get install -y curl git net-tools iputils-ping ntp ntpdate nginx wget dumb-init cloc
|
||||
|
||||
# install python
|
||||
RUN apt-get install -y python3 python3-pip \
|
||||
&& ln -s /usr/bin/pip3 /usr/local/bin/pip \
|
||||
&& ln -s /usr/bin/python3 /usr/local/bin/python
|
||||
|
||||
# install golang
|
||||
RUN curl -OL https://storage.googleapis.com/golang/go1.16.7.linux-amd64.tar.gz \
|
||||
&& tar -C /usr/local -xvf go1.16.7.linux-amd64.tar.gz \
|
||||
&& ln -s /usr/local/go/bin/go /usr/local/bin/go
|
||||
|
||||
# install seaweedfs
|
||||
RUN wget https://github.com/chrislusf/seaweedfs/releases/download/2.59/linux_amd64.tar.gz \
|
||||
RUN wget https://github.com/chrislusf/seaweedfs/releases/download/2.76/linux_amd64.tar.gz \
|
||||
&& tar -zxf linux_amd64.tar.gz \
|
||||
&& cp weed /usr/local/bin
|
||||
|
||||
# install backend
|
||||
RUN pip install scrapy pymongo bs4 requests -i https://mirrors.aliyun.com/pypi/simple
|
||||
RUN pip install crawlab-sdk==0.6.b20210729-1634
|
||||
RUN pip install crawlab-sdk==0.6.b20211024-1207
|
||||
|
||||
# add files
|
||||
COPY ./backend/conf /app/backend/conf
|
||||
|
||||
@@ -16,9 +16,6 @@ WORKDIR /app
|
||||
#RUN rm /app/.npmrc
|
||||
|
||||
# install frontend
|
||||
#RUN npm config set unsafe-perm true
|
||||
#RUN npm install -g yarn && yarn install
|
||||
|
||||
RUN yarn install && yarn run build:docker
|
||||
|
||||
# images
|
||||
@@ -33,18 +30,26 @@ ENV CRAWLAB_IS_DOCKER Y
|
||||
# install packages
|
||||
RUN chmod 777 /tmp \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y curl git net-tools iputils-ping ntp ntpdate python3 python3-pip nginx wget dumb-init cloc \
|
||||
&& apt-get install -y curl git net-tools iputils-ping ntp ntpdate nginx wget dumb-init cloc
|
||||
|
||||
# install python
|
||||
RUN apt-get install -y python3 python3-pip \
|
||||
&& ln -s /usr/bin/pip3 /usr/local/bin/pip \
|
||||
&& ln -s /usr/bin/python3 /usr/local/bin/python
|
||||
|
||||
# install golang
|
||||
RUN curl -OL https://golang.org/dl/go1.16.7.linux-amd64.tar.gz \
|
||||
&& tar -C /usr/local -xvf go1.16.7.linux-amd64.tar.gz \
|
||||
&& ln -s /usr/local/go/bin/go /usr/local/bin/go
|
||||
|
||||
# install seaweedfs
|
||||
RUN wget https://github.com/chrislusf/seaweedfs/releases/download/2.59/linux_amd64.tar.gz \
|
||||
RUN wget https://github.com/chrislusf/seaweedfs/releases/download/2.76/linux_amd64.tar.gz \
|
||||
&& tar -zxf linux_amd64.tar.gz \
|
||||
&& cp weed /usr/local/bin
|
||||
|
||||
# install backend
|
||||
RUN pip install scrapy pymongo bs4 requests -i https://mirrors.aliyun.com/pypi/simple
|
||||
RUN pip install crawlab-sdk==0.6.b20210729-1634
|
||||
RUN pip install crawlab-sdk==0.6.b20211024-1207
|
||||
|
||||
# add files
|
||||
COPY ./backend/conf /app/backend/conf
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.15
|
||||
|
||||
require (
|
||||
github.com/apex/log v1.9.0
|
||||
github.com/crawlab-team/crawlab-core v0.6.0-beta.20210811.1634
|
||||
github.com/crawlab-team/crawlab-core v0.6.0-beta.20211113.2050
|
||||
github.com/crawlab-team/go-trace v0.1.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/spf13/cobra v1.1.3
|
||||
|
||||
@@ -16,9 +16,7 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
@@ -26,20 +24,17 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/StackExchange/wmi v1.2.0 h1:noJEYkMQVlFCEAc+2ma5YyRhlfjcWfZqk5sBRYozdyM=
|
||||
github.com/StackExchange/wmi v1.2.0/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/Unknwon/goconfig v0.0.0-20191126170842-860a72fb44fd h1:+CYOsXi89xOqBkj7CuEJjA2It+j+R3ngUZEydr6mtkw=
|
||||
github.com/Unknwon/goconfig v0.0.0-20191126170842-860a72fb44fd/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0=
|
||||
github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
|
||||
@@ -72,28 +67,26 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/crawlab-team/crawlab-core v0.0.1 h1:tZ9Rlji5L5uoZ6nRZFqnSGmEl5TdmdxAyxueJDKfUl8=
|
||||
github.com/crawlab-team/crawlab-core v0.0.1/go.mod h1:6dJHMvrmIJbfYHhYNeGZkGOLEBvur+yGiFzLCRXx92k=
|
||||
github.com/crawlab-team/crawlab-core v0.6.0-beta.20210811.1634 h1:rUCWl/AwGczVIx54StgAeu3pOVN9n4zUWcN9//7Juu8=
|
||||
github.com/crawlab-team/crawlab-core v0.6.0-beta.20210811.1634/go.mod h1:dvqfN3ZjLZQSxXYkHFFPEbgxZNHqad+yFhZv++Qg4tQ=
|
||||
github.com/crawlab-team/crawlab-core v0.6.0-beta.20211113.2050 h1:S58HbKOjErL6jOafBHi/5TvtAsCozwGQ1mBGonIQpHA=
|
||||
github.com/crawlab-team/crawlab-core v0.6.0-beta.20211113.2050/go.mod h1:FVYKQc+//BZ6eaAAXPsHzVaAKD4fkGytqcoRSvgpK9I=
|
||||
github.com/crawlab-team/crawlab-db v0.0.2/go.mod h1:o7o4rbcyAWlFGHg9VS7V7tM/GqRq+N2mnAXO71cZA78=
|
||||
github.com/crawlab-team/crawlab-db v0.1.1 h1:156h2fbbFKXAHs1mxprqRFC8zs2nrdyaG9JKG7patVw=
|
||||
github.com/crawlab-team/crawlab-db v0.1.1/go.mod h1:t0VidSjXKzQgACqNSQV5wusXncFtL6lGEiQTbLfNR04=
|
||||
github.com/crawlab-team/crawlab-fs v0.0.0/go.mod h1:k2VXprQspLAmbgO5sSpqMjg/xP4iKDkW4RyTWY8eTZM=
|
||||
github.com/crawlab-team/crawlab-fs v0.1.0 h1:iKSJJY4Wvea8Qss+zC/tLiZ371VeV75Z3cuqlsxydzY=
|
||||
github.com/crawlab-team/crawlab-fs v0.1.0/go.mod h1:dOE0TeWPDz9krwzt1H72rjj0Fn/aHe53yn7GoOZHD0s=
|
||||
github.com/crawlab-team/crawlab-grpc v0.6.0-beta.20210811.1628 h1:VcW4n+EvmkbG1UMJ0xPDDyVS+lzKywIcrgFG+VJw1RA=
|
||||
github.com/crawlab-team/crawlab-grpc v0.6.0-beta.20210811.1628/go.mod h1:W9Yee6xfesxoaqS5K1sF1I1zlH+i6xqwy4lyoBTOdkc=
|
||||
github.com/crawlab-team/crawlab-fs v0.6.0-beta.20211101.1940 h1:KFZ39oe/QyhhRhXYZSmzDZl3L/JUEPkiiaf4+/iuboY=
|
||||
github.com/crawlab-team/crawlab-fs v0.6.0-beta.20211101.1940/go.mod h1:dA1G6xeiClbTMkjRuoagGrcKfQ97jJZRAhZUSwrKdoI=
|
||||
github.com/crawlab-team/crawlab-grpc v0.6.0-beta.20211009.1455 h1:jykwiu71Vy+bD4taRQBkHhPureFTSAhpxuK5WdaWj/A=
|
||||
github.com/crawlab-team/crawlab-grpc v0.6.0-beta.20211009.1455/go.mod h1:W9Yee6xfesxoaqS5K1sF1I1zlH+i6xqwy4lyoBTOdkc=
|
||||
github.com/crawlab-team/crawlab-log v0.1.0 h1:0t+lZEojs3Vqb/bMkk2qs3I+1+XdwKG3pMTfeK5PZWM=
|
||||
github.com/crawlab-team/crawlab-log v0.1.0/go.mod h1:N8nTTKEbr9ZQSlmw0+HNB4ZAMQF4yVMaJLx8YhXvhNo=
|
||||
github.com/crawlab-team/crawlab-vcs v0.1.0 h1:LjtKOOFzx1o7vvgGppC7jt/8lznyvFwwXBYggbSW9+4=
|
||||
github.com/crawlab-team/crawlab-vcs v0.1.0/go.mod h1:G6Hnt/3255QCGHO5Q0xJe1AbJE7m5t65E0v7flRJBJM=
|
||||
github.com/crawlab-team/crawlab-vcs v0.6.0-beta.20211103.2013 h1:kdlyHC4LFz8ANSk7W9HuzJn2HdoVnSR7ddsiYFsNel4=
|
||||
github.com/crawlab-team/crawlab-vcs v0.6.0-beta.20211103.2013/go.mod h1:G6Hnt/3255QCGHO5Q0xJe1AbJE7m5t65E0v7flRJBJM=
|
||||
github.com/crawlab-team/go-trace v0.1.0 h1:uCqfdqNfb+NwqdkQrBkcYfQ9iqGJ76MbPw1wK8n7xGg=
|
||||
github.com/crawlab-team/go-trace v0.1.0/go.mod h1:LcWyn68HoT+d29CHM8L41pFHxsAcBMF1xjqJmWdyFh8=
|
||||
github.com/crawlab-team/goseaweedfs v0.1.6/go.mod h1:u+rwfqb0rnYllTLjCctE/z1Yp+TC8L+CbbWH8E2NstA=
|
||||
github.com/crawlab-team/goseaweedfs v0.2.0/go.mod h1:u+rwfqb0rnYllTLjCctE/z1Yp+TC8L+CbbWH8E2NstA=
|
||||
github.com/crawlab-team/goseaweedfs v0.6.0-beta.20210725.1917 h1:Kb8AErE3357UO0jPf8Q2wqG/qcmL0hKDwPMaOZ/JjcY=
|
||||
github.com/crawlab-team/goseaweedfs v0.6.0-beta.20210725.1917/go.mod h1:u+rwfqb0rnYllTLjCctE/z1Yp+TC8L+CbbWH8E2NstA=
|
||||
github.com/crawlab-team/goseaweedfs v0.6.0-beta.20211101.1936 h1:c4SgTj2baDqD2UYa1eCpj3ukOF3mXOjvOCP4cWwgfyw=
|
||||
github.com/crawlab-team/goseaweedfs v0.6.0-beta.20211101.1936/go.mod h1:u+rwfqb0rnYllTLjCctE/z1Yp+TC8L+CbbWH8E2NstA=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -131,7 +124,6 @@ github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
@@ -165,6 +157,7 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
|
||||
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
@@ -274,7 +267,6 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
@@ -284,7 +276,6 @@ github.com/imroc/req v0.3.0 h1:3EioagmlSG+z+KySToa+Ylo3pTFZs+jh3Brl7ngU12U=
|
||||
github.com/imroc/req v0.3.0/go.mod h1:F+NZ+2EFSo6EFXdeIbpfE9hcC233id70kf0byW97Caw=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
@@ -360,7 +351,6 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -387,7 +377,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olivere/elastic/v7 v7.0.15 h1:v7kX5S+oMFfYKS4ZyzD37GH6lfZSpBo9atynRwBUywE=
|
||||
github.com/olivere/elastic/v7 v7.0.15/go.mod h1:+FgncZ8ho1QF3NlBo77XbuoTKYHhvEOfFZKIAfHnnDE=
|
||||
@@ -469,9 +458,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
|
||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -488,6 +475,8 @@ github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuI
|
||||
github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
|
||||
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
|
||||
github.com/swaggo/swag v1.6.6/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
|
||||
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
||||
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||
@@ -605,6 +594,8 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b h1:eB48h3HiRycXNy8E0Gf5e0hv7YT6Kt14L/D73G1fuwo=
|
||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
@@ -647,6 +638,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -658,6 +650,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -733,7 +728,6 @@ google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -749,13 +743,9 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkp
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/russross/blackfriday.v2 v2.0.0 h1:+FlnIV8DSQnT7NZ43hcVKcdJdzZoeCmJj4Ql8gq5keA=
|
||||
gopkg.in/russross/blackfriday.v2 v2.0.0/go.mod h1:6sSBNz/GtOm/pJTuh5UmBK2ZHfmnxGbl2NZg1UliSOI=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
- [ ] **Git 集成**. 将作为插件存在。
|
||||
- [ ] **Scrapy 集成**. 将作为插件存在。
|
||||
- [ ] **消息通知**. 将作为插件存在。
|
||||
- [ ] **关联人物**. 如果任务执行模式为 “所有节点” 或 “指定节点”,那么将会有主任务和子任务之分。
|
||||
- [ ] **关联任务**. 如果任务执行模式为 “所有节点” 或 “指定节点”,那么将会有主任务和子任务之分。
|
||||
- [ ] **Crontab 编辑器**. 可视化 Crontab 编辑的前端组件。
|
||||
- [ ] **结果去重**.
|
||||
- [ ] **环境变量**.
|
||||
|
||||
34
changelog/v0.6.0-beta.20211116-zh.md
Normal file
34
changelog/v0.6.0-beta.20211116-zh.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 更新日志 (v0.6.0-beta.20211116)
|
||||
|
||||
## 概览
|
||||
|
||||
这是下个重大版本 v0.6.0 的第二次 beta 发布(参考 [第一次 beta 版本](https://github.com/crawlab-team/crawlab/releases/tag/v0.6.0-beta.20210803))。伴随着更多功能和优化,正式版本 v0.6.0 将会很快发布了。
|
||||
|
||||
## 升级优化
|
||||
|
||||
#### 后端
|
||||
|
||||
- [x] **插件框架**. **Crawlab 插件框架 (CPF)** 已发布. 详情请参考 [这里](https://docs-next.crawlab.cn/zh/guide/use-crawlab/basic-concepts/plugin.html).
|
||||
- [x] **Git 集成**. Git 集成被作为内置功能.
|
||||
- [x] **Scrapy 集成**. Scrapy 集成以插件形式存在,插件为 [spider-assistant](https://github.com/crawlab-team/plugin-spider-assistant).
|
||||
- [x] **依赖集成**. Dependency 集成以插件形式存在,插件为 [dependency](https://github.com/crawlab-team/plugin-dependency).
|
||||
- [x] **消息通知**. 消息通知功能以插件形式存在,插件为 [notification](https://github.com/crawlab-team/plugin-notification).
|
||||
- [x] **文档网站**. 搭建 [文档网站](https://docs-next.crawlab.cn).
|
||||
|
||||
#### 前端
|
||||
- **Bug 修复**.
|
||||
|
||||
#### 待完成
|
||||
- [ ] **关联任务**. 如果任务执行模式为 “所有节点” 或 “指定节点”,那么将会有主任务和子任务之分。
|
||||
- [ ] **Crontab 编辑器**. 可视化 Crontab 编辑的前端组件。
|
||||
- [ ] **结果去重**.
|
||||
- [ ] **环境变量**.
|
||||
- [ ] **国际化**. 支持中文.
|
||||
- [ ] **前端易用性优化**. 更多高级功能,例如表格形式保存。
|
||||
- [ ] **日志自动清理**.
|
||||
- [ ] **跟多文档**.
|
||||
|
||||
## 未来计划
|
||||
|
||||
下一个版本有可能是 v0.6.0 的正式版本,但是没有确定。本次发布版本将会进行更多的测试,以保证其健壮性以及生产可用。
|
||||
|
||||
33
changelog/v0.6.0-beta.20211116.md
Normal file
33
changelog/v0.6.0-beta.20211116.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Change Log (v0.6.0-beta.20211116)
|
||||
|
||||
## Overview
|
||||
|
||||
This is the second beta release for the next major version v0.6.0 after the [first beta release](https://github.com/crawlab-team/crawlab/releases/tag/v0.6.0-beta.20210803). With more features and optimization coming in, the release of official version v0.6.0 is approaching soon.
|
||||
|
||||
## Enhancement
|
||||
|
||||
#### Backend
|
||||
|
||||
- [x] **Plugin Framework**. **Crawlab Plugin Framework (CPF)** has been released. See more info [here](https://docs-next.crawlab.cn/en/guide/use-crawlab/basic-concepts/plugin.html).
|
||||
- [x] **Git Integration**. Git integration is implemented as a built-in feature.
|
||||
- [x] **Scrapy Integration**. Scrapy integration is implemented as a plugin [spider-assistant](https://github.com/crawlab-team/plugin-spider-assistant).
|
||||
- [x] **Dependency Integration**. Dependency integration is implemented as a plugin [dependency](https://github.com/crawlab-team/plugin-dependency).
|
||||
- [x] **Notifications**. Notifications feature is implemented as a plugin [notification](https://github.com/crawlab-team/plugin-notification).
|
||||
- [x] **Documentation Site**. Set up [documentation site](https://docs-next.crawlab.cn/en).
|
||||
|
||||
#### Frontend
|
||||
- **Bug Fixing**.
|
||||
|
||||
#### TODOs
|
||||
- [ ] **Associated Tasks**. There will be main tasks and their sub-tasks if task mode is "all nodes" or "selected nodes".
|
||||
- [ ] **Crontab Editor**. Frontend component that visualize the crontab editing.
|
||||
- [ ] **Results Deduplication**.
|
||||
- [ ] **Environment Variables**.
|
||||
- [ ] **Internationalization**. Support Chinese.
|
||||
- [ ] **Frontend Utility Enhancement**. Advanced features such as saved table customization.
|
||||
- [ ] **Log Auto Cleanup**.
|
||||
- [ ] **More Documentation**.
|
||||
|
||||
## What Next
|
||||
|
||||
The next version could the official release of v0.6.0, but not determined yet. There will be more tests running against the current beta version to ensure robostness and production-ready deployment.
|
||||
@@ -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
frontend/.env
Normal file
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
VUE_APP_API_BASE_URL=
|
||||
@@ -1,2 +1 @@
|
||||
NODE_ENV='development'
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
NODE_ENV='production'
|
||||
VUE_APP_API_BASE_URL='VUE_APP_API_BASE_URL'
|
||||
VUE_APP_API_BASE_URL=/api
|
||||
|
||||
@@ -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,72 +1,27 @@
|
||||
{
|
||||
"name": "crawlab-frontend",
|
||||
"version": "0.6.0-beta.20210715",
|
||||
"private": false,
|
||||
"name": "@crawlab/app",
|
||||
"version": "0.6.0-beta.202111052150",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --port=8081",
|
||||
"serve:build:local": "vue-cli-service serve --port=8082 --model local",
|
||||
"serve": "vue-cli-service serve",
|
||||
"serve:dist": "serve dist",
|
||||
"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"
|
||||
"build:development": "vue-cli-service build --mode development",
|
||||
"build:docker": "vue-cli-service build --mode docker"
|
||||
},
|
||||
"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",
|
||||
"vuex": "^4.0.0-0"
|
||||
"crawlab-ui": "0.6.0-beta.20211113.2052",
|
||||
"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",
|
||||
"serve": "^13.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/public/fonts/FontAwesome.otf
Normal file
BIN
frontend/public/fonts/FontAwesome.otf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/fontawesome-webfont.eot
Normal file
BIN
frontend/public/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
2671
frontend/public/fonts/fontawesome-webfont.svg
Normal file
2671
frontend/public/fonts/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 434 KiB |
BIN
frontend/public/fonts/fontawesome-webfont.ttf
Normal file
BIN
frontend/public/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/fontawesome-webfont.woff
Normal file
BIN
frontend/public/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
frontend/public/fonts/fontawesome-webfont.woff2
Normal file
BIN
frontend/public/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
@@ -1,213 +1,214 @@
|
||||
<!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>
|
||||
<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;
|
||||
}
|
||||
<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-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 {
|
||||
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 {
|
||||
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(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(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(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(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(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(6) {
|
||||
animation-delay: calc(1s / 7 * 5 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(7) {
|
||||
animation-delay: calc(1s / 7 * 6 / 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 {
|
||||
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 {
|
||||
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(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(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(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 .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;
|
||||
}
|
||||
#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%;
|
||||
}
|
||||
@keyframes blink-loading {
|
||||
0% {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 50%;
|
||||
}
|
||||
50% {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes change-shape {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
@keyframes change-shape {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flip {
|
||||
0% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
@keyframes flip {
|
||||
0% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
2% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
2% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
7% {
|
||||
transform: rotate3d(1, 0, 0, 0);
|
||||
}
|
||||
7% {
|
||||
transform: rotate3d(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
23% {
|
||||
transform: rotate3d(1, 0, 0, 0);
|
||||
}
|
||||
23% {
|
||||
transform: rotate3d(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
27% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
27% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100% {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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 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 -->
|
||||
|
||||
2006
frontend/public/js/vue3-sfc-loader.js
Normal file
2006
frontend/public/js/vue3-sfc-loader.js
Normal file
File diff suppressed because one or more lines are too long
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,3 +0,0 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
</template>
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user