Merge pull request #1023 from crawlab-team/develop

Develop
This commit is contained in:
Marvin Zhang
2021-11-16 19:40:52 +08:00
committed by GitHub
447 changed files with 7227 additions and 29817 deletions

121
.gitignore vendored
View File

@@ -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/

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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...)

View File

@@ -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())
}

View File

@@ -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...)

View File

@@ -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

View File

@@ -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=

View File

@@ -1,7 +1,7 @@
{
"key": "master",
"is_master": true,
"name": "master",
"name": "Master Node",
"ip": "",
"mac": "",
"hostname": "",

View File

@@ -1,7 +1,7 @@
{
"key": "worker",
"key": "worker-01",
"is_master": false,
"name": "worker",
"name": "Worker Node 01",
"ip": "",
"mac": "",
"hostname": "",

View File

@@ -0,0 +1,10 @@
{
"key": "worker-02",
"is_master": false,
"name": "Worker Node 02",
"ip": "",
"mac": "",
"hostname": "",
"description": "",
"auth_key": "Crawlab2021!"
}

View File

@@ -37,7 +37,7 @@
- [ ] **Git 集成**. 将作为插件存在
- [ ] **Scrapy 集成**. 将作为插件存在
- [ ] **消息通知**. 将作为插件存在
- [ ] **关联人物**. 如果任务执行模式为 所有节点 指定节点那么将会有主任务和子任务之分
- [ ] **关联任务**. 如果任务执行模式为 所有节点 指定节点那么将会有主任务和子任务之分
- [ ] **Crontab 编辑器**. 可视化 Crontab 编辑的前端组件
- [ ] **结果去重**.
- [ ] **环境变量**.

View 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 的正式版本但是没有确定本次发布版本将会进行更多的测试以保证其健壮性以及生产可用

View 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.

View File

@@ -1,3 +0,0 @@
> 1%
last 2 versions
not dead

View File

@@ -1 +0,0 @@
node_modules/

View File

@@ -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
View File

@@ -0,0 +1 @@
VUE_APP_API_BASE_URL=

View File

@@ -1,2 +1 @@
NODE_ENV='development'
VUE_APP_API_BASE_URL=http://localhost:8000

View File

@@ -1,2 +1 @@
NODE_ENV='production'
VUE_APP_API_BASE_URL='VUE_APP_API_BASE_URL'
VUE_APP_API_BASE_URL=/api

View File

@@ -1,2 +0,0 @@
NODE_ENV='production'
VUE_APP_API_BASE_URL=http://localhost:8000

View File

@@ -1,2 +0,0 @@
NODE_ENV='test'
VUE_APP_API_BASE_URL=http://localhost:8000

View File

@@ -1,2 +1,3 @@
*/**/*.js
./src/i18n/**/*.ts
*/**/*.js
*.js

6
frontend/.gitignore vendored
View File

@@ -23,3 +23,9 @@ pnpm-debug.log*
*.sw?
tmp/
lib/
**/.DS_Store
**/.idea
**/dist
**/node_modules
**/package-lock.json

View File

@@ -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.

View File

@@ -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/).

View File

@@ -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"
}
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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 -->

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
<template>
<router-view/>
</template>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,3 +0,0 @@
type BasicType = 'primary' | 'success' | 'warning' | 'danger' | 'info';
type BasicEffect = 'dark' | 'light' | 'plain';
type BasicSize = 'mini' | 'small' | 'medium' | 'large';

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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