├── .gitignore ├── API Design Doc.md ├── Dockerfile ├── Makefile ├── README.md ├── Script Schema Design.md ├── Table Schema Design.md ├── cmd └── ticheck-server │ ├── api │ └── response.go │ ├── handler │ ├── cluster_handler.go │ ├── report_handler.go │ ├── session_handler.go │ ├── store_handler.go │ └── view_handler.go │ ├── main.go │ └── router │ └── router.go ├── config ├── app_config.go ├── execution_config.csv └── load_variables_template.sh ├── executor ├── edit_check_script.sh ├── edit_connection_config.sh ├── edit_execution_config.sh ├── executor.go ├── generate_report.py └── new_check_script.sh ├── go.mod ├── go.sum ├── install.sh ├── internal ├── model │ ├── check_data.go │ ├── check_history.go │ ├── cluster.go │ ├── cluster_checklist.go │ ├── db.go │ ├── dict.go │ ├── jsontime.go │ ├── probe.go │ ├── scheduler.go │ ├── user.go │ └── util.go └── service │ └── cron.go ├── logpath.sh ├── probes ├── example │ └── example1 │ │ ├── example1.sh │ │ ├── package.json │ │ └── readme.md └── local │ ├── alive_pd_number │ ├── alive_pd_number.py │ ├── package.json │ └── readme.md │ ├── alive_tidb_number │ ├── alive_tidb_number.py │ ├── package.json │ └── readme.md │ ├── alive_tikv_number │ ├── alive_tikv_number.py │ ├── package.json │ └── readme.md │ ├── available_memory │ ├── available_memory.py │ ├── package.json │ └── readme.md │ ├── failed_query_type │ ├── failed_query_type.py │ ├── package.json │ └── readme.md │ ├── goroutine_number │ ├── goroutine_number.py │ ├── package.json │ └── readme.md │ ├── long_ddl_job │ ├── long_ddl_job.sh │ ├── package.json │ └── readme.md │ ├── network_in │ ├── network_in.py │ ├── package.json │ └── readme.md │ ├── network_out │ ├── network_out.py │ ├── package.json │ └── readme.md │ ├── no_primary_key │ ├── no_primary_key.sh │ ├── package.json │ └── readme.md │ ├── running_sql_5min │ ├── package.json │ ├── readme.md │ └── running_sql_5min.sh │ ├── succeeded_query_number │ ├── package.json │ ├── readme.md │ └── succeeded_query_number.py │ ├── tidb_connections │ ├── package.json │ ├── readme.md │ └── tidb_connections.py │ └── tikv_region_number │ ├── package.json │ ├── readme.md │ └── tikv_region_number.py ├── run.sh ├── store └── ticheck.db ├── util ├── file.go └── logutil │ └── log.go └── web ├── .browserslistrc ├── .editorconfig ├── .env.development ├── .env.preview ├── .eslintrc.js ├── .eslintrc.json ├── babel.config.js ├── config ├── plugin.config.js └── themePluginConfig.js ├── jest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── avatar2.jpg ├── index.html └── logo.png ├── src ├── App.vue ├── api │ ├── check.js │ ├── cluster.js │ ├── login.js │ └── manage.js ├── assets │ ├── background.svg │ ├── icons │ │ ├── bx-analyse.svg │ │ ├── python.png │ │ └── shell.png │ ├── logo.png │ ├── logo.svg │ └── logo1.png ├── components │ ├── ArticleListContent │ │ ├── ArticleListContent.vue │ │ └── index.js │ ├── AvatarList │ │ ├── Item.jsx │ │ ├── List.jsx │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── Charts │ │ ├── Bar.vue │ │ ├── ChartCard.vue │ │ ├── Liquid.vue │ │ ├── MiniArea.vue │ │ ├── MiniBar.vue │ │ ├── MiniProgress.vue │ │ ├── MiniSmoothArea.vue │ │ ├── Radar.vue │ │ ├── RankList.vue │ │ ├── TagCloud.vue │ │ ├── TransferBar.vue │ │ ├── Trend.vue │ │ ├── chart.less │ │ └── smooth.area.less │ ├── Dialog.js │ ├── Editor │ │ ├── QuillEditor.vue │ │ └── WangEditor.vue │ ├── Ellipsis │ │ ├── Ellipsis.vue │ │ ├── index.js │ │ └── index.md │ ├── FooterToolbar │ │ ├── FooterToolBar.vue │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── GlobalFooter │ │ └── index.vue │ ├── GlobalHeader │ │ ├── AvatarDropdown.vue │ │ └── RightContent.vue │ ├── IconSelector │ │ ├── IconSelector.vue │ │ ├── README.md │ │ ├── icons.js │ │ └── index.js │ ├── MultiTab │ │ ├── MultiTab.vue │ │ ├── events.js │ │ ├── index.js │ │ └── index.less │ ├── NProgress │ │ └── nprogress.less │ ├── NoticeIcon │ │ ├── NoticeIcon.vue │ │ └── index.js │ ├── NumberInfo │ │ ├── NumberInfo.vue │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── Other │ │ └── CarbonAds.vue │ ├── PageLoading │ │ └── index.jsx │ ├── Search │ │ ├── GlobalSearch.jsx │ │ └── index.less │ ├── SelectLang │ │ ├── index.jsx │ │ └── index.less │ ├── SettingDrawer │ │ ├── SettingDrawer.vue │ │ ├── SettingItem.vue │ │ ├── index.js │ │ ├── settingConfig.js │ │ └── themeColor.js │ ├── StandardFormRow │ │ ├── StandardFormRow.vue │ │ └── index.js │ ├── Table │ │ ├── README.md │ │ └── index.js │ ├── TagSelect │ │ ├── TagSelectOption.jsx │ │ └── index.jsx │ ├── TextArea │ │ ├── index.jsx │ │ └── style.less │ ├── Tree │ │ └── Tree.jsx │ ├── Trend │ │ ├── Trend.vue │ │ ├── index.js │ │ ├── index.less │ │ └── index.md │ ├── _util │ │ └── util.js │ ├── index.js │ ├── index.less │ └── tools │ │ └── TwoStepCaptcha.vue ├── config │ ├── defaultSettings.js │ └── router.config.js ├── core │ ├── bootstrap.js │ ├── directives │ │ └── action.js │ ├── icons.js │ ├── lazy_use.js │ ├── permission │ │ └── permission.js │ └── use.js ├── global.less ├── layouts │ ├── BasicLayout.less │ ├── BasicLayout.vue │ ├── BlankLayout.vue │ ├── MenuLayout.vue │ ├── PageView.vue │ ├── RouteView.vue │ ├── UserLayout.vue │ └── index.js ├── locales │ ├── index.js │ └── lang │ │ ├── en-US.js │ │ ├── en-US │ │ ├── account.js │ │ ├── account │ │ │ └── settings.js │ │ ├── check.js │ │ ├── check │ │ │ ├── execute.js │ │ │ ├── history.js │ │ │ └── probe.js │ │ ├── cluster.js │ │ ├── cluster │ │ │ ├── info.js │ │ │ ├── list.js │ │ │ └── scheduler.js │ │ ├── dashboard.js │ │ ├── dashboard │ │ │ └── analysis.js │ │ ├── dict.js │ │ ├── form.js │ │ ├── form │ │ │ └── basicForm.js │ │ ├── global.js │ │ ├── menu.js │ │ ├── result.js │ │ ├── result │ │ │ ├── fail.js │ │ │ └── success.js │ │ ├── setting.js │ │ ├── store.js │ │ └── user.js │ │ ├── zh-CN.js │ │ └── zh-CN │ │ ├── account.js │ │ ├── account │ │ └── settings.js │ │ ├── check.js │ │ ├── check │ │ ├── execute.js │ │ ├── history.js │ │ └── probe.js │ │ ├── cluster.js │ │ ├── cluster │ │ ├── info.js │ │ ├── list.js │ │ └── scheduler.js │ │ ├── dashboard.js │ │ ├── dashboard │ │ └── analysis.js │ │ ├── form.js │ │ ├── form │ │ └── basicForm.js │ │ ├── global.js │ │ ├── menu.js │ │ ├── result.js │ │ ├── result │ │ ├── fail.js │ │ └── success.js │ │ ├── setting.js │ │ ├── store.js │ │ └── user.js ├── main.js ├── mock │ ├── index.js │ ├── services │ │ ├── article.js │ │ ├── auth.js │ │ ├── manage.js │ │ ├── other.js │ │ ├── tagCloud.js │ │ └── user.js │ └── util.js ├── permission.js ├── router │ ├── README.md │ ├── generator-routers.js │ └── index.js ├── store │ ├── app-mixin.js │ ├── device-mixin.js │ ├── getters.js │ ├── i18n-mixin.js │ ├── index.js │ ├── modules │ │ ├── app.js │ │ ├── async-router.js │ │ ├── permission.js │ │ └── user.js │ └── mutation-types.js ├── utils │ ├── axios.js │ ├── domUtil.js │ ├── filter.js │ ├── request.js │ ├── routeConvert.js │ ├── screenLog.js │ ├── util.js │ └── utils.less └── views │ ├── 404.vue │ ├── check │ ├── ExecuteCheck.vue │ ├── History.vue │ ├── ProbeAdd.vue │ ├── ProbeList.vue │ └── ReportDetail.vue │ ├── cluster │ ├── Addon.vue │ ├── Index.vue │ ├── Info.vue │ ├── List.vue │ ├── Scheduler.vue │ ├── Setting.vue │ └── components │ │ └── Info.vue │ ├── exception │ ├── 403.vue │ ├── 404.vue │ └── 500.vue │ ├── store │ ├── Custom.vue │ ├── Index.vue │ ├── Local.vue │ └── Remote.vue │ └── user │ ├── Login.vue │ ├── Register.vue │ └── RegisterResult.vue └── vue.config.js /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.19-alpine3.15 as web 2 | 3 | WORKDIR /webpack 4 | COPY ./web . 5 | #RUN yarn config set registry https://registry.npm.taobao.org 6 | RUN yarn install 7 | RUN yarn build 8 | 9 | FROM golang:1.16-buster as service 10 | WORKDIR /app 11 | COPY . . 12 | RUN make build 13 | 14 | FROM ubuntu:20.04 as final 15 | 16 | ENV DEBIAN_FRONTEND noninteractive 17 | 18 | RUN apt update \ 19 | && apt install -y python3 \ 20 | python3-pip \ 21 | mysql-client \ 22 | expect \ 23 | && ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime \ 24 | && echo 'Etc/UTC' /etc/timezone \ 25 | && rm -rf /var /lib/apt/list/* 26 | RUN pip3 install requests 27 | 28 | WORKDIR /ticheck 29 | COPY --from=web /webpack/dist ./web/dist 30 | COPY --from=service /app/bin/ticheck-server ./service/bin/ 31 | COPY ./probes ./probes 32 | COPY ./config ./config 33 | COPY ./executor ./excutor 34 | COPY ./logpath.sh . 35 | COPY ./run.sh . 36 | 37 | WORKDIR /ticheck/service/bin 38 | # Create data dir 39 | RUN mkdir ../../store 40 | # Set gin mode 41 | ENV GIN_MODE=release 42 | 43 | EXPOSE 8081 44 | 45 | ENTRYPOINT ["./ticheck-server"] 46 | 47 | 48 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT="TiCheck" 2 | VERSION=0.1.0 3 | 4 | default: 5 | echo "Welcome to ${PROJECT} v${VERSION}" 6 | 7 | fmt: 8 | echo "Formatting..." 9 | @clang-format -i *.c *.h 10 | 11 | install: 12 | @echo "Installing ${PROJECT}..." 13 | @./install.sh 14 | @echo "Install Done." 15 | 16 | build: 17 | @echo "Building ${PROJECT}..." 18 | @cd ./cmd/ticheck-server && go build -o ../../bin/ticheck-server 19 | @cd ../../ 20 | @cd ./web && npm install && npm run build 21 | @echo "Build Done." 22 | 23 | test: 24 | echo "Testing ${PROJECT}..." 25 | echo "Test Done." 26 | 27 | .PHONY: default install test -------------------------------------------------------------------------------- /Script Schema Design.md: -------------------------------------------------------------------------------- 1 | ### 组件设计规范 2 | 3 | 每个组件由以下3种文件构成: 4 | 5 | - *元信息文件(package.json),描述了组件的基本信息 6 | 7 | - *脚本文件(python或者shell),至少要有一个主文件 8 | 9 | - 说明文档(readme.md),产品使用手册、原理解析、参数、安全风险等 10 | 11 | 12 | 13 | #### package.json格式 14 | ``` 15 | { 16 | "_id": "alive_pd_number", //id要和打包名称一致 17 | "name": "存活的PD节点数量", 18 | "author": { 19 | "name": "DigitalChina", 20 | "email": "heao@digitalchina.com" 21 | }, 22 | "description": "this script description.", 23 | "files": [ 24 | "index.js", 25 | "lib/" 26 | ], 27 | "main": "index.js", //入口文件 28 | "tags": [ 29 | "cluster", 30 | "network", 31 | "running_state", 32 | "others" 33 | ], 34 | "rules": [ 35 | { 36 | "operator": 3, //0-无,1-等于,2-大于,3-大于等于,4-小于,5-小于等于 37 | "threshold": "3", 38 | "args": [] 39 | } 40 | ], 41 | "homepage": "", 42 | "version": "1.0.1", 43 | "createTime": "", 44 | "updateTime": "" 45 | } 46 | 47 | ``` 48 | 49 | 50 | ### 输入输出规范 51 | 52 | 我们为每一个Probe制定了统一的输入输出格式,会把一些全局参数传给要运行的脚本,方然也支持给脚本自定义启动参数。与此同时,脚本输出信息也要符合一定要求才能被TiCheck捕获到,这和是否能正确判断阈值至关重要。 53 | 54 | #### 默认输入参数 55 | 56 | 针对`shell`和`python`脚本,我们会统一传入三个参数,他们依次是: 57 | 58 | - BasePath,比如`/data/tidb/ticheck`,程序运行的主目录 59 | - MysqlLoginPath,比如`tidb-login`,tidb集群的登录连接信息,可以使用login path的方式登录执行sql 60 | - PremetheusPath,比如`http://10.0.0.1:9090`,tidb集群的Prometheus地址,可以使用Psql查询集群监控指标 61 | 62 | > **注意:** 63 | > 这三个参数需要按传入顺序获取,比如在shell中依次是`$1`、`$2`、`$3`,在python中依次是`sys.argv[1]`、`sys.argv[2]`、`sys.argv[3]`。 64 | 65 | #### 自定义输入参数 66 | 67 | 脚本自定义参数在`package.json`的`rules.args`中设置,可以设置多个自定义参数,目前不支持使用`--参数名=参数值`或者`--参数名 参数值`的形式,**每个参数只填写参数值即可,这些参数会追加到默认参数后面传入,也就是说它们的顺序在3以后,获取方式参考前面的描述**。 68 | 69 | #### 巡检值输出格式 70 | 71 | TiCheck能够识别两种格式的输出信息。 72 | 73 | 第一种是本次巡检的实际值,这个结果会用来和设置的阈值做对比,得出巡检结果。它的格式是`[tck_result:]xxxx=yy`。参考示例: 74 | ``` 75 | // shell 76 | echo "[tck_result:] TiDB节点数量=2" 77 | echo "$i" | awk '{print [tck_result:] $1"."$2=无主键}' 78 | 79 | // python 80 | print ("[tck_result:] TiDB节点数量=2") 81 | ``` 82 | 每一行仅输出一个巡检项实际值,有多个巡检项的情况下请分多行输出,例如需要检查每一个节点的网络流量的时候每一个节点都应该是一行单独的输出。 83 | 84 | 第二种是脚本需要记录的日志,这些信息通常用于跟踪脚本执行逻辑是否符合预期,它的格式是`[tck_log:]xxxxx`,目前只能在go控制台中打印出来,后面会考虑持久化到数据库中,给巡检结果提供查看入口。 -------------------------------------------------------------------------------- /cmd/ticheck-server/handler/view_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type ViewHandler struct{} 10 | 11 | func (v *ViewHandler) GetIndex(c *gin.Context) { 12 | c.HTML(http.StatusOK, "frontend", nil) 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /config/app_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var GlobalConfig *AppConfig 4 | 5 | type AppConfig struct { 6 | WorkDir string 7 | Port int 8 | } 9 | 10 | func (c *AppConfig) GetProbePrefix() string { 11 | return c.WorkDir + "probes" 12 | } 13 | 14 | func (c *AppConfig) GetStorePath() string { 15 | return c.WorkDir + "store/ticheck.db" 16 | } 17 | -------------------------------------------------------------------------------- /config/execution_config.csv: -------------------------------------------------------------------------------- 1 | 是否开启:检测类别:检测项目:检测脚本:检测方式:检测阈值:检测脚本参数 2 | ENABLE:集群:存活的TiDB数量:alive_tidb_number.py:等于:2: 3 | ENABLE:集群:存活的TiKV数量:alive_tikv_number.py:等于:3: 4 | ENABLE:集群:存活的PD数量:alive_pd_number.py:等于:3: 5 | ENABLE:网络:今天网卡流入流量(MB):network_in.py:小于等于:4096: 6 | ENABLE:网络:今天网卡流出流量(MB):network_out.py:小于等于:4096: 7 | ENABLE:运行状态:表没有主键:no_primary_key.sh:无数据:NA: 8 | ENABLE:运行状态:正在执行且超过5分钟的SQL:running_sql_5min.sh:无数据:NA: 9 | ENABLE:运行状态:执行超过1h的DDL:long_ddl_job.sh:无数据:NA: 10 | ENABLE:运行状态:TiKV Region数量:tikv_region_number.py:小于等于:30000: 11 | ENABLE:运行状态:goroutine数量:goroutine_number.py:小于等于:50000: 12 | ENABLE:运行状态:每个tidb的连接数:tidb_connections.py:小于等于:800: 13 | DISABLE:运行状态:今天成功的Query数:succeeded_query_number.py:大于等于:0: 14 | ENABLE:运行状态:今天失败Query的类型:failed_query_type.py:无数据:NA: 15 | ENABLE:运行状态:内存剩余绝对值(MB):available_memory.py:大于等于:10240: 16 | -------------------------------------------------------------------------------- /config/load_variables_template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OS_USER= 4 | PROMETHEUS_ADDRESS= 5 | MYSQL_LOGIN_PATH= 6 | -------------------------------------------------------------------------------- /executor/edit_check_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # load current path as base path 4 | BASE_PATH=$(cd "$(dirname "$0")" || exit 1; pwd) 5 | 6 | code "$BASE_PATH"/../script/ -------------------------------------------------------------------------------- /executor/edit_connection_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # load current path as base path 4 | BASE_PATH=$(cd "$(dirname "$0")" || exit 1; pwd) 5 | 6 | code "$BASE_PATH"/../config/load_variables.sh -------------------------------------------------------------------------------- /executor/edit_execution_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # load current path as base path 4 | BASE_PATH=$(cd "$(dirname "$0")" || exit 1; pwd) 5 | 6 | code "$BASE_PATH"/../config/execution_config.csv -------------------------------------------------------------------------------- /executor/new_check_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # load current path as base path 4 | BASE_PATH=$(cd "$(dirname "$0")" || exit 1; pwd) 5 | 6 | cat << EOF > ../script/new_script 7 | # parameters to this script: 8 | # [path to TiCheck] [MySQL login-path] [Prometheus address] [check arguments from config] 9 | # 10 | # DONT FORGET TO CHANGE FILENAME AND SAVE IT 11 | # DONT FORGET TO ENABLE IT IN EXECUTION CONFIG 12 | EOF 13 | 14 | code "$BASE_PATH"/../script/new_script -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module TiCheck 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-contrib/multitemplate v0.0.0-20220203231411-2a098756d076 7 | github.com/gin-gonic/gin v1.7.7 8 | github.com/go-sql-driver/mysql v1.6.0 9 | github.com/gorilla/websocket v1.4.2 10 | github.com/kr/text v0.2.0 // indirect 11 | github.com/mattn/go-sqlite3 v1.14.12 12 | github.com/natefinch/lumberjack v2.0.0+incompatible // indirect 13 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 14 | github.com/robfig/cron/v3 v3.0.0 // indirect 15 | go.uber.org/atomic v1.9.0 // indirect 16 | go.uber.org/multierr v1.8.0 // indirect 17 | go.uber.org/zap v1.21.0 // indirect 18 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 19 | gopkg.in/yaml.v2 v2.4.0 // indirect 20 | gorm.io/driver/sqlite v1.3.1 21 | gorm.io/gorm v1.23.1 22 | ) 23 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | RED='\033[0;31m' 3 | NC='\033[0m' 4 | GREEN='\033[32m' 5 | YELLOW='\033[33m' 6 | 7 | check_dependency(){ 8 | echo "+--------------------------------+-------------+" 9 | if ! type $1 >/dev/null 2>&1; then 10 | printf "+ %-30s |${RED} %-10s ${NC} +\n" $1 "fail"; 11 | else 12 | printf "+ %-30s |${GREEN} %-10s ${NC} +\n" $1 "pass"; 13 | fi 14 | } 15 | 16 | 17 | main(){ 18 | # dependency install check 19 | echo "+--------------------------------+-------------+" 20 | printf "+ %-30s | %-10s +\n" "Dependency detection" "State"; 21 | check_dependency npm 22 | check_dependency python3 23 | check_dependency pip 24 | check_dependency mysql 25 | check_dependency mysql_config_editor 26 | check_dependency expect 27 | echo "+--------------------------------+-------------+" 28 | echo -e "${YELLOW}Warning: ${NC}If the above items fail to be detected, it may affect the normal operation of the inspection function. \n" 29 | 30 | # build project 31 | make build 32 | 33 | # run server 34 | result=$(ps -aux | grep ticheck-server | grep -v "grep" | wc -l) 35 | if [[ $result -gt 0 ]];then 36 | kill -9 $(ps aux | grep ticheck-server | grep -v "grep" | awk '{print $2}') 37 | fi 38 | export GIN_MODE=release 39 | nohup ./bin/ticheck-server --work-dir=$(pwd)'/' --port=8066 & 40 | echo -e "${GREEN}TiCheck Successfuly installed. Now visit http://localhost:8066 to enjoy it ${NC}" 41 | } 42 | 43 | main; 44 | 45 | exit 0 -------------------------------------------------------------------------------- /internal/model/check_data.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type CheckData struct { 8 | ID uint `gorm:"primarykey" json:"id"` 9 | HistoryID uint `gorm:"not null" json:"history_id"` 10 | ClusterID uint `gorm:"not null" json:"cluster_id"` 11 | ProbeID string `gorm:"not null" json:"probe_id"` 12 | CheckTime time.Time `gorm:"not null" json:"check_time"` 13 | CheckTag string `gorm:"not null" json:"check_tag"` 14 | CheckName string `gorm:"not null" json:"check_name"` 15 | Comparator `gorm:"embedded" json:",inline"` 16 | Duration int64 `gorm:"not null" json:"duration"` // unit is ms 17 | CheckItem string `gorm:"not null" json:"check_item"` 18 | CheckValue string `json:"check_value"` // null: script no output 19 | CheckStatus int `gorm:"not null" json:"check_status"` //-1:脚本运行异常,错误信息在CheckValue,0:正常, 1:异常_已有, 2:异常_新增 20 | } 21 | 22 | func (cd *CheckData) TableName() string { 23 | return "check_data" 24 | } 25 | 26 | func (cd *CheckData) GetDataByHistoryID(id int) ([]CheckData, error) { 27 | var cds []CheckData 28 | 29 | err := DbConn.Where("history_id = ?", id).Find(&cds).Error 30 | 31 | if err != nil { 32 | return cds, err 33 | } 34 | 35 | return cds, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/model/dict.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | var ( 4 | Dict_ProbeTags = map[string]string{ 5 | "cluster": "Cluster", 6 | "network": "Network", 7 | "running_state": "RunningState", 8 | "others": "Others", 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /internal/model/jsontime.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | const ( 11 | DateFormat = "2006-01-02" 12 | TimeFormat = "2006-01-02 15:04:05" 13 | ) 14 | 15 | type JsonTime time.Time 16 | 17 | func (t JsonTime) String() string { 18 | return time.Time(t).Format(TimeFormat) 19 | } 20 | 21 | //MarshalJSON 实现它的json序列化方法 22 | func (t JsonTime) MarshalJSON() ([]byte, error) { 23 | var stamp = fmt.Sprintf("\"%s\"", time.Time(t).Format(TimeFormat)) 24 | return []byte(stamp), nil 25 | } 26 | 27 | func (t *JsonTime) UnmarshalJSON(data []byte) (err error) { 28 | now, err := time.ParseInLocation(`"`+TimeFormat+`"`, string(data), time.Local) 29 | *t = JsonTime(now) 30 | return 31 | } 32 | 33 | func (t JsonTime) Value() (driver.Value, error) { 34 | // MyTime 转换成 time.Time 类型 35 | tTime := time.Time(t) 36 | return tTime.Format(TimeFormat), nil 37 | } 38 | 39 | func (t *JsonTime) Scan(v interface{}) error { 40 | switch vt := v.(type) { 41 | case time.Time: 42 | // 字符串转成 time.Time 类型 43 | *t = JsonTime(vt) 44 | default: 45 | return errors.New("类型处理错误") 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/model/scheduler.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Scheduler struct { 8 | ID uint `gorm:"primarykey"` 9 | Name string `gorm:"not null"` 10 | ClusterID uint `gorm:"not null"` 11 | CronExpression string `gorm:"not null"` 12 | IsEnabled int `gorm:"not null"` 13 | Creator string `gorm:"not null"` 14 | RunCount int `gorm:"not null"` 15 | CreateTime time.Time `gorm:"not null"` 16 | } 17 | 18 | func (s *Scheduler) QuerySchedulersByClusterID(id int) (schedulerList []Scheduler, err error) { 19 | err = DbConn. 20 | Where("cluster_id = ?", id). 21 | Order("create_time asc"). 22 | Find(&schedulerList). 23 | Error 24 | if err != nil { 25 | return schedulerList, err 26 | } 27 | 28 | return schedulerList, nil 29 | } 30 | 31 | func (s *Scheduler) AddScheduler() error { 32 | err := DbConn.Create(&s).Error 33 | if err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | func (s *Scheduler) UpdateScheduler() error { 40 | updateData := map[string]interface{}{ 41 | "name": s.Name, 42 | "is_enabled": s.IsEnabled, 43 | "cron_expression": s.CronExpression, 44 | } 45 | 46 | err := DbConn.Model(&s). 47 | Updates(updateData). 48 | Error 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (s *Scheduler) DeleteScheduler() error { 57 | err := DbConn.Delete(&s).Error 58 | if err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | func (s *Scheduler) TableName() string { 65 | return "schedulers" 66 | } 67 | -------------------------------------------------------------------------------- /internal/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | type User struct { 10 | ID uint `gorm:"primarykey"` 11 | UserName string `gorm:"unique;not null"` 12 | UserPassword string `gorm:"not null"` 13 | FullName string `gorm:"not null"` 14 | Email string `gorm:"not null"` 15 | IsEnabled int `gorm:"not null"` // 0: Disabled, 1: Enabled 16 | Creator string `gorm:"not null"` 17 | CreateTime time.Time `gorm:"not null"` 18 | } 19 | 20 | func (u *User) TableName() string { 21 | return "users" 22 | } 23 | 24 | 25 | // VerifyUser Check the login permission of the user 26 | func (u *User) VerifyUser() bool { 27 | var total int64 28 | err := DbConn.Model(&u).Where("user_name = ? and user_password = ? and is_enabled = 1 ", u.UserName, fmt.Sprintf("%X", sha1.Sum([]byte(u.UserPassword)))).Count(&total).Error 29 | if err != nil { 30 | return false 31 | } 32 | 33 | if total < 1 { 34 | return false 35 | } 36 | 37 | return true 38 | } 39 | 40 | func (u *User) GetUserInfoByName() error { 41 | err := DbConn.Select("user_name,full_name,email,is_enabled").Where("user_name = ?", u.UserName).Limit(1).Find(u).Error 42 | if err != nil { 43 | return err 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/model/util.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type Comparator struct { 11 | // represents way to compare threshold and result 12 | // 0: NA, 1: eq. 2: g, 3: ge, 4: l, 5: le 13 | Operator int `json:"operator"` 14 | Threshold string `json:"threshold"` 15 | Arg string `json:"arg,omitempty"` 16 | } 17 | 18 | type Paginator struct { 19 | PageSize uint 20 | PageNum uint 21 | Total uint 22 | Rows interface{} 23 | Filters map[interface{}][]interface{} 24 | Pager func(db *gorm.DB) *gorm.DB 25 | 26 | Err error 27 | } 28 | 29 | func (p *Paginator) ApplyQuery(db *gorm.DB, dest interface{}) *Paginator { 30 | for f := range p.Filters { 31 | db = db.Where(f, p.Filters[f]...) 32 | } 33 | 34 | var total int64 35 | err := db.Count(&total).Error 36 | p.Err = err 37 | if err == nil { 38 | p.Total = uint(total) 39 | } 40 | 41 | err = db.Scopes(p.Pager).Find(dest).Error 42 | p.Err = err 43 | if err == nil { 44 | p.Rows = dest 45 | } 46 | 47 | return p 48 | } 49 | 50 | func (p *Paginator) AddFilter(query interface{}, args ...interface{}) { 51 | if p.Filters == nil { 52 | p.Filters = make(map[interface{}][]interface{}) 53 | } 54 | p.Filters[query] = args 55 | } 56 | 57 | // func (p *Paginator) ApplyFilters(db *gorm.DB) *gorm.DB { 58 | // for f := range p.Filters { 59 | // db = db.Where(f, p.Filters[f]...) 60 | // } 61 | // return db 62 | // } 63 | 64 | func (p *Paginator) SetPager(r *http.Request, order string) { 65 | p.Pager = func(db *gorm.DB) *gorm.DB { 66 | page, _ := strconv.Atoi(r.URL.Query().Get("page")) 67 | if page == 0 { 68 | page = 1 69 | } 70 | 71 | pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) 72 | switch { 73 | case pageSize > 100: 74 | pageSize = 100 75 | case pageSize <= 0: 76 | pageSize = 10 77 | } 78 | 79 | offset := (page - 1) * pageSize 80 | return db.Order(order).Offset(offset).Limit(pageSize) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/service/cron.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "TiCheck/executor" 5 | "TiCheck/internal/model" 6 | "github.com/robfig/cron/v3" 7 | "time" 8 | ) 9 | 10 | var CronService SchedulerCron 11 | 12 | type SchedulerCron struct { 13 | Cron *cron.Cron 14 | Tasks []SchedulerTask 15 | } 16 | 17 | type SchedulerTask struct { 18 | ID cron.EntryID 19 | SchedulerID uint 20 | Spec string 21 | JobFunc func() 22 | } 23 | 24 | // Initialize initial a cron service 25 | func (sc *SchedulerCron) Initialize() { 26 | sc.Cron = cron.New(cron.WithSeconds(), cron.WithLocation(time.Local)) 27 | sc.Cron.Start() 28 | // todo check if there is a task in the scheduler table, if so,add it into the cron task queue 29 | } 30 | 31 | //AddTask add a cron task to cron service by scheduler 32 | func (sc *SchedulerCron) AddTask(scheduler model.Scheduler) error { 33 | job := CreateJob(scheduler) 34 | taskID, err := sc.Cron.AddFunc(scheduler.CronExpression, job) 35 | if err != nil { 36 | return err 37 | } 38 | var task = SchedulerTask{ 39 | taskID, 40 | scheduler.ID, 41 | scheduler.CronExpression, 42 | job, 43 | } 44 | sc.Tasks = append(sc.Tasks, task) 45 | return nil 46 | } 47 | 48 | // RemoveTask remove a task from cron service by task info 49 | func (sc *SchedulerCron) RemoveTask(task SchedulerTask) { 50 | sc.Cron.Remove(task.ID) 51 | } 52 | 53 | func (sc *SchedulerCron) StopAll() { 54 | sc.Cron.Stop() 55 | } 56 | 57 | // CreateJob create a cron job by scheduler cron expression, return a job func 58 | func CreateJob(scheduler model.Scheduler) func() { 59 | jobFunc := func() { 60 | exe := executor.CreateClusterExecutor(scheduler.ClusterID, scheduler.ID) 61 | resultCh := make(chan executor.CheckResult, 10) 62 | // ctx := context.WithValue(context.Background(), "", "") 63 | go exe.Execute(resultCh) 64 | } 65 | return jobFunc 66 | } 67 | -------------------------------------------------------------------------------- /logpath.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ##============================================================================================== 4 | ## load login-path variables 5 | ##============================================================================================== 6 | LOGIN_PATH_NAME=$1 7 | LOGIN_USER=$2 8 | HOST=$3 9 | PORT=$4 10 | PASSWD=$5 11 | 12 | ##============================================================================================== 13 | ## remove MySQL login-path first 14 | ##============================================================================================== 15 | mysql_config_editor remove --login-path="${LOGIN_PATH_NAME}" 16 | 17 | ##============================================================================================== 18 | ## generate MySQL login-path 19 | ##============================================================================================== 20 | expect -c " 21 | spawn mysql_config_editor set --login-path=${LOGIN_PATH_NAME} --user=${LOGIN_USER} --host=${HOST} --port=${PORT} --password 22 | expect -nocase \"Enter Password:\" 23 | send \"${PASSWD}\r\" 24 | interact 25 | " 26 | -------------------------------------------------------------------------------- /probes/example/example1/example1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BasePath=$1 4 | MysqlLoginPath=$2 5 | PremetheusPath=$3 6 | 7 | MyArg1=$4 8 | MyArg2=$5 9 | 10 | # you can use MysqlLoginPath to login tidb and run sql query 11 | 12 | echo "[tck_result:]example1 script check value1=0" 13 | echo "[tck_result:]example1 script check value2=10" -------------------------------------------------------------------------------- /probes/example/example1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "example1", 3 | "name": "脚本示例1", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "heao@digitalchina.com" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "index.js", 11 | "lib/" 12 | ], 13 | "main": "example1.sh", 14 | "tags": [ 15 | "集群", 16 | "网络", 17 | "运行状态" 18 | ], 19 | "rules": [ 20 | { 21 | "operator": 3, 22 | "threshold": "3", 23 | "args": [] 24 | } 25 | ], 26 | "homepage": "", 27 | "version": "1.0.1", 28 | "createTime": "", 29 | "updateTime": "" 30 | } -------------------------------------------------------------------------------- /probes/example/example1/readme.md: -------------------------------------------------------------------------------- 1 | ### example1使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/alive_pd_number/alive_pd_number.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | # base_path = sys.argv[1] 7 | # mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | pql = 'sum(probe_success{group="pd"})' 10 | 11 | try: 12 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 13 | 14 | for result in response.json()['data']['result']: 15 | result_value = result['value'][1] 16 | print ("[tck_result:] PD节点数量="+result_value) 17 | 18 | except : 19 | sys.exit(1) 20 | -------------------------------------------------------------------------------- /probes/local/alive_pd_number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "alive_pd_number", 3 | "name": "存活的PD节点数量", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "alive_pd_number.py" 11 | ], 12 | "main": "alive_pd_number.py", 13 | "tags": [ 14 | "cluster" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 1, 19 | "threshold": "3", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/alive_pd_number/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | 这里是一段代码: 4 | 5 | ```c# 6 | public class Main{ 7 | 8 | 9 | } 10 | ``` -------------------------------------------------------------------------------- /probes/local/alive_tidb_number/alive_tidb_number.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'sum(probe_success{group="tidb"})' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_value = result['value'][1] 17 | print ("[tck_result:] TiDB节点数量="+result_value) 18 | except: 19 | sys.exit(1) 20 | -------------------------------------------------------------------------------- /probes/local/alive_tidb_number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "alive_tidb_number", 3 | "name": "存活的TiDB节点数量", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "alive_tidb_number.py" 11 | ], 12 | "main": "alive_tidb_number.py", 13 | "tags": [ 14 | "cluster" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 1, 19 | "threshold": "2", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/alive_tidb_number/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/alive_tikv_number/alive_tikv_number.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'sum(probe_success{group="tikv"})' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_value = result['value'][1] 17 | print ("[tck_result:] TiKV节点数量="+result_value) 18 | except: 19 | sys.exit(1) 20 | -------------------------------------------------------------------------------- /probes/local/alive_tikv_number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "alive_tikv_number", 3 | "name": "存活的TiKV节点数量", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "alive_tikv_number.py" 11 | ], 12 | "main": "alive_tikv_number.py", 13 | "tags": [ 14 | "cluster" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 1, 19 | "threshold": "3", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/alive_tikv_number/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/available_memory/available_memory.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | # base_path = sys.argv[1] 7 | # mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'node_memory_MemAvailable_bytes / 1024 / 1024' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_instance = result['metric']['instance'] 17 | result_value = float(result['value'][1]) 18 | print ("[tck_result:] "+result_instance+"="+"{:.3f}".format(result_value)) 19 | except: 20 | sys.exit(1) 21 | -------------------------------------------------------------------------------- /probes/local/available_memory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "available_memory", 3 | "name": "内存剩余绝对值(MB)", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "available_memory.py" 11 | ], 12 | "main": "available_memory.py", 13 | "tags": [ 14 | "running_sate" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 3, 19 | "threshold": "10240", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/available_memory/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/failed_query_type/failed_query_type.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'sum(increase(tidb_server_execute_error_total[1d])) by (type, instance) > 0' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_instance = result['metric']['instance'] 17 | result_type = result['metric']['type'] 18 | result_value = result['value'][1].split('.')[0] 19 | print ("[tck_result:] "+result_instance + result_type+"="+result_value) 20 | except: 21 | sys.exit(1) 22 | -------------------------------------------------------------------------------- /probes/local/failed_query_type/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "failed_query_type", 3 | "name": "今天失败Query的类型", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "failed_query_type.py" 11 | ], 12 | "main": "failed_query_type.py", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 0, 19 | "threshold": "NA", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/failed_query_type/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/goroutine_number/goroutine_number.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'go_goroutines{job=~"tidb.*"}' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_instance = result['metric']['instance'] 17 | result_value = result['value'][1] 18 | print ("[tck_result:] "+result_instance+"="+result_value) 19 | except: 20 | sys.exit(1) 21 | -------------------------------------------------------------------------------- /probes/local/goroutine_number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "goroutine_number", 3 | "name": "goroutine数量", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "goroutine_number.py" 11 | ], 12 | "main": "goroutine_number.py", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 5, 19 | "threshold": "50000", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/goroutine_number/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/long_ddl_job/long_ddl_job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_PATH=$1 4 | MYSQL_LOGIN_PATH=$2 5 | PROMETHEUS_ADDRESSES=$3 6 | 7 | # 今天以内执行花费5s以上并且走的是tikv的SQL 8 | # 会打印出JOB ID以及所用时间 9 | 10 | print_sql=\ 11 | "select 12 | JOB_ID, TIMESTAMPDIFF(SECOND,START_TIME,END_TIME) as cost_time 13 | from information_schema.DDL_JOBS 14 | where START_TIME between (now() - INTERVAL 1 DAY) and now() 15 | having cost_time > 3600 16 | order by cost_time desc 17 | limit 100;" 18 | 19 | to_print=$(mysql --login-path="${MYSQL_LOGIN_PATH}" -e "$print_sql" -ss) 20 | 21 | IFS=$'\n' 22 | for i in $to_print ; do 23 | echo "$i" | awk '{print "[tck_result:] DDL JOB_ID:"$1"=花费时间"$2"秒"}' 24 | done 25 | 26 | # save_sql=\ 27 | # "select 28 | # JOB_ID,DB_NAME,TABLE_NAME,JOB_TYPE,STATE,QUERY,START_TIME,END_TIME,TIMESTAMPDIFF(SECOND,START_TIME,END_TIME) as cost_time 29 | # from information_schema.DDL_JOBS 30 | # where START_TIME between (now() - INTERVAL 1 DAY) and now() 31 | # having cost_time > 3600 32 | # order by cost_time desc 33 | # limit 100 \G" 34 | 35 | # to_save=$(mysql --login-path="${MYSQL_LOGIN_PATH}" -e "$save_sql") 36 | # if [ -n "$to_save" ]; then 37 | # echo "$to_save" > "$BASE_PATH"/err_report/long_ddl_job 38 | # fi 39 | -------------------------------------------------------------------------------- /probes/local/long_ddl_job/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "long_ddl_job", 3 | "name": "执行超过1h的DDL", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "long_ddl_job.sh" 11 | ], 12 | "main": "long_ddl_job.sh", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 0, 19 | "threshold": "NA", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/long_ddl_job/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/network_in/network_in.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | check_args = sys.argv[4:] if len(sys.argv) > 3 else "" 11 | 12 | pql = 'max_over_time( ( irate( node_network_receive_bytes_total{device!="lo"}[5m] ) )[1d:5m] ) / 1024 / 1024' 13 | 14 | if check_args: 15 | device_filter = "" 16 | for device in check_args: 17 | device_filter = device_filter + device + "|" 18 | device_filter = device_filter.strip("|") 19 | pql = 'max_over_time( ( irate( node_network_receive_bytes_total{device=~"%s"}[5m] ) )[1d:5m] ) / 1024 / 1024' \ 20 | % device_filter 21 | 22 | try: 23 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 24 | 25 | for result in response.json()['data']['result']: 26 | result_instance = result['metric']['instance'] 27 | result_device = result['metric']['device'] 28 | result_value = float(result['value'][1]) 29 | print ("[tck_result:] "+result_instance + '/' + result_device+"="+"{:.3f}".format(result_value)) 30 | except: 31 | sys.exit(1) 32 | -------------------------------------------------------------------------------- /probes/local/network_in/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "network_in", 3 | "name": "今天网卡流入流量(MB)", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "network_in.py" 11 | ], 12 | "main": "network_in.py", 13 | "tags": [ 14 | "network" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 5, 19 | "threshold": "4096", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/network_in/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/network_out/network_out.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | check_args = sys.argv[4:] if len(sys.argv) > 3 else "" 11 | 12 | pql = 'max_over_time( ( irate( node_network_transmit_bytes_total{device!="lo"}[5m] ) )[1d:5m] ) / 1024 / 1024' 13 | 14 | if check_args: 15 | device_filter = "" 16 | for device in check_args: 17 | device_filter = device_filter + device + "|" 18 | device_filter = device_filter.strip("|") 19 | pql = 'max_over_time( ( irate( node_network_transmit_bytes_total{device=~"%s"}[5m] ) )[1d:5m] ) / 1024 / 1024' \ 20 | % device_filter 21 | 22 | try: 23 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 24 | 25 | for result in response.json()['data']['result']: 26 | result_instance = result['metric']['instance'] 27 | result_device = result['metric']['device'] 28 | result_value = float(result['value'][1]) 29 | print ("[tck_result:] "+result_instance + '/' + result_device+"="+"{:.3f}".format(result_value)) 30 | except: 31 | sys.exit(1) 32 | -------------------------------------------------------------------------------- /probes/local/network_out/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "network_out", 3 | "name": "今天网卡流出流量(MB)", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "network_out.py" 11 | ], 12 | "main": "network_out.py", 13 | "tags": [ 14 | "network" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 5, 19 | "threshold": "4096", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/network_out/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/no_primary_key/no_primary_key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_PATH=$1 4 | MYSQL_LOGIN_PATH=$2 5 | PROMETHEUS_ADDRESSES=$3 6 | 7 | # 检查表是否没有主键 8 | # 打印出所有没有主键副本的库名及表名 9 | 10 | sql_command=\ 11 | "select table_schema,table_name from information_schema.tables 12 | where (table_schema,table_name) not in( 13 | select distinct table_schema,table_name from information_schema.columns where COLUMN_KEY='PRI' 14 | ) 15 | and table_schema not in ( 16 | 'METRICS_SCHEMA','mysql','INFORMATION_SCHEMA','PERFORMANCE_SCHEMA','test');" 17 | 18 | result=$(mysql --login-path="${MYSQL_LOGIN_PATH}" -e "$sql_command" -ss) 19 | 20 | IFS=$'\n' 21 | for i in $result ; do 22 | echo "$i" | awk '{print [tck_result:] $1"."$2=无主键}' 23 | done 24 | -------------------------------------------------------------------------------- /probes/local/no_primary_key/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "no_primary_key", 3 | "name": "表没有主键", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "no_primary_key.sh" 11 | ], 12 | "main": "no_primary_key.sh", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 0, 19 | "threshold": "NA", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/no_primary_key/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/running_sql_5min/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "running_sql_5min", 3 | "name": "正在执行且超过5分钟的SQL", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "running_sql_5min.sh" 11 | ], 12 | "main": "running_sql_5min.sh", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 0, 19 | "threshold": "NA", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/running_sql_5min/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/running_sql_5min/running_sql_5min.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_PATH=$1 4 | MYSQL_LOGIN_PATH=$2 5 | PROMETHEUS_ADDRESSES=$3 6 | 7 | # 正在执行时间超过5min的SQL 8 | # 打印出DIGEST与花费时间 9 | 10 | print_sql=\ 11 | "select 12 | \`DIGEST\`,\`TIME\` 13 | from information_schema.cluster_processlist 14 | where \`COMMAND\` <> 'Sleep' 15 | and \`TIME\`> 300;" 16 | 17 | to_print=$(mysql --login-path="${MYSQL_LOGIN_PATH}" -e "$print_sql" -ss) 18 | 19 | IFS=$'\n' 20 | for i in $to_print ; do 21 | echo "$i" | awk '{print "[tck_result:] SQL ID:"$1"=已经运行"$2"秒"}' 22 | done 23 | 24 | # save_sql=\ 25 | # "select * 26 | # from information_schema.cluster_processlist 27 | # where \`COMMAND\` <> 'Sleep' 28 | # and \`TIME\`> 300 \G" 29 | 30 | # to_save=$(mysql --login-path="${MYSQL_LOGIN_PATH}" -e "$save_sql") 31 | # if [ -n "$to_save" ]; then 32 | # echo "$to_save" > "$BASE_PATH"/err_report/running_sql_5min 33 | # fi 34 | -------------------------------------------------------------------------------- /probes/local/succeeded_query_number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "succeeded_query_number", 3 | "name": "今天成功的Query数", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "succeeded_query_number.py" 11 | ], 12 | "main": "succeeded_query_number.py", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 3, 19 | "threshold": "0", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/succeeded_query_number/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/succeeded_query_number/succeeded_query_number.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'sum(increase(tidb_server_query_total{result="OK"}[1d])) by (result)' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_value = result['value'][1].split('.')[0] 17 | print ("[tck_result:] 今天成功的Query数="+result_value) 18 | except: 19 | sys.exit(1) 20 | -------------------------------------------------------------------------------- /probes/local/tidb_connections/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "tidb_connections", 3 | "name": "每个tidb的连接数", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "tidb_connections.py" 11 | ], 12 | "main": "tidb_connections.py", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 5, 19 | "threshold": "800", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/tidb_connections/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/tidb_connections/tidb_connections.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'tidb_server_connections' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_instance = result['metric']['instance'] 17 | result_value = result['value'][1] 18 | print ("[tck_result:] "+result_instance+"="+result_value) 19 | except: 20 | sys.exit(1) 21 | -------------------------------------------------------------------------------- /probes/local/tikv_region_number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "tikv_region_number", 3 | "name": "TiKV Region数量", 4 | "author": { 5 | "name": "DigitalChina", 6 | "email": "" 7 | }, 8 | "description": "this script description.", 9 | "files": [ 10 | "tikv_region_number.py" 11 | ], 12 | "main": "tikv_region_number.py", 13 | "tags": [ 14 | "running_state" 15 | ], 16 | "rules": [ 17 | { 18 | "operator": 5, 19 | "threshold": "30000", 20 | "args": [] 21 | } 22 | ], 23 | "homepage": "", 24 | "version": "1.0.1", 25 | "createTime": "2022-03-07 15:28:30", 26 | "updateTime": "2022-03-07 15:28:30" 27 | } -------------------------------------------------------------------------------- /probes/local/tikv_region_number/readme.md: -------------------------------------------------------------------------------- 1 | ### alive_pd_number使用手册 2 | 3 | -------------------------------------------------------------------------------- /probes/local/tikv_region_number/tikv_region_number.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import requests 5 | 6 | base_path = sys.argv[1] 7 | mysql_login_path = sys.argv[2] 8 | prometheus_address = sys.argv[3] 9 | 10 | pql = 'sum(tikv_raftstore_region_count{type="region"}) by (instance)' 11 | 12 | try: 13 | response = requests.get('%s/api/v1/query' % prometheus_address, params={'query': pql}) 14 | 15 | for result in response.json()['data']['result']: 16 | result_address = result['metric']['instance'] 17 | result_value = result['value'][1] 18 | print ("[tck_result:] "+result_address+"="+result_value) 19 | except: 20 | sys.exit(1) 21 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ##============================================================================================== 4 | ## load run-time variables 5 | ##============================================================================================== 6 | 7 | CHECK_TIME=$1 8 | 9 | # load current path as base path 10 | BASE_PATH=$(cd "$(dirname "$0")" || exit 1; pwd) 11 | 12 | # check if run-time variable exist 13 | if [ ! -d "${BASE_PATH}"/config ] || [ ! -f "${BASE_PATH}"/config/load_variables.sh ]; then 14 | echo "[run.sh] error! fail to load runtime variables" 15 | echo "[run.sh] failed to run!" 16 | exit 1 17 | fi 18 | 19 | source "${BASE_PATH}"/config/load_variables.sh 20 | 21 | ##============================================================================================== 22 | ## check script runner to be the same as specified in the config file 23 | ##============================================================================================== 24 | 25 | if [[ $(whoami) != "${OS_USER}" ]]; then 26 | echo "[run.sh] error! current user is not ${OS_USER}!" 27 | echo "[run.sh] failed to run!" 28 | exit 1; 29 | fi 30 | 31 | ##============================================================================================== 32 | ## executing all check scripts formats print 33 | ## this will also generate report in /report and error report in err_report/ 34 | ##============================================================================================== 35 | if python "${BASE_PATH}"/run/generate_report.py "$BASE_PATH" "$MYSQL_LOGIN_PATH" "$PROMETHEUS_ADDRESS" "$CHECK_TIME" 36 | then 37 | echo "[run.sh] success!" 38 | else 39 | echo "[run.sh] error! failed to run generate_report.py" 40 | exit 1 41 | fi 42 | -------------------------------------------------------------------------------- /store/ticheck.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalChinaOpenSource/TiCheck/9bebdfade9f2caf8674fa2b582666a82c4368abf/store/ticheck.db -------------------------------------------------------------------------------- /util/file.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | /** 11 | @zipFile:压缩文件 12 | @dest:解压之后文件保存路径 13 | */ 14 | func DeCompress(zipFile, dest string) error { 15 | if _, err := os.Stat(dest); err != nil { 16 | if os.IsNotExist(err) { 17 | os.MkdirAll(dest, 0755) 18 | } 19 | } 20 | 21 | reader, err := zip.OpenReader(zipFile) 22 | if err != nil { 23 | return err 24 | } 25 | defer reader.Close() 26 | for _, file := range reader.File { 27 | rc, err := file.Open() 28 | if err != nil { 29 | return err 30 | } 31 | defer rc.Close() 32 | filename := dest + "/" + file.Name 33 | err = os.MkdirAll(getDir(filename), 0755) 34 | if err != nil { 35 | return err 36 | } 37 | w, err := os.Create(filename) 38 | if err != nil { 39 | return err 40 | } 41 | defer w.Close() 42 | _, err = io.Copy(w, rc) 43 | if err != nil { 44 | return err 45 | } 46 | w.Close() 47 | rc.Close() 48 | } 49 | return nil 50 | } 51 | 52 | func getDir(path string) string { 53 | return subString(path, 0, strings.LastIndex(path, "/")) 54 | } 55 | 56 | func subString(str string, start, end int) string { 57 | rs := []rune(str) 58 | length := len(rs) 59 | 60 | if start < 0 || start > length { 61 | panic("start is wrong") 62 | } 63 | 64 | if end < start || end > length { 65 | panic("end is wrong") 66 | } 67 | 68 | return string(rs[start:end]) 69 | } 70 | -------------------------------------------------------------------------------- /util/logutil/log.go: -------------------------------------------------------------------------------- 1 | package logutil 2 | 3 | import ( 4 | "github.com/natefinch/lumberjack" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zapcore" 7 | "os" 8 | ) 9 | 10 | const ( 11 | // DefaultLogFilePath is the default path for saving log files. 12 | DefaultLogFilePath = "../../log/ticheck.log" 13 | // DefaultLogMaxSize is the default size of log files. 14 | DefaultLogMaxSize = 96 15 | // DefaultLogAge is the default age of the log. 16 | DefaultLogAge = 30 17 | // DefaultLogBackups is the default number of log backups. 18 | DefaultLogBackups = 3 19 | // DefaultLogCompress is the default value for whether compress log. 20 | DefaultLogCompress = false 21 | ) 22 | 23 | var Logger *zap.Logger 24 | 25 | type LogConfig struct { 26 | Level zapcore.Level 27 | 28 | LumberjackConfig lumberjack.Logger 29 | } 30 | 31 | func NewLogConfig(Level zapcore.Level, LumberjackConfig lumberjack.Logger) LogConfig { 32 | return LogConfig{ 33 | Level: Level, 34 | LumberjackConfig: LumberjackConfig, 35 | } 36 | } 37 | 38 | func InitLog(conf LogConfig) { 39 | // 编码器配置 40 | config := zap.NewProductionEncoderConfig() 41 | // 指定时间编码器 42 | config.EncodeTime = zapcore.ISO8601TimeEncoder 43 | // 日志级别用大写 44 | config.EncodeLevel = zapcore.CapitalLevelEncoder 45 | // 编码器 46 | encoder := zapcore.NewConsoleEncoder(config) 47 | 48 | // 配置 49 | //lj := &lumberjack.Logger{ 50 | // Filename: DefaultLogFilePath, 51 | // MaxSize: DefaultLogMaxSize, 52 | // MaxBackups: DefaultLogBackups, 53 | // MaxAge: DefaultLogAge, 54 | // Compress: DefaultLogCompress, 55 | //} 56 | 57 | sync := zapcore.AddSync(&conf.LumberjackConfig) 58 | 59 | // create Logger 60 | // sync is config for writing to file. 61 | // zapcore.AddSync(os.Stdout) is config for writing to console. 62 | 63 | core := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(sync, zapcore.AddSync(os.Stdout)), conf.Level) 64 | Logger = zap.New(core, zap.AddCaller()) 65 | 66 | Logger.Info("Completed init the logger") 67 | } 68 | -------------------------------------------------------------------------------- /web/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 10 4 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=false 5 | indent_style=space 6 | indent_size=2 7 | 8 | [{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}] 9 | indent_style=space 10 | indent_size=2 11 | 12 | [{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}] 13 | indent_style=space 14 | indent_size=2 15 | 16 | [{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}] 17 | indent_style=space 18 | indent_size=2 19 | 20 | [*.svg] 21 | indent_style=space 22 | indent_size=2 23 | 24 | [*.js.map] 25 | indent_style=space 26 | indent_size=2 27 | 28 | [*.less] 29 | indent_style=space 30 | indent_size=2 31 | 32 | [*.vue] 33 | indent_style=space 34 | indent_size=2 35 | 36 | [{.analysis_options,*.yml,*.yaml}] 37 | indent_style=space 38 | indent_size=2 39 | 40 | -------------------------------------------------------------------------------- /web/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VUE_APP_PREVIEW=true 3 | VUE_APP_API_BASE_URL=/api -------------------------------------------------------------------------------- /web/.env.preview: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VUE_APP_PREVIEW=true 3 | VUE_APP_API_BASE_URL=/api -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/strongly-recommended', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'generator-star-spacing': 'off', 14 | 'no-mixed-operators': 0, 15 | 'vue/max-attributes-per-line': [ 16 | 2, 17 | { 18 | 'singleline': 5, 19 | 'multiline': { 20 | 'max': 1, 21 | 'allowFirstLine': false 22 | } 23 | } 24 | ], 25 | 'vue/attribute-hyphenation': 0, 26 | 'vue/html-self-closing': 0, 27 | 'vue/component-name-in-template-casing': 0, 28 | 'vue/html-closing-bracket-spacing': 0, 29 | 'vue/singleline-html-element-content-newline': 0, 30 | 'vue/no-unused-components': 0, 31 | 'vue/multiline-html-element-content-newline': 0, 32 | 'vue/no-use-v-if-with-v-for': 0, 33 | 'vue/html-closing-bracket-newline': 0, 34 | 'vue/no-parsing-error': 0, 35 | 'no-tabs': 0, 36 | 'quotes': [ 37 | 2, 38 | 'single', 39 | { 40 | 'avoidEscape': true, 41 | 'allowTemplateLiterals': true 42 | } 43 | ], 44 | 'semi': [ 45 | 2, 46 | 'never', 47 | { 48 | 'beforeStatementContinuationChars': 'never' 49 | } 50 | ], 51 | 'no-delete-var': 2, 52 | 'prefer-const': [ 53 | 2, 54 | { 55 | 'ignoreReadBeforeAssign': false 56 | } 57 | ], 58 | 'template-curly-spacing': 'off', 59 | 'indent': 'off' 60 | }, 61 | parserOptions: { 62 | parser: 'babel-eslint' 63 | }, 64 | overrides: [ 65 | { 66 | files: [ 67 | '**/__tests__/*.{j,t}s?(x)', 68 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 69 | ], 70 | env: { 71 | jest: true 72 | } 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "space-before-function-paren": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/babel.config.js: -------------------------------------------------------------------------------- 1 | const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) 2 | const IS_PREVIEW = process.env.VUE_APP_PREVIEW === 'true' 3 | 4 | const plugins = [] 5 | if (IS_PROD && !IS_PREVIEW) { 6 | // 去除日志的插件, 7 | plugins.push('transform-remove-console') 8 | } 9 | 10 | // lazy load ant-design-vue 11 | // if your use import on Demand, Use this code 12 | plugins.push(['import', { 13 | 'libraryName': 'ant-design-vue', 14 | 'libraryDirectory': 'es', 15 | 'style': true // `style: true` 会加载 less 文件 16 | }]) 17 | 18 | module.exports = { 19 | presets: [ 20 | '@vue/cli-plugin-babel/preset', 21 | [ 22 | '@babel/preset-env', 23 | { 24 | 'useBuiltIns': 'entry', 25 | 'corejs': 3 26 | } 27 | ] 28 | ], 29 | plugins 30 | } 31 | -------------------------------------------------------------------------------- /web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest' 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/src/$1' 15 | }, 16 | snapshotSerializers: [ 17 | 'jest-serializer-vue' 18 | ], 19 | testMatch: [ 20 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 21 | ], 22 | testURL: 'http://localhost/' 23 | } 24 | -------------------------------------------------------------------------------- /web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "dist"], 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/public/avatar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalChinaOpenSource/TiCheck/9bebdfade9f2caf8674fa2b582666a82c4368abf/web/public/avatar2.jpg -------------------------------------------------------------------------------- /web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalChinaOpenSource/TiCheck/9bebdfade9f2caf8674fa2b582666a82c4368abf/web/public/logo.png -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /web/src/api/cluster.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | const clusterApi = { 4 | getClusterList: '/cluster/list', 5 | getClusterInfo: '/cluster/info/', 6 | getInitialCluster: '/cluster/initial/', 7 | addCluster: '/cluster/add', 8 | updateCluster: '/cluster/update/', 9 | getSchedulerList: '/cluster/scheduler/', 10 | addScheduler: '/cluster/scheduler/add', 11 | updateScheduler: '/cluster/scheduler/update', 12 | deleteScheduler: '/cluster/scheduler/delete/' 13 | } 14 | 15 | export function getClusterList () { 16 | return request({ 17 | url: clusterApi.getClusterList, 18 | method: 'get' 19 | }) 20 | } 21 | 22 | export function getClusterInfo (param) { 23 | return request({ 24 | url: clusterApi.getClusterInfo + param, 25 | method: 'get' 26 | }) 27 | } 28 | 29 | export function getInitialCluster (param) { 30 | return request({ 31 | url: clusterApi.getInitialCluster + param, 32 | method: 'get' 33 | }) 34 | } 35 | 36 | export function addCluster (parameter) { 37 | return request({ 38 | url: clusterApi.addCluster, 39 | method: 'post', 40 | data: parameter 41 | }) 42 | } 43 | 44 | export function updateCluster (id, parameter) { 45 | return request({ 46 | method: 'put', 47 | url: clusterApi.updateCluster + id, 48 | data: parameter 49 | }) 50 | } 51 | 52 | export function getSchedulerList (id) { 53 | return request({ 54 | method: 'get', 55 | url: clusterApi.getSchedulerList + id 56 | }) 57 | } 58 | 59 | export function addScheduler (parameter) { 60 | return request({ 61 | url: clusterApi.addScheduler, 62 | method: 'post', 63 | data: parameter 64 | }) 65 | } 66 | 67 | export function updateScheduler (parameter) { 68 | return request({ 69 | method: 'put', 70 | url: clusterApi.updateScheduler, 71 | data: parameter 72 | }) 73 | } 74 | 75 | export function deleteScheduler (id) { 76 | return request({ 77 | method: 'delete', 78 | url: clusterApi.deleteScheduler + id 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /web/src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | const userApi = { 4 | // Login: '/auth/login', 5 | Login: '/session', 6 | // Logout: '/auth/logout', 7 | Logout: '/session/logout', 8 | ForgePassword: '/auth/forge-password', 9 | Register: '/auth/register', 10 | twoStepCode: '/auth/2step-code', 11 | SendSms: '/account/sms', 12 | SendSmsErr: '/account/sms_err', 13 | // get my info 14 | // UserInfo: '/user/info', 15 | UserInfo: '/session/info', 16 | UserMenu: '/user/nav' 17 | } 18 | 19 | /** 20 | * login func 21 | * parameter: { 22 | * username: '', 23 | * password: '', 24 | * remember_me: true, 25 | * captcha: '12345' 26 | * } 27 | * @param parameter 28 | * @returns {*} 29 | */ 30 | export function login (parameter) { 31 | return request({ 32 | url: userApi.Login, 33 | method: 'post', 34 | data: parameter 35 | }) 36 | } 37 | 38 | export function getSmsCaptcha (parameter) { 39 | return request({ 40 | url: userApi.SendSms, 41 | method: 'post', 42 | data: parameter 43 | }) 44 | } 45 | 46 | export function getInfo () { 47 | return request({ 48 | url: userApi.UserInfo, 49 | method: 'get', 50 | headers: { 51 | 'Content-Type': 'application/json;charset=UTF-8' 52 | } 53 | }) 54 | } 55 | 56 | export function getCurrentUserNav () { 57 | return request({ 58 | url: userApi.UserMenu, 59 | method: 'get' 60 | }) 61 | } 62 | 63 | export function logout () { 64 | return request({ 65 | url: userApi.Logout, 66 | method: 'post', 67 | headers: { 68 | 'Content-Type': 'application/json;charset=UTF-8' 69 | } 70 | }) 71 | } 72 | 73 | /** 74 | * get user 2step code open? 75 | * @param parameter {*} 76 | */ 77 | export function get2step (parameter) { 78 | return request({ 79 | url: userApi.twoStepCode, 80 | method: 'post', 81 | data: parameter 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /web/src/api/manage.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | const api = { 4 | user: '/user', 5 | role: '/role', 6 | service: '/service', 7 | permission: '/permission', 8 | permissionNoPager: '/permission/no-pager', 9 | orgTree: '/org/tree' 10 | } 11 | 12 | export default api 13 | 14 | export function getUserList (parameter) { 15 | return request({ 16 | url: api.user, 17 | method: 'get', 18 | params: parameter 19 | }) 20 | } 21 | 22 | export function getRoleList (parameter) { 23 | return request({ 24 | url: api.role, 25 | method: 'get', 26 | params: parameter 27 | }) 28 | } 29 | 30 | export function getServiceList (parameter) { 31 | return request({ 32 | url: api.service, 33 | method: 'get', 34 | params: parameter 35 | }) 36 | } 37 | 38 | export function getPermissions (parameter) { 39 | return request({ 40 | url: api.permissionNoPager, 41 | method: 'get', 42 | params: parameter 43 | }) 44 | } 45 | 46 | export function getOrgTree (parameter) { 47 | return request({ 48 | url: api.orgTree, 49 | method: 'get', 50 | params: parameter 51 | }) 52 | } 53 | // id == 0 add post 54 | // id != 0 update put 55 | export function saveService (parameter) { 56 | return request({ 57 | url: api.service, 58 | method: parameter.id === 0 ? 'post' : 'put', 59 | data: parameter 60 | }) 61 | } 62 | 63 | export function saveSub (sub) { 64 | return request({ 65 | url: '/sub', 66 | method: sub.id === 0 ? 'post' : 'put', 67 | data: sub 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /web/src/assets/icons/bx-analyse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalChinaOpenSource/TiCheck/9bebdfade9f2caf8674fa2b582666a82c4368abf/web/src/assets/icons/python.png -------------------------------------------------------------------------------- /web/src/assets/icons/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalChinaOpenSource/TiCheck/9bebdfade9f2caf8674fa2b582666a82c4368abf/web/src/assets/icons/shell.png -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalChinaOpenSource/TiCheck/9bebdfade9f2caf8674fa2b582666a82c4368abf/web/src/assets/logo.png -------------------------------------------------------------------------------- /web/src/assets/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalChinaOpenSource/TiCheck/9bebdfade9f2caf8674fa2b582666a82c4368abf/web/src/assets/logo1.png -------------------------------------------------------------------------------- /web/src/components/ArticleListContent/ArticleListContent.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 47 | 48 | 90 | -------------------------------------------------------------------------------- /web/src/components/ArticleListContent/index.js: -------------------------------------------------------------------------------- 1 | import ArticleListContent from './ArticleListContent' 2 | 3 | export default ArticleListContent 4 | -------------------------------------------------------------------------------- /web/src/components/AvatarList/Item.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'ant-design-vue/es/_util/vue-types' 2 | import { Tooltip, Avatar } from 'ant-design-vue' 3 | import { getSlotOptions } from 'ant-design-vue/lib/_util/props-util' 4 | import { warning } from 'ant-design-vue/lib/vc-util/warning' 5 | 6 | export const AvatarListItemProps = { 7 | tips: PropTypes.string, 8 | src: PropTypes.string.def('') 9 | } 10 | 11 | const Item = { 12 | __ANT_AVATAR_CHILDREN: true, 13 | name: 'AvatarListItem', 14 | props: AvatarListItemProps, 15 | created () { 16 | warning(getSlotOptions(this.$parent).__ANT_AVATAR_LIST, 'AvatarListItem must be a subcomponent of AvatarList') 17 | }, 18 | render () { 19 | const size = this.$parent.size === 'mini' ? 'small' : this.$parent.size 20 | const AvatarDom = 21 | return (this.tips && {AvatarDom}) || 22 | } 23 | } 24 | 25 | export default Item 26 | -------------------------------------------------------------------------------- /web/src/components/AvatarList/List.jsx: -------------------------------------------------------------------------------- 1 | import './index.less' 2 | 3 | import PropTypes from 'ant-design-vue/es/_util/vue-types' 4 | import Avatar from 'ant-design-vue/es/avatar' 5 | import Item from './Item.jsx' 6 | import { filterEmpty } from '@/components/_util/util' 7 | 8 | /** 9 | * size: `number`、 `large`、`small`、`default` 默认值: default 10 | * maxLength: number 11 | * excessItemsStyle: CSSProperties 12 | */ 13 | const AvatarListProps = { 14 | prefixCls: PropTypes.string.def('ant-pro-avatar-list'), 15 | size: { 16 | validator: val => { 17 | return typeof val === 'number' || ['small', 'large', 'default'].includes(val) 18 | }, 19 | default: 'default' 20 | }, 21 | maxLength: PropTypes.number.def(0), 22 | excessItemsStyle: PropTypes.object.def({ 23 | color: '#f56a00', 24 | backgroundColor: '#fde3cf' 25 | }) 26 | } 27 | 28 | const AvatarList = { 29 | __ANT_AVATAR_LIST: true, 30 | Item, 31 | name: 'AvatarList', 32 | props: AvatarListProps, 33 | render (h) { 34 | const { prefixCls, size } = this.$props 35 | const className = { 36 | [`${prefixCls}`]: true, 37 | [`${size}`]: true 38 | } 39 | 40 | const items = filterEmpty(this.$slots.default) 41 | const itemsDom = items && items.length ?
    {this.getItems(items)}
: null 42 | return ( 43 |
44 | {itemsDom} 45 |
46 | ) 47 | }, 48 | methods: { 49 | getItems (items) { 50 | const className = { 51 | [`${this.prefixCls}-item`]: true, 52 | [`${this.size}`]: true 53 | } 54 | const totalSize = items.length 55 | 56 | if (this.maxLength > 0) { 57 | items = items.slice(0, this.maxLength) 58 | items.push(({`+${totalSize - this.maxLength}`})) 59 | } 60 | return items.map((item) => ( 61 |
  • {item}
  • 62 | )) 63 | } 64 | } 65 | } 66 | 67 | AvatarList.install = function (Vue) { 68 | Vue.component(AvatarList.name, AvatarList) 69 | Vue.component(AvatarList.Item.name, AvatarList.Item) 70 | } 71 | 72 | export default AvatarList 73 | -------------------------------------------------------------------------------- /web/src/components/AvatarList/index.js: -------------------------------------------------------------------------------- 1 | import AvatarList from './List' 2 | import Item from './Item' 3 | 4 | export { 5 | AvatarList, 6 | Item as AvatarListItem 7 | } 8 | 9 | export default AvatarList 10 | -------------------------------------------------------------------------------- /web/src/components/AvatarList/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list"; 4 | @avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item"; 5 | 6 | .@{avatar-list-prefix-cls} { 7 | display: inline-block; 8 | 9 | ul { 10 | list-style: none; 11 | display: inline-block; 12 | padding: 0; 13 | margin: 0 0 0 8px; 14 | font-size: 0; 15 | } 16 | } 17 | 18 | .@{avatar-list-item-prefix-cls} { 19 | display: inline-block; 20 | font-size: @font-size-base; 21 | margin-left: -8px; 22 | width: @avatar-size-base; 23 | height: @avatar-size-base; 24 | 25 | :global { 26 | .ant-avatar { 27 | border: 1px solid #fff; 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | &.large { 33 | width: @avatar-size-lg; 34 | height: @avatar-size-lg; 35 | } 36 | 37 | &.small { 38 | width: @avatar-size-sm; 39 | height: @avatar-size-sm; 40 | } 41 | 42 | &.mini { 43 | width: 20px; 44 | height: 20px; 45 | 46 | :global { 47 | .ant-avatar { 48 | width: 20px; 49 | height: 20px; 50 | line-height: 20px; 51 | 52 | .ant-avatar-string { 53 | font-size: 12px; 54 | line-height: 18px; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /web/src/components/AvatarList/index.md: -------------------------------------------------------------------------------- 1 | # AvatarList 用户头像列表 2 | 3 | 4 | 一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。 5 | 6 | 7 | 8 | 引用方式: 9 | 10 | ```javascript 11 | import AvatarList from '@/components/AvatarList' 12 | const AvatarListItem = AvatarList.Item 13 | 14 | export default { 15 | components: { 16 | AvatarList, 17 | AvatarListItem 18 | } 19 | } 20 | ``` 21 | 22 | 23 | 24 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 25 | 26 | ```html 27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | 或 34 | ```html 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ``` 45 | 46 | 47 | 48 | ## API 49 | 50 | ### AvatarList 51 | 52 | | 参数 | 说明 | 类型 | 默认值 | 53 | | ---------------- | -------- | ---------------------------------- | --------- | 54 | | size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` | 55 | | maxLength | 要显示的最大项目 | number | - | 56 | | excessItemsStyle | 多余的项目风格 | CSSProperties | - | 57 | 58 | ### AvatarList.Item 59 | 60 | | 参数 | 说明 | 类型 | 默认值 | 61 | | ---- | ------ | --------- | --- | 62 | | tips | 头像展示文案 | string | - | 63 | | src | 头像图片连接 | string | - | 64 | 65 | -------------------------------------------------------------------------------- /web/src/components/Charts/Bar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | -------------------------------------------------------------------------------- /web/src/components/Charts/Liquid.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 64 | 65 | 68 | -------------------------------------------------------------------------------- /web/src/components/Charts/MiniArea.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /web/src/components/Charts/MiniBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /web/src/components/Charts/MiniProgress.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 76 | -------------------------------------------------------------------------------- /web/src/components/Charts/MiniSmoothArea.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /web/src/components/Charts/Radar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /web/src/components/Charts/RankList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 78 | -------------------------------------------------------------------------------- /web/src/components/Charts/TransferBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 65 | -------------------------------------------------------------------------------- /web/src/components/Charts/Trend.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 52 | 53 | 83 | -------------------------------------------------------------------------------- /web/src/components/Charts/chart.less: -------------------------------------------------------------------------------- 1 | .antv-chart-mini { 2 | position: relative; 3 | width: 100%; 4 | 5 | .chart-wrapper { 6 | position: absolute; 7 | bottom: -28px; 8 | width: 100%; 9 | 10 | /* margin: 0 -5px; 11 | overflow: hidden;*/ 12 | } 13 | } -------------------------------------------------------------------------------- /web/src/components/Charts/smooth.area.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area"; 4 | 5 | .@{smoothArea-prefix-cls} { 6 | position: relative; 7 | width: 100%; 8 | 9 | .chart-wrapper { 10 | position: absolute; 11 | bottom: -28px; 12 | width: 100%; 13 | } 14 | } -------------------------------------------------------------------------------- /web/src/components/Editor/QuillEditor.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 69 | 70 | 84 | -------------------------------------------------------------------------------- /web/src/components/Editor/WangEditor.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 58 | -------------------------------------------------------------------------------- /web/src/components/Ellipsis/Ellipsis.vue: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /web/src/components/Ellipsis/index.js: -------------------------------------------------------------------------------- 1 | import Ellipsis from './Ellipsis' 2 | 3 | export default Ellipsis 4 | -------------------------------------------------------------------------------- /web/src/components/Ellipsis/index.md: -------------------------------------------------------------------------------- 1 | # Ellipsis 文本自动省略号 2 | 3 | 文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import Ellipsis from '@/components/Ellipsis' 11 | 12 | export default { 13 | components: { 14 | Ellipsis 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 25 | There were injuries alleged in three cases in 2015, and a 26 | fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall. 27 | 28 | ``` 29 | 30 | 31 | 32 | ## API 33 | 34 | 35 | 参数 | 说明 | 类型 | 默认值 36 | ----|------|-----|------ 37 | tooltip | 移动到文本展示完整内容的提示 | boolean | - 38 | length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - -------------------------------------------------------------------------------- /web/src/components/FooterToolbar/FooterToolBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /web/src/components/FooterToolbar/index.js: -------------------------------------------------------------------------------- 1 | import FooterToolBar from './FooterToolBar' 2 | import './index.less' 3 | 4 | export default FooterToolBar 5 | -------------------------------------------------------------------------------- /web/src/components/FooterToolbar/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar"; 4 | 5 | .@{footer-toolbar-prefix-cls} { 6 | position: fixed; 7 | width: 100%; 8 | bottom: 0; 9 | right: 0; 10 | height: 56px; 11 | line-height: 56px; 12 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03); 13 | background: #fff; 14 | border-top: 1px solid #e8e8e8; 15 | padding: 0 24px; 16 | z-index: 9; 17 | 18 | &:after { 19 | content: ""; 20 | display: block; 21 | clear: both; 22 | } 23 | } -------------------------------------------------------------------------------- /web/src/components/FooterToolbar/index.md: -------------------------------------------------------------------------------- 1 | # FooterToolbar 底部工具栏 2 | 3 | 固定在底部的工具栏。 4 | 5 | 6 | 7 | ## 何时使用 8 | 9 | 固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。 10 | 11 | 12 | 13 | 引用方式: 14 | 15 | ```javascript 16 | import FooterToolBar from '@/components/FooterToolbar' 17 | 18 | export default { 19 | components: { 20 | FooterToolBar 21 | } 22 | } 23 | ``` 24 | 25 | 26 | 27 | ## 代码演示 28 | 29 | ```html 30 | 31 | 提交 32 | 33 | ``` 34 | 或 35 | ```html 36 | 37 | 提交 38 | 39 | ``` 40 | 41 | 42 | ## API 43 | 44 | 参数 | 说明 | 类型 | 默认值 45 | ----|------|-----|------ 46 | children (slot) | 工具栏内容,向右对齐 | - | - 47 | extra | 额外信息,向左对齐 | String, Object | - 48 | 49 | -------------------------------------------------------------------------------- /web/src/components/GlobalFooter/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /web/src/components/GlobalHeader/RightContent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 59 | -------------------------------------------------------------------------------- /web/src/components/IconSelector/IconSelector.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 62 | 63 | 87 | -------------------------------------------------------------------------------- /web/src/components/IconSelector/README.md: -------------------------------------------------------------------------------- 1 | IconSelector 2 | ==== 3 | 4 | > 图标选择组件,常用于为某一个数据设定一个图标时使用 5 | > eg: 设定菜单列表时,为每个菜单设定一个图标 6 | 7 | 该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装 8 | 9 | 10 | 11 | ### 使用方式 12 | 13 | ```vue 14 | 19 | 20 | 39 | ``` 40 | 41 | 42 | 43 | ### 事件 44 | 45 | 46 | | 名称 | 说明 | 类型 | 默认值 | 47 | | ------ | -------------------------- | ------ | ------ | 48 | | change | 当改变了 `icon` 选中项触发 | String | - | 49 | -------------------------------------------------------------------------------- /web/src/components/IconSelector/index.js: -------------------------------------------------------------------------------- 1 | import IconSelector from './IconSelector' 2 | export default IconSelector 3 | -------------------------------------------------------------------------------- /web/src/components/MultiTab/events.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default new Vue() 3 | -------------------------------------------------------------------------------- /web/src/components/MultiTab/index.js: -------------------------------------------------------------------------------- 1 | import events from './events' 2 | import MultiTab from './MultiTab' 3 | import './index.less' 4 | 5 | const api = { 6 | /** 7 | * open new tab on route fullPath 8 | * @param config 9 | */ 10 | open: function (config) { 11 | events.$emit('open', config) 12 | }, 13 | rename: function (key, name) { 14 | events.$emit('rename', { key: key, name: name }) 15 | }, 16 | /** 17 | * close current page 18 | */ 19 | closeCurrentPage: function () { 20 | this.close() 21 | }, 22 | /** 23 | * close route fullPath tab 24 | * @param config 25 | */ 26 | close: function (config) { 27 | events.$emit('close', config) 28 | } 29 | } 30 | 31 | MultiTab.install = function (Vue) { 32 | if (Vue.prototype.$multiTab) { 33 | return 34 | } 35 | api.instance = events 36 | Vue.prototype.$multiTab = api 37 | Vue.component('multi-tab', MultiTab) 38 | } 39 | 40 | export default MultiTab 41 | -------------------------------------------------------------------------------- /web/src/components/MultiTab/index.less: -------------------------------------------------------------------------------- 1 | @import '../index'; 2 | 3 | @multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab"; 4 | @multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper"; 5 | 6 | /* 7 | .topmenu .@{multi-tab-prefix-cls} { 8 | max-width: 1200px; 9 | margin: -23px auto 24px auto; 10 | } 11 | */ 12 | .@{multi-tab-prefix-cls} { 13 | margin: -23px -24px 24px -24px; 14 | background: #fff; 15 | } 16 | 17 | .topmenu .@{multi-tab-wrapper-prefix-cls} { 18 | max-width: 1200px; 19 | margin: 0 auto; 20 | } 21 | 22 | .topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} { 23 | max-width: 100%; 24 | margin: 0 auto; 25 | } 26 | -------------------------------------------------------------------------------- /web/src/components/NProgress/nprogress.less: -------------------------------------------------------------------------------- 1 | @import url('../index.less'); 2 | 3 | /* Make clicks pass-through */ 4 | #nprogress { 5 | pointer-events: none; 6 | } 7 | 8 | #nprogress .bar { 9 | background: @primary-color; 10 | 11 | position: fixed; 12 | z-index: 1031; 13 | top: 0; 14 | left: 0; 15 | 16 | width: 100%; 17 | height: 2px; 18 | } 19 | 20 | /* Fancy blur effect */ 21 | #nprogress .peg { 22 | display: block; 23 | position: absolute; 24 | right: 0px; 25 | width: 100px; 26 | height: 100%; 27 | box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color; 28 | opacity: 1.0; 29 | 30 | -webkit-transform: rotate(3deg) translate(0px, -4px); 31 | -ms-transform: rotate(3deg) translate(0px, -4px); 32 | transform: rotate(3deg) translate(0px, -4px); 33 | } 34 | 35 | /* Remove these to get rid of the spinner */ 36 | #nprogress .spinner { 37 | display: block; 38 | position: fixed; 39 | z-index: 1031; 40 | top: 15px; 41 | right: 15px; 42 | } 43 | 44 | #nprogress .spinner-icon { 45 | width: 18px; 46 | height: 18px; 47 | box-sizing: border-box; 48 | 49 | border: solid 2px transparent; 50 | border-top-color: @primary-color; 51 | border-left-color: @primary-color; 52 | border-radius: 50%; 53 | 54 | -webkit-animation: nprogress-spinner 400ms linear infinite; 55 | animation: nprogress-spinner 400ms linear infinite; 56 | } 57 | 58 | .nprogress-custom-parent { 59 | overflow: hidden; 60 | position: relative; 61 | } 62 | 63 | .nprogress-custom-parent #nprogress .spinner, 64 | .nprogress-custom-parent #nprogress .bar { 65 | position: absolute; 66 | } 67 | 68 | @-webkit-keyframes nprogress-spinner { 69 | 0% { -webkit-transform: rotate(0deg); } 70 | 100% { -webkit-transform: rotate(360deg); } 71 | } 72 | @keyframes nprogress-spinner { 73 | 0% { transform: rotate(0deg); } 74 | 100% { transform: rotate(360deg); } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /web/src/components/NoticeIcon/index.js: -------------------------------------------------------------------------------- 1 | import NoticeIcon from './NoticeIcon' 2 | export default NoticeIcon 3 | -------------------------------------------------------------------------------- /web/src/components/NumberInfo/NumberInfo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 51 | 52 | 55 | -------------------------------------------------------------------------------- /web/src/components/NumberInfo/index.js: -------------------------------------------------------------------------------- 1 | import NumberInfo from './NumberInfo' 2 | 3 | export default NumberInfo 4 | -------------------------------------------------------------------------------- /web/src/components/NumberInfo/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info"; 4 | 5 | .@{numberInfo-prefix-cls} { 6 | 7 | .ant-pro-number-info-subtitle { 8 | color: @text-color-secondary; 9 | font-size: @font-size-base; 10 | height: 22px; 11 | line-height: 22px; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | word-break: break-all; 15 | white-space: nowrap; 16 | } 17 | 18 | .number-info-value { 19 | margin-top: 4px; 20 | font-size: 0; 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | word-break: break-all; 24 | white-space: nowrap; 25 | 26 | & > span { 27 | color: @heading-color; 28 | display: inline-block; 29 | line-height: 32px; 30 | height: 32px; 31 | font-size: 24px; 32 | margin-right: 32px; 33 | } 34 | 35 | .sub-total { 36 | color: @text-color-secondary; 37 | font-size: @font-size-lg; 38 | vertical-align: top; 39 | margin-right: 0; 40 | i { 41 | font-size: 12px; 42 | transform: scale(0.82); 43 | margin-left: 4px; 44 | } 45 | :global { 46 | .anticon-caret-up { 47 | color: @red-6; 48 | } 49 | .anticon-caret-down { 50 | color: @green-6; 51 | } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /web/src/components/NumberInfo/index.md: -------------------------------------------------------------------------------- 1 | # NumberInfo 数据文本 2 | 3 | 常用在数据卡片中,用于突出展示某个业务数据。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import NumberInfo from '@/components/NumberInfo' 11 | 12 | export default { 13 | components: { 14 | NumberInfo 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 29 | ``` 30 | 31 | 32 | 33 | ## API 34 | 35 | 参数 | 说明 | 类型 | 默认值 36 | ----|------|-----|------ 37 | title | 标题 | ReactNode\|string | - 38 | subTitle | 子标题 | ReactNode\|string | - 39 | total | 总量 | ReactNode\|string | - 40 | subTotal | 子总量 | ReactNode\|string | - 41 | status | 增加状态 | 'up \| down' | - 42 | theme | 状态样式 | string | 'light' 43 | gap | 设置数字和描述之间的间距(像素)| number | 8 44 | -------------------------------------------------------------------------------- /web/src/components/Other/CarbonAds.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 63 | -------------------------------------------------------------------------------- /web/src/components/Search/GlobalSearch.jsx: -------------------------------------------------------------------------------- 1 | import { Select } from 'ant-design-vue' 2 | import './index.less' 3 | 4 | const GlobalSearch = { 5 | name: 'GlobalSearch', 6 | data () { 7 | return { 8 | visible: false 9 | } 10 | }, 11 | mounted () { 12 | const keyboardHandle = (e) => { 13 | e.preventDefault() 14 | e.stopPropagation() 15 | const { ctrlKey, shiftKey, altKey, keyCode } = e 16 | console.log('keyCode:', e.keyCode, e) 17 | // key is `K` and hold ctrl 18 | if (keyCode === 75 && ctrlKey && !shiftKey && !altKey) { 19 | this.visible = !this.visible 20 | } 21 | } 22 | document.addEventListener('keydown', keyboardHandle) 23 | }, 24 | render () { 25 | const { visible } = this 26 | const handleSearch = (e) => { 27 | this.$emit('search', e) 28 | } 29 | 30 | const handleChange = (e) => { 31 | this.$emit('change', e) 32 | } 33 | if (!visible) { 34 | return null 35 | } 36 | return ( 37 | 55 | ) 56 | } 57 | } 58 | 59 | GlobalSearch.install = function (Vue) { 60 | Vue.component(GlobalSearch.name, GlobalSearch) 61 | } 62 | 63 | export default GlobalSearch 64 | -------------------------------------------------------------------------------- /web/src/components/Search/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default"; 2 | 3 | .global-search-wrapper { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | z-index: @zindex-modal-mask; 10 | background: @modal-mask-bg; 11 | 12 | .global-search-box { 13 | position: absolute; 14 | top: 20%; 15 | left: 50%; 16 | width: 450px; 17 | transform: translate(-50%, -50%); 18 | 19 | .global-search-tips { 20 | color: @white; 21 | font-size: @font-size-lg; 22 | text-align: right; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /web/src/components/SelectLang/index.jsx: -------------------------------------------------------------------------------- 1 | import './index.less' 2 | 3 | import { Icon, Menu, Dropdown } from 'ant-design-vue' 4 | import { i18nRender } from '@/locales' 5 | import i18nMixin from '@/store/i18n-mixin' 6 | 7 | // const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'] 8 | const locales = ['zh-CN', 'en-US'] 9 | const languageLabels = { 10 | 'zh-CN': '简体中文', 11 | // 'zh-TW': '繁体中文', 12 | 'en-US': 'English' 13 | // 'pt-BR': 'Português' 14 | } 15 | // eslint-disable-next-line 16 | const languageIcons = { 17 | 'zh-CN': '🇨🇳', 18 | // 'zh-TW': '🇭🇰', 19 | 'en-US': '🇺🇸' 20 | // 'pt-BR': '🇧🇷' 21 | } 22 | 23 | const SelectLang = { 24 | props: { 25 | prefixCls: { 26 | type: String, 27 | default: 'ant-pro-drop-down' 28 | } 29 | }, 30 | name: 'SelectLang', 31 | mixins: [i18nMixin], 32 | render () { 33 | const { prefixCls } = this 34 | const changeLang = ({ key }) => { 35 | this.setLang(key) 36 | } 37 | const langMenu = ( 38 | 39 | {locales.map(locale => ( 40 | 41 | 42 | {languageIcons[locale]} 43 | {' '} 44 | {languageLabels[locale]} 45 | 46 | ))} 47 | 48 | ) 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | ) 56 | } 57 | } 58 | 59 | export default SelectLang 60 | -------------------------------------------------------------------------------- /web/src/components/SelectLang/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default"; 2 | 3 | @header-menu-prefix-cls: ~'@{ant-prefix}-pro-header-menu'; 4 | @header-drop-down-prefix-cls: ~'@{ant-prefix}-pro-drop-down'; 5 | 6 | .@{header-menu-prefix-cls} { 7 | 8 | .anticon { 9 | margin-right: 8px; 10 | } 11 | .ant-dropdown-menu-item { 12 | min-width: 160px; 13 | } 14 | } 15 | 16 | .@{header-drop-down-prefix-cls} { 17 | 18 | line-height: @layout-header-height; 19 | vertical-align: top; 20 | cursor: pointer; 21 | 22 | > i { 23 | font-size: 16px !important; 24 | transform: none !important; 25 | 26 | svg { 27 | position: relative; 28 | top: -1px; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/src/components/SettingDrawer/SettingItem.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 39 | -------------------------------------------------------------------------------- /web/src/components/SettingDrawer/index.js: -------------------------------------------------------------------------------- 1 | import SettingDrawer from './SettingDrawer' 2 | export default SettingDrawer 3 | -------------------------------------------------------------------------------- /web/src/components/SettingDrawer/settingConfig.js: -------------------------------------------------------------------------------- 1 | import message from 'ant-design-vue/es/message' 2 | // import defaultSettings from '../defaultSettings'; 3 | import themeColor from './themeColor.js' 4 | 5 | // let lessNodesAppended 6 | const colorList = [ 7 | { 8 | key: '薄暮', color: '#F5222D' 9 | }, 10 | { 11 | key: '火山', color: '#FA541C' 12 | }, 13 | { 14 | key: '日暮', color: '#FAAD14' 15 | }, 16 | { 17 | key: '明青', color: '#13C2C2' 18 | }, 19 | { 20 | key: '极光绿', color: '#52C41A' 21 | }, 22 | { 23 | key: '拂晓蓝(默认)', color: '#1890FF' 24 | }, 25 | { 26 | key: '极客蓝', color: '#2F54EB' 27 | }, 28 | { 29 | key: '酱紫', color: '#722ED1' 30 | } 31 | ] 32 | 33 | const updateTheme = newPrimaryColor => { 34 | const hideMessage = message.loading('正在切换主题!', 0) 35 | themeColor.changeColor(newPrimaryColor).finally(() => { 36 | setTimeout(() => { 37 | hideMessage() 38 | }, 10) 39 | }) 40 | } 41 | 42 | const updateColorWeak = colorWeak => { 43 | // document.body.className = colorWeak ? 'colorWeak' : ''; 44 | const app = document.body.querySelector('#app') 45 | colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak') 46 | } 47 | 48 | export { updateTheme, colorList, updateColorWeak } 49 | -------------------------------------------------------------------------------- /web/src/components/SettingDrawer/themeColor.js: -------------------------------------------------------------------------------- 1 | import client from 'webpack-theme-color-replacer/client' 2 | import generate from '@ant-design/colors/lib/generate' 3 | 4 | export default { 5 | getAntdSerials (color) { 6 | // 淡化(即less的tint) 7 | const lightens = new Array(9).fill().map((t, i) => { 8 | return client.varyColor.lighten(color, i / 10) 9 | }) 10 | // colorPalette变换得到颜色值 11 | const colorPalettes = generate(color) 12 | const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',') 13 | return lightens.concat(colorPalettes).concat(rgb) 14 | }, 15 | changeColor (newColor) { 16 | var options = { 17 | newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors` 18 | changeUrl (cssUrl) { 19 | return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path 20 | } 21 | } 22 | return client.changer.changeColor(options, Promise) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/src/components/StandardFormRow/index.js: -------------------------------------------------------------------------------- 1 | import StandardFormRow from './StandardFormRow' 2 | 3 | export default StandardFormRow 4 | -------------------------------------------------------------------------------- /web/src/components/TagSelect/TagSelectOption.jsx: -------------------------------------------------------------------------------- 1 | import { Tag } from 'ant-design-vue' 2 | const { CheckableTag } = Tag 3 | 4 | export default { 5 | name: 'TagSelectOption', 6 | props: { 7 | prefixCls: { 8 | type: String, 9 | default: 'ant-pro-tag-select-option' 10 | }, 11 | value: { 12 | type: [String, Number, Object], 13 | default: '' 14 | }, 15 | checked: { 16 | type: Boolean, 17 | default: false 18 | } 19 | }, 20 | data () { 21 | return { 22 | localChecked: this.checked || false 23 | } 24 | }, 25 | watch: { 26 | 'checked' (val) { 27 | this.localChecked = val 28 | }, 29 | '$parent.items': { 30 | handler: function (val) { 31 | this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value]) 32 | }, 33 | deep: true 34 | } 35 | }, 36 | render () { 37 | const { $slots, value } = this 38 | const onChange = (checked) => { 39 | this.$emit('change', { value, checked }) 40 | } 41 | return ( 42 | {$slots.default} 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/src/components/TextArea/index.jsx: -------------------------------------------------------------------------------- 1 | import './style.less' 2 | import { getStrFullLength, cutStrByFullLength } from '../_util/util' 3 | import Input from 'ant-design-vue/es/input' 4 | const TextArea = Input.TextArea 5 | 6 | export default { 7 | name: 'LimitTextArea', 8 | model: { 9 | prop: 'value', 10 | event: 'change' 11 | }, 12 | props: Object.assign({}, TextArea.props, { 13 | prefixCls: { 14 | type: String, 15 | default: 'ant-textarea-limit' 16 | }, 17 | // eslint-disable-next-line 18 | value: { 19 | type: String 20 | }, 21 | limit: { 22 | type: Number, 23 | default: 200 24 | } 25 | }), 26 | data () { 27 | return { 28 | currentLimit: 0 29 | } 30 | }, 31 | watch: { 32 | value (val) { 33 | this.calcLimitNum(val) 34 | } 35 | }, 36 | created () { 37 | this.calcLimitNum(this.value) 38 | }, 39 | methods: { 40 | handleChange (e) { 41 | const value = e.target.value 42 | const len = getStrFullLength(value) 43 | if (len <= this.limit) { 44 | this.currentLimit = len 45 | this.$emit('change', value) 46 | return 47 | } else { 48 | const str = cutStrByFullLength(value, this.limit) 49 | this.currentLimit = getStrFullLength(str) 50 | this.$emit('change', str) 51 | } 52 | console.error('limit out! currentLimit:', this.currentLimit) 53 | }, 54 | calcLimitNum (val) { 55 | const len = getStrFullLength(val) 56 | this.currentLimit = len 57 | } 58 | }, 59 | render () { 60 | const { prefixCls, ...props } = this.$props 61 | return ( 62 |
    63 | 65 | {this.currentLimit}/{this.limit} 66 |
    67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/src/components/TextArea/style.less: -------------------------------------------------------------------------------- 1 | .ant-textarea-limit { 2 | position: relative; 3 | 4 | .limit { 5 | position: absolute; 6 | color: #909399; 7 | background: #fff; 8 | font-size: 12px; 9 | bottom: 5px; 10 | right: 10px; 11 | } 12 | } -------------------------------------------------------------------------------- /web/src/components/Trend/Trend.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /web/src/components/Trend/index.js: -------------------------------------------------------------------------------- 1 | import Trend from './Trend.vue' 2 | 3 | export default Trend 4 | -------------------------------------------------------------------------------- /web/src/components/Trend/index.less: -------------------------------------------------------------------------------- 1 | @import "../index"; 2 | 3 | @trend-prefix-cls: ~"@{ant-pro-prefix}-trend"; 4 | 5 | .@{trend-prefix-cls} { 6 | display: inline-block; 7 | font-size: @font-size-base; 8 | line-height: 22px; 9 | 10 | .up, 11 | .down { 12 | margin-left: 4px; 13 | position: relative; 14 | top: 1px; 15 | 16 | i { 17 | font-size: 12px; 18 | transform: scale(0.83); 19 | } 20 | } 21 | 22 | .item-text { 23 | display: inline-block; 24 | margin-left: 8px; 25 | color: rgba(0,0,0,.85); 26 | } 27 | 28 | .up { 29 | color: @red-6; 30 | } 31 | .down { 32 | color: @green-6; 33 | top: -1px; 34 | } 35 | 36 | &.reverse-color .up { 37 | color: @green-6; 38 | } 39 | &.reverse-color .down { 40 | color: @red-6; 41 | } 42 | } -------------------------------------------------------------------------------- /web/src/components/Trend/index.md: -------------------------------------------------------------------------------- 1 | # Trend 趋势标记 2 | 3 | 趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。 4 | 5 | 6 | 7 | 引用方式: 8 | 9 | ```javascript 10 | import Trend from '@/components/Trend' 11 | 12 | export default { 13 | components: { 14 | Trend 15 | } 16 | } 17 | ``` 18 | 19 | 20 | 21 | ## 代码演示 [demo](https://pro.loacg.com/test/home) 22 | 23 | ```html 24 | 5% 25 | ``` 26 | 或 27 | ```html 28 | 29 | 工资 30 | 5% 31 | 32 | ``` 33 | 或 34 | ```html 35 | 5% 36 | ``` 37 | 38 | 39 | ## API 40 | 41 | | 参数 | 说明 | 类型 | 默认值 | 42 | |----------|------------------------------------------|-------------|-------| 43 | | flag | 上升下降标识:`up|down` | string | - | 44 | | reverseColor | 颜色反转 | Boolean | false | 45 | 46 | -------------------------------------------------------------------------------- /web/src/components/_util/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * components util 3 | */ 4 | 5 | /** 6 | * 清理空值,对象 7 | * @param children 8 | * @returns {*[]} 9 | */ 10 | export function filterEmpty (children = []) { 11 | return children.filter(c => c.tag || (c.text && c.text.trim() !== '')) 12 | } 13 | 14 | /** 15 | * 获取字符串长度,英文字符 长度1,中文字符长度2 16 | * @param {*} str 17 | */ 18 | export const getStrFullLength = (str = '') => 19 | str.split('').reduce((pre, cur) => { 20 | const charCode = cur.charCodeAt(0) 21 | if (charCode >= 0 && charCode <= 128) { 22 | return pre + 1 23 | } 24 | return pre + 2 25 | }, 0) 26 | 27 | /** 28 | * 截取字符串,根据 maxLength 截取后返回 29 | * @param {*} str 30 | * @param {*} maxLength 31 | */ 32 | export const cutStrByFullLength = (str = '', maxLength) => { 33 | let showLength = 0 34 | return str.split('').reduce((pre, cur) => { 35 | const charCode = cur.charCodeAt(0) 36 | if (charCode >= 0 && charCode <= 128) { 37 | showLength += 1 38 | } else { 39 | showLength += 2 40 | } 41 | if (showLength <= maxLength) { 42 | return pre + cur 43 | } 44 | return pre 45 | }, '') 46 | } 47 | -------------------------------------------------------------------------------- /web/src/components/index.js: -------------------------------------------------------------------------------- 1 | // chart 2 | import Bar from '@/components/Charts/Bar' 3 | import ChartCard from '@/components/Charts/ChartCard' 4 | import Liquid from '@/components/Charts/Liquid' 5 | import MiniArea from '@/components/Charts/MiniArea' 6 | import MiniSmoothArea from '@/components/Charts/MiniSmoothArea' 7 | import MiniBar from '@/components/Charts/MiniBar' 8 | import MiniProgress from '@/components/Charts/MiniProgress' 9 | import Radar from '@/components/Charts/Radar' 10 | import RankList from '@/components/Charts/RankList' 11 | import TransferBar from '@/components/Charts/TransferBar' 12 | import TagCloud from '@/components/Charts/TagCloud' 13 | 14 | // pro components 15 | import AvatarList from '@/components/AvatarList' 16 | import Ellipsis from '@/components/Ellipsis' 17 | import FooterToolbar from '@/components/FooterToolbar' 18 | import NumberInfo from '@/components/NumberInfo' 19 | import Tree from '@/components/Tree/Tree' 20 | import Trend from '@/components/Trend' 21 | import STable from '@/components/Table' 22 | import MultiTab from '@/components/MultiTab' 23 | import IconSelector from '@/components/IconSelector' 24 | import TagSelect from '@/components/TagSelect' 25 | import StandardFormRow from '@/components/StandardFormRow' 26 | import ArticleListContent from '@/components/ArticleListContent' 27 | 28 | import Dialog from '@/components/Dialog' 29 | 30 | export { 31 | AvatarList, 32 | Bar, 33 | ChartCard, 34 | Liquid, 35 | MiniArea, 36 | MiniSmoothArea, 37 | MiniBar, 38 | MiniProgress, 39 | Radar, 40 | TagCloud, 41 | RankList, 42 | TransferBar, 43 | Trend, 44 | Ellipsis, 45 | FooterToolbar, 46 | NumberInfo, 47 | Tree, 48 | STable, 49 | MultiTab, 50 | IconSelector, 51 | TagSelect, 52 | StandardFormRow, 53 | ArticleListContent, 54 | 55 | Dialog 56 | } 57 | -------------------------------------------------------------------------------- /web/src/components/index.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/lib/style/index"; 2 | 3 | // The prefix to use on all css classes from ant-pro. 4 | @ant-pro-prefix : ant-pro; 5 | @ant-global-sider-zindex : 106; 6 | @ant-global-header-zindex : 105; -------------------------------------------------------------------------------- /web/src/config/defaultSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 项目默认配置项 3 | * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage 4 | * navTheme - sidebar theme ['dark', 'light'] 两种主题 5 | * colorWeak - 色盲模式 6 | * layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局 7 | * fixedHeader - 固定 Header : boolean 8 | * fixSiderbar - 固定左侧菜单栏 : boolean 9 | * contentWidth - 内容区布局: 流式 | 固定 10 | * 11 | * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage) 12 | * 13 | */ 14 | 15 | export default { 16 | navTheme: 'dark', // theme for nav menu 17 | primaryColor: '#F5222D', // '#F5222D', // primary color of ant design 18 | layout: 'topmenu', // nav menu position: `sidemenu` or `topmenu` 19 | contentWidth: 'Fluid', // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu 20 | fixedHeader: true, // sticky header 21 | fixSiderbar: false, // sticky siderbar 22 | colorWeak: false, 23 | menu: { 24 | locale: true 25 | }, 26 | title: 'TiCheck', 27 | pwa: false, 28 | iconfontUrl: '', 29 | production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true' 30 | } 31 | -------------------------------------------------------------------------------- /web/src/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import storage from 'store' 3 | import { 4 | ACCESS_TOKEN, 5 | APP_LANGUAGE, 6 | TOGGLE_CONTENT_WIDTH, 7 | TOGGLE_FIXED_HEADER, 8 | TOGGLE_FIXED_SIDEBAR, TOGGLE_HIDE_HEADER, 9 | TOGGLE_LAYOUT, TOGGLE_NAV_THEME, TOGGLE_WEAK, 10 | TOGGLE_COLOR, TOGGLE_MULTI_TAB 11 | } from '@/store/mutation-types' 12 | import { printANSI } from '@/utils/screenLog' 13 | import defaultSettings from '@/config/defaultSettings' 14 | 15 | export default function Initializer () { 16 | printANSI() // 请自行移除该行. please remove this line 17 | 18 | store.commit(TOGGLE_LAYOUT, storage.get(TOGGLE_LAYOUT, defaultSettings.layout)) 19 | store.commit(TOGGLE_FIXED_HEADER, storage.get(TOGGLE_FIXED_HEADER, defaultSettings.fixedHeader)) 20 | store.commit(TOGGLE_FIXED_SIDEBAR, storage.get(TOGGLE_FIXED_SIDEBAR, defaultSettings.fixSiderbar)) 21 | store.commit(TOGGLE_CONTENT_WIDTH, storage.get(TOGGLE_CONTENT_WIDTH, defaultSettings.contentWidth)) 22 | store.commit(TOGGLE_HIDE_HEADER, storage.get(TOGGLE_HIDE_HEADER, defaultSettings.autoHideHeader)) 23 | store.commit(TOGGLE_NAV_THEME, storage.get(TOGGLE_NAV_THEME, defaultSettings.navTheme)) 24 | store.commit(TOGGLE_WEAK, storage.get(TOGGLE_WEAK, defaultSettings.colorWeak)) 25 | store.commit(TOGGLE_COLOR, storage.get(TOGGLE_COLOR, defaultSettings.primaryColor)) 26 | store.commit(TOGGLE_MULTI_TAB, storage.get(TOGGLE_MULTI_TAB, defaultSettings.multiTab)) 27 | store.commit('SET_TOKEN', storage.get(ACCESS_TOKEN)) 28 | 29 | store.dispatch('setLang', storage.get(APP_LANGUAGE, 'en-US')) 30 | // last step 31 | } 32 | -------------------------------------------------------------------------------- /web/src/core/directives/action.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | 4 | /** 5 | * Action 权限指令 6 | * 指令用法: 7 | * - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下: 8 | * 添加用户 9 | * 删除用户 10 | * 修改 11 | * 12 | * - 当前用户没有权限时,组件上使用了该指令则会被隐藏 13 | * - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可 14 | * 15 | * @see https://github.com/vueComponent/ant-design-vue-pro/pull/53 16 | */ 17 | const action = Vue.directive('action', { 18 | inserted: function (el, binding, vnode) { 19 | const actionName = binding.arg 20 | const roles = store.getters.roles 21 | const elVal = vnode.context.$route.meta.permission 22 | const permissionId = elVal instanceof String && [elVal] || elVal 23 | roles.permissions.forEach(p => { 24 | if (!permissionId.includes(p.permissionId)) { 25 | return 26 | } 27 | if (p.actionList && !p.actionList.includes(actionName)) { 28 | el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none') 29 | } 30 | }) 31 | } 32 | }) 33 | 34 | export default action 35 | -------------------------------------------------------------------------------- /web/src/core/icons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom icon list 3 | * All icons are loaded here for easy management 4 | * @see https://vue.ant.design/components/icon/#Custom-Font-Icon 5 | * 6 | * 自定义图标加载表 7 | * 所有图标均从这里加载,方便管理 8 | */ 9 | import bxAnaalyse from '@/assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file. 10 | 11 | export { bxAnaalyse } 12 | -------------------------------------------------------------------------------- /web/src/core/permission/permission.js: -------------------------------------------------------------------------------- 1 | export const PERMISSION_ENUM = { 2 | 'add': { key: 'add', label: '新增' }, 3 | 'delete': { key: 'delete', label: '删除' }, 4 | 'edit': { key: 'edit', label: '修改' }, 5 | 'query': { key: 'query', label: '查询' }, 6 | 'get': { key: 'get', label: '详情' }, 7 | 'enable': { key: 'enable', label: '启用' }, 8 | 'disable': { key: 'disable', label: '禁用' }, 9 | 'import': { key: 'import', label: '导入' }, 10 | 'export': { key: 'export', label: '导出' } 11 | } 12 | 13 | /** 14 | * Button 15 | * @param Vue 16 | */ 17 | function plugin (Vue) { 18 | if (plugin.installed) { 19 | return 20 | } 21 | 22 | !Vue.prototype.$auth && Object.defineProperties(Vue.prototype, { 23 | $auth: { 24 | get () { 25 | const _this = this 26 | return (permissions) => { 27 | const [permission, action] = permissions.split('.') 28 | const permissionList = _this.$store.getters.roles.permissions 29 | return permissionList.find((val) => { 30 | return val.permissionId === permission 31 | }).actionList.findIndex((val) => { 32 | return val === action 33 | }) > -1 34 | } 35 | } 36 | } 37 | }) 38 | 39 | !Vue.prototype.$enum && Object.defineProperties(Vue.prototype, { 40 | $enum: { 41 | get () { 42 | // const _this = this; 43 | return (val) => { 44 | let result = PERMISSION_ENUM 45 | val && val.split('.').forEach(v => { 46 | result = result && result[v] || null 47 | }) 48 | return result 49 | } 50 | } 51 | } 52 | }) 53 | } 54 | 55 | export default plugin 56 | -------------------------------------------------------------------------------- /web/src/core/use.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // base library 4 | import Antd from 'ant-design-vue' 5 | import Viser from 'viser-vue' 6 | import VueCropper from 'vue-cropper' 7 | import 'ant-design-vue/dist/antd.less' 8 | 9 | // ext library 10 | import VueClipboard from 'vue-clipboard2' 11 | import MultiTab from '@/components/MultiTab' 12 | import PageLoading from '@/components/PageLoading' 13 | import PermissionHelper from '@/core/permission/permission' 14 | // import '@/components/use' 15 | import './directives/action' 16 | 17 | VueClipboard.config.autoSetContainer = true 18 | 19 | Vue.use(Antd) 20 | Vue.use(Viser) 21 | Vue.use(MultiTab) 22 | Vue.use(PageLoading) 23 | Vue.use(VueClipboard) 24 | Vue.use(PermissionHelper) 25 | Vue.use(VueCropper) 26 | 27 | process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] WARNING: Antd now use fulled imported.') 28 | -------------------------------------------------------------------------------- /web/src/global.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/es/style/themes/default.less'; 2 | 3 | html, 4 | body, 5 | #app, #root { 6 | height: 100%; 7 | } 8 | 9 | .colorWeak { 10 | filter: invert(80%); 11 | } 12 | 13 | .ant-layout.layout-basic { 14 | height: 100vh; 15 | min-height: 100vh; 16 | } 17 | 18 | canvas { 19 | display: block; 20 | } 21 | 22 | body { 23 | text-rendering: optimizeLegibility; 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | ul, 29 | ol { 30 | list-style: none; 31 | } 32 | 33 | // 数据列表 样式 34 | .table-alert { 35 | margin-bottom: 16px; 36 | } 37 | // 数据列表 操作 38 | .table-operator { 39 | margin-bottom: 18px; 40 | 41 | button { 42 | margin-right: 8px; 43 | } 44 | } 45 | // 数据列表 搜索条件 46 | .table-page-search-wrapper { 47 | 48 | .ant-form-inline { 49 | .ant-form-item { 50 | display: flex; 51 | margin-bottom: 24px; 52 | margin-right: 0; 53 | 54 | .ant-form-item-control-wrapper { 55 | flex: 1 1; 56 | display: inline-block; 57 | vertical-align: middle; 58 | } 59 | 60 | > .ant-form-item-label { 61 | line-height: 32px; 62 | padding-right: 8px; 63 | width: auto; 64 | } 65 | .ant-form-item-control { 66 | height: 32px; 67 | line-height: 32px; 68 | } 69 | } 70 | } 71 | 72 | .table-page-search-submitButtons { 73 | display: block; 74 | margin-bottom: 24px; 75 | white-space: nowrap; 76 | } 77 | } 78 | 79 | @media (max-width: @screen-xs) { 80 | .ant-table { 81 | width: 100%; 82 | overflow-x: auto; 83 | &-thead > tr, 84 | &-tbody > tr { 85 | > th, 86 | > td { 87 | white-space: pre; 88 | > span { 89 | display: block; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /web/src/layouts/BasicLayout.less: -------------------------------------------------------------------------------- 1 | @import "~ant-design-vue/es/style/themes/default.less"; 2 | 3 | .ant-pro-global-header-index-right { 4 | margin-right: 8px; 5 | 6 | &.ant-pro-global-header-index-dark { 7 | .ant-pro-global-header-index-action { 8 | color: hsla(0, 0%, 100%, .85); 9 | 10 | &:hover { 11 | background: #1890ff; 12 | } 13 | } 14 | } 15 | 16 | .ant-pro-account-avatar { 17 | .antd-pro-global-header-index-avatar { 18 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 19 | margin-right: 8px; 20 | color: @primary-color; 21 | vertical-align: top; 22 | background: rgba(255, 255, 255, 0.85); 23 | } 24 | } 25 | 26 | .menu { 27 | .anticon { 28 | margin-right: 8px; 29 | } 30 | 31 | .ant-dropdown-menu-item { 32 | min-width: 100px; 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /web/src/layouts/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /web/src/layouts/PageView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /web/src/layouts/RouteView.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /web/src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import UserLayout from './UserLayout' 2 | import BlankLayout from './BlankLayout' 3 | import BasicLayout from './BasicLayout' 4 | import RouteView from './RouteView' 5 | import PageView from './PageView' 6 | import MenuLayout from './MenuLayout' 7 | 8 | export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView, MenuLayout } 9 | -------------------------------------------------------------------------------- /web/src/locales/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import storage from 'store' 4 | import moment from 'moment' 5 | 6 | // default lang 7 | import enUS from './lang/en-US' 8 | 9 | Vue.use(VueI18n) 10 | 11 | export const defaultLang = 'en-US' 12 | 13 | const messages = { 14 | 'en-US': { 15 | ...enUS 16 | } 17 | } 18 | 19 | const i18n = new VueI18n({ 20 | silentTranslationWarn: true, 21 | locale: defaultLang, 22 | fallbackLocale: defaultLang, 23 | messages 24 | }) 25 | 26 | const loadedLanguages = [defaultLang] 27 | 28 | function setI18nLanguage (lang) { 29 | i18n.locale = lang 30 | // request.headers['Accept-Language'] = lang 31 | document.querySelector('html').setAttribute('lang', lang) 32 | return lang 33 | } 34 | 35 | export function loadLanguageAsync (lang = defaultLang) { 36 | return new Promise(resolve => { 37 | // 缓存语言设置 38 | storage.set('lang', lang) 39 | if (i18n.locale !== lang) { 40 | if (!loadedLanguages.includes(lang)) { 41 | return import(/* webpackChunkName: "lang-[request]" */ `./lang/${lang}`).then(msg => { 42 | const locale = msg.default 43 | i18n.setLocaleMessage(lang, locale) 44 | loadedLanguages.push(lang) 45 | moment.updateLocale(locale.momentName, locale.momentLocale) 46 | return setI18nLanguage(lang) 47 | }) 48 | } 49 | return resolve(setI18nLanguage(lang)) 50 | } 51 | return resolve(lang) 52 | }) 53 | } 54 | 55 | export function i18nRender (key) { 56 | return i18n.t(`${key}`) 57 | } 58 | 59 | export default i18n 60 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US.js: -------------------------------------------------------------------------------- 1 | import antdEnUS from 'ant-design-vue/es/locale-provider/en_US' 2 | import momentEU from 'moment/locale/eu' 3 | import global from './en-US/global' 4 | import dictData from './en-US/dict' 5 | 6 | import menu from './en-US/menu' 7 | import setting from './en-US/setting' 8 | import user from './en-US/user' 9 | 10 | import dashboard from './en-US/dashboard' 11 | import form from './en-US/form' 12 | import result from './en-US/result' 13 | import account from './en-US/account' 14 | import cluster from './en-US/cluster' 15 | import check from './en-US/check' 16 | 17 | import store from './en-US/store' 18 | 19 | const components = { 20 | antLocale: antdEnUS, 21 | momentName: 'eu', 22 | momentLocale: momentEU 23 | } 24 | 25 | export default { 26 | message: '-', 27 | 28 | 'layouts.usermenu.dialog.title': 'Message', 29 | 'layouts.usermenu.dialog.content': 'Are you sure you would like to logout?', 30 | 'layouts.userLayout.title': 'TiDB automated checklist for hackathon 2021.', 31 | 'layouts.list.load-more': 'Load More', 32 | 'layouts.list.no-more-data': 'no more data', 33 | ...components, 34 | ...global, 35 | ...dictData, 36 | ...menu, 37 | ...setting, 38 | ...user, 39 | ...dashboard, 40 | ...form, 41 | ...result, 42 | ...account, 43 | ...store, 44 | ...cluster, 45 | ...check 46 | } 47 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/account.js: -------------------------------------------------------------------------------- 1 | import settings from './account/settings' 2 | 3 | export default { 4 | ...settings 5 | } 6 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/check.js: -------------------------------------------------------------------------------- 1 | import history from './check/history' 2 | import probe from './check/probe' 3 | import execute from './check/execute' 4 | 5 | export default { 6 | ...history, 7 | ...probe, 8 | ...execute 9 | } 10 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/check/execute.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'check.execute.title': 'Execute Check', 3 | 'check.execute.times': 'Number of times checked', 4 | 'check.execute.lastTime': 'Last time checked', 5 | 'check.execute.runCheck': 'Run Check', 6 | 'check.execute.editProbe': 'Edit Probe', 7 | 'check.execute.checkResult': 'Check Result', 8 | 9 | // modal 10 | 'check.execute.modal.title': 'Run Check', 11 | 'check.execute.modal.contents': 'Sure to run check?', 12 | 13 | // notification 14 | 'check.execute.notification.success': 'The Check is completed successfully.', 15 | 'check.execute.notification.timeout': 'The check is timeout.', 16 | 'check.execute.notification.error': 'Can not connect to the server.', 17 | 'check.execute.notification.info.no_probe.message': 'No Check Probe', 18 | 'check.execute.notification.info.no_probe.description': 'The cluster has no check probes, please add check probes first.', 19 | 'check.execute.notification.error.disconnected': 'The server disconnected abnormally.', 20 | 'check.execute.notification.error.conflict': 'There is already a running check in this cluster. Please try again later', 21 | 22 | // List 23 | 'check.execute.list.check_threshold': 'Check Threshold', 24 | 'check.execute.list.result': 'Result', 25 | } 26 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/check/history.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'check.history.title': 'Check History', 3 | 'check.history.timeSelect': 'Time Select', 4 | 'check.history.table.checkTime': 'Check Time', 5 | 'check.history.table.checkState': 'State', 6 | 'check.history.table.duration': 'Duration', 7 | 'check.history.table.normal': 'Normal', 8 | 'check.history.table.warning': 'Warning', 9 | 'check.history.table.total': 'Total', 10 | 'check.history.table.action': 'Action', 11 | 'check.history.table.detail': 'Detail', 12 | 'check.history.table.download': 'Download', 13 | 14 | // detail 15 | 'check.history.detail.title': 'Check History Detail', 16 | 'check.history.detail.download': 'Download', 17 | 'check.history.detail.table.name': 'Name', 18 | 'check.history.detail.table.tag': 'Tag', 19 | 'check.history.detail.table.item': 'Item', 20 | 'check.history.detail.table.status': 'Status', 21 | 'check.history.detail.table.operator': 'Operator', 22 | 'check.history.detail.table.threshold': 'Threshold', 23 | 'check.history.detail.table.actual': 'Actual', 24 | 'check.history.detail.table.duration': 'Duration', 25 | 'check.history.detail.table.status.normal': 'normal', 26 | 'check.history.detail.table.status.new_abnormal': 'new abnormal', 27 | 'check.history.detail.table.status.existing_abnormal': 'existing abnormal', 28 | 'check.history.detail.table.status.script_error': 'script error', 29 | } 30 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/check/probe.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'check.probe.title': 'Probe', 3 | 'check.probe.add': 'Add Probe', 4 | 'check.probe.table.probeID': 'ID', 5 | 'check.probe.table.name': 'Name', 6 | 'check.probe.table.tag': 'Tag', 7 | 'check.probe.table.description': 'Description', 8 | 'check.probe.table.operator': 'Operator', 9 | 'check.probe.table.threshold': 'Threshold', 10 | 'check.probe.table.isEnabled': 'Is Enabled', 11 | 'check.probe.table.isEnabled.enabled': 'Enabled', 12 | 'check.probe.table.isEnabled.disabled': 'Disabled', 13 | 'check.probe.table.search': 'Search', 14 | 'check.probe.table.reset': 'Reset', 15 | 'check.probe.table.action': 'Action', 16 | 'check.probe.table.edit': 'Edit', 17 | 'check.probe.table.delete': 'Delete', 18 | 19 | 'check.probe.table.delete.modal.title': 'Delete Probe', 20 | 'check.probe.table.delete.modal.contents': 'Are you sure to delete this probe?', 21 | 22 | // tag 23 | 'check.probe.tag.all': 'All', 24 | 'check.probe.tag.cluster': 'Cluster', 25 | 'check.probe.tag.network': 'Network', 26 | 'check.probe.tag.running_state': 'RunningState', 27 | 'check.probe.tag.others': 'Others', 28 | 29 | // add probe 30 | 'check.probe.add.title': 'Add Probe', 31 | 'check.probe.add.local': 'Local', 32 | 'check.probe.add.remote': 'Remote', 33 | 'check.probe.add.custom': 'Custom', 34 | 'check.probe.add.table.probeID': 'ID', 35 | 'check.probe.add.table.name': 'Name', 36 | 'check.probe.add.table.tag': 'Tag', 37 | 'check.probe.add.table.operator': 'Operator', 38 | 'check.probe.add.table.threshold': 'Threshold', 39 | 'check.probe.add.table.creator': 'Creator', 40 | 'check.probe.add.table.updateTime': 'Update Time', 41 | 'check.probe.add.table.action': 'Action', 42 | 'check.probe.add.table.add': 'Add', 43 | 'check.probe.add.table.search': 'Search', 44 | 'check.probe.add.table.reset': 'Reset', 45 | 46 | // module 47 | 'check.probe.module.title': 'Edit Probe Config', 48 | 'check.probe.module.id': 'ID', 49 | 'check.probe.module.name': 'Name', 50 | 'check.probe.module.operator': 'Operator', 51 | 'check.probe.module.threshold': 'Threshold', 52 | 'check.probe.module.submit': 'Submit', 53 | 'check.probe.module.cancel': 'Cancel', 54 | } 55 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/cluster.js: -------------------------------------------------------------------------------- 1 | import list from './cluster/list' 2 | import info from './cluster/info' 3 | import scheduler from './cluster/scheduler' 4 | 5 | export default { 6 | ...list, 7 | ...info, 8 | ...scheduler 9 | } 10 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/cluster/info.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cluster.info.cluster-info': 'Cluster Info', 3 | 'cluster.info.check': 'Launch', 4 | 'cluster.info.name': 'Cluster Name', 5 | 'cluster.info.version': 'Cluster Version', 6 | 'cluster.info.create-time': 'Create Time', 7 | 'cluster.info.description': 'Cluster Description', 8 | 'cluster.info.owner': 'Cluster Owner', 9 | 'cluster.info.status': 'Cluster Status', 10 | 'cluster.info.status.count': 'Total Check Count', 11 | 'cluster.info.status.total': 'Total Check Items', 12 | 'cluster.info.status.count-today': 'Today Check Count', 13 | 'cluster.info.status.total-today': 'Today Check Items', 14 | 'cluster.info.status.last': 'Last Check At', 15 | 'cluster.info.status.last-normal': 'Normal Count', 16 | 'cluster.info.status.last-warning': 'Warning Count', 17 | 'cluster.info.status.healthy': 'Cluster Healthy', 18 | 'cluster.info.status.healthy-update': 'Update At', 19 | 'cluster.info.recent': 'Latest Warnings Trending' 20 | } 21 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/cluster/list.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cluster.list.cluster-list': 'Cluster List', 3 | 'cluster.list.name': 'Cluster Name', 4 | 'cluster.list.add-cluster': 'Add Cluster', 5 | 'cluster.list.add-time': 'Create Time', 6 | 'cluster.list.description': 'Cluster Description', 7 | 'cluster.list.node-info': 'Node Info', 8 | 'cluster.list.prometheus.warning': 'Could not connect to the cluster prometheus server', 9 | 'cluster.list.last-check-time': 'Last Check', 10 | 'cluster.list.prometheus': 'Prometheus Url', 11 | 'cluster.list.user': 'User', 12 | 'cluster.list.passwd': 'Password', 13 | 'cluster.list.input.name': 'please input TiDB cluster name', 14 | 'cluster.list.input.prometheus': 'please input Prometheus url', 15 | 'cluster.list.input.user': 'please input TiDB database user', 16 | 'cluster.list.input.passwd': 'please input TiDB database password', 17 | 'cluster.list.input.description': 'please describe this TiDB cluster', 18 | 'cluster.list.setting.title': 'Update Cluster', 19 | 'cluster.list.setting.btn': 'Update', 20 | 'cluster.list.setting.confirm.title': 'Do you want to update this cluster?', 21 | 'cluster.list.setting.confirm.content': 'When you clicked OK button, this cluster will be updated, and this dialog weill closed after 1 second!' 22 | } 23 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/cluster/scheduler.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cluster.scheduler.title': 'Cluster Scheduler', 3 | 'cluster.scheduler.btn.add': 'NEW', 4 | 'cluster.scheduler.table.id': 'ID', 5 | 'cluster.scheduler.table.name': 'name', 6 | 'cluster.scheduler.table.create': 'CreateTime', 7 | 'cluster.scheduler.table.cron': 'Cron', 8 | 'cluster.scheduler.table.status': 'Status', 9 | 'cluster.scheduler.table.status.no': 'inactive', 10 | 'cluster.scheduler.table.status.yes': 'active', 11 | 'cluster.scheduler.table.status.null': 'NA', 12 | 'cluster.scheduler.table.count': 'Count', 13 | 'cluster.scheduler.table.action': 'View Details', 14 | 'cluster.scheduler.table.action.history': 'history', 15 | 'cluster.scheduler.table.operation': 'Operation', 16 | 'cluster.scheduler.modal.title': 'Add Scheduler', 17 | 'cluster.scheduler.modal.name': 'Name', 18 | 'cluster.scheduler.modal.place.name': 'Please enter a name', 19 | 'cluster.scheduler.modal.radio.1': 'customize', 20 | 'cluster.scheduler.modal.radio.2': '08:00PM Every Night', 21 | 'cluster.scheduler.modal.radio.3': '00:00AM Every Sunday', 22 | 'cluster.scheduler.switch.child.yes': 'yes', 23 | 'cluster.scheduler.switch.child.no': 'no', 24 | 'cluster.scheduler.modal.active': 'Active now', 25 | 'cluster.scheduler.modal.cron': 'Cron', 26 | 'cluster.scheduler.modal.edit.title': 'Edit Scheduler', 27 | 'cluster.scheduler.btn.edit': 'edit', 28 | 'cluster.scheduler.btn.delete': 'delete', 29 | 'cluster.scheduler.delete.msg': 'Are you sure delete this scheduler?', 30 | 'cluster.scheduler.delete.ok': 'YES', 31 | 'cluster.scheduler.delete.cancel': 'NO' 32 | } 33 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/dashboard.js: -------------------------------------------------------------------------------- 1 | import analysis from './dashboard/analysis' 2 | 3 | export default { 4 | ...analysis 5 | } 6 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/dashboard/analysis.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'dashboard.analysis.test': 'Gongzhuan No.{no} shop', 3 | 'dashboard.analysis.introduce': 'Introduce', 4 | 'dashboard.analysis.total-sales': 'Total Sales', 5 | 'dashboard.analysis.day-sales': 'Daily Sales', 6 | 'dashboard.analysis.visits': 'Visits', 7 | 'dashboard.analysis.visits-trend': 'Visits Trend', 8 | 'dashboard.analysis.visits-ranking': 'Visits Ranking', 9 | 'dashboard.analysis.day-visits': 'Daily Visits', 10 | 'dashboard.analysis.week': 'WoW Change', 11 | 'dashboard.analysis.day': 'DoD Change', 12 | 'dashboard.analysis.payments': 'Payments', 13 | 'dashboard.analysis.conversion-rate': 'Conversion Rate', 14 | 'dashboard.analysis.operational-effect': 'Operational Effect', 15 | 'dashboard.analysis.sales-trend': 'Stores Sales Trend', 16 | 'dashboard.analysis.sales-ranking': 'Sales Ranking', 17 | 'dashboard.analysis.all-year': 'All Year', 18 | 'dashboard.analysis.all-month': 'All Month', 19 | 'dashboard.analysis.all-week': 'All Week', 20 | 'dashboard.analysis.all-day': 'All day', 21 | 'dashboard.analysis.search-users': 'Search Users', 22 | 'dashboard.analysis.per-capita-search': 'Per Capita Search', 23 | 'dashboard.analysis.online-top-search': 'Online Top Search', 24 | 'dashboard.analysis.the-proportion-of-sales': 'The Proportion Of Sales', 25 | 'dashboard.analysis.dropdown-option-one': 'Operation one', 26 | 'dashboard.analysis.dropdown-option-two': 'Operation two', 27 | 'dashboard.analysis.channel.all': 'ALL', 28 | 'dashboard.analysis.channel.online': 'Online', 29 | 'dashboard.analysis.channel.stores': 'Stores', 30 | 'dashboard.analysis.sales': 'Sales', 31 | 'dashboard.analysis.traffic': 'Traffic', 32 | 'dashboard.analysis.table.rank': 'Rank', 33 | 'dashboard.analysis.table.search-keyword': 'Keyword', 34 | 'dashboard.analysis.table.users': 'Users', 35 | 'dashboard.analysis.table.weekly-range': 'Weekly Range' 36 | } 37 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/dict.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'dict.script-tags': [ 3 | 'Cluster', 4 | 'Network', 5 | 'Storage', 6 | 'RunningState' 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/form.js: -------------------------------------------------------------------------------- 1 | import basicForm from './form/basicForm' 2 | 3 | export default { 4 | ...basicForm 5 | } 6 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/global.js: -------------------------------------------------------------------------------- 1 | export default { 2 | submit: 'Submit', 3 | save: 'Save', 4 | 'submit.ok': 'Submit successfully', 5 | 'save.ok': 'Saved successfully' 6 | } 7 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.home': 'Home', 4 | 'menu.cluster': 'Cluster', 5 | 'menu.cluster.list': 'List', 6 | 'menu.cluster.info': 'Cluster Infomation', 7 | 'menu.cluster.check.history': 'Check History', 8 | 'menu.cluster.check.probe': 'Check Probe', 9 | 'menu.cluster.check.execute': 'Execute Check', 10 | 'menu.cluster.scheduler': 'Scheduler', 11 | 'menu.cluster.settings': 'Cluster Settings', 12 | 'menu.cluster.user': 'User', 13 | 'menu.store': 'Store', 14 | 'menu.store.local': 'Local', 15 | 'menu.store.remote': 'Remote', 16 | 'menu.store.custom': 'Custom', 17 | 'menu.setting': 'Setting', 18 | 'menu.setting.store': 'Store Mirror', 19 | 'menu.account': 'Users', 20 | 'menu.account.center': 'Account Center', 21 | 'menu.account.settings': 'Account Settings', 22 | 'menu.account.trigger': 'Trigger Error', 23 | 'menu.account.logout': 'Logout' 24 | } 25 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/result.js: -------------------------------------------------------------------------------- 1 | import success from './result/success' 2 | import fail from './result/fail' 3 | 4 | export default { 5 | ...success, 6 | ...fail 7 | } 8 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/result/fail.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.fail.error.title': 'Submission Failed', 3 | 'result.fail.error.description': 4 | 'Please check and modify the following information before resubmitting.', 5 | 'result.fail.error.hint-title': 'The content you submitted has the following error:', 6 | 'result.fail.error.hint-text1': 'Your account has been frozen', 7 | 'result.fail.error.hint-btn1': 'Thaw immediately', 8 | 'result.fail.error.hint-text2': 'Your account is not yet eligible to apply', 9 | 'result.fail.error.hint-btn2': 'Upgrade immediately', 10 | 'result.fail.error.btn-text': 'Return to modify' 11 | } 12 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/result/success.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.success.title': 'Submission Success', 3 | 'result.success.description': 4 | 'The submission results page is used to feed back the results of a series of operational tasks. If it is a simple operation, use the Message global prompt feedback. This text area can show a simple supplementary explanation. If there is a similar requirement for displaying “documents”, the following gray area can present more complicated content.', 5 | 'result.success.operate-title': 'Project Name', 6 | 'result.success.operate-id': 'Project ID', 7 | 'result.success.principal': 'Principal', 8 | 'result.success.operate-time': 'Effective time', 9 | 'result.success.step1-title': 'Create project', 10 | 'result.success.step1-operator': 'Qu Lili', 11 | 'result.success.step2-title': 'Departmental preliminary review', 12 | 'result.success.step2-operator': 'Zhou Maomao', 13 | 'result.success.step2-extra': 'Urge', 14 | 'result.success.step3-title': 'Financial review', 15 | 'result.success.step4-title': 'Finish', 16 | 'result.success.btn-return': 'Back List', 17 | 'result.success.btn-project': 'View Project', 18 | 'result.success.btn-print': 'Print' 19 | } 20 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/setting.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.light': 'Light style', 4 | 'app.setting.pagestyle.dark': 'Dark style', 5 | 'app.setting.pagestyle.realdark': 'RealDark style', 6 | 'app.setting.themecolor': 'Theme Color', 7 | 'app.setting.navigationmode': 'Navigation Mode', 8 | 'app.setting.content-width': 'Content Width', 9 | 'app.setting.fixedheader': 'Fixed Header', 10 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 11 | 'app.setting.sidemenu': 'Side Menu Layout', 12 | 'app.setting.topmenu': 'Top Menu Layout', 13 | 'app.setting.content-width.fixed': 'Fixed', 14 | 'app.setting.content-width.fluid': 'Fluid', 15 | 'app.setting.othersettings': 'Other Settings', 16 | 'app.setting.weakmode': 'Weak Mode', 17 | 'app.setting.copy': 'Copy Setting', 18 | 'app.setting.loading': 'Loading theme', 19 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/config/defaultSettings.js', 20 | 'app.setting.production.hint': 'Setting panel shows in development environment only, please manually modify', 21 | 'app.setting.themecolor.daybreak': 'Daybreak Blue', 22 | 'app.setting.themecolor.dust': 'Dust Red', 23 | 'app.setting.themecolor.volcano': 'Volcano', 24 | 'app.setting.themecolor.sunset': 'Sunset Orange', 25 | 'app.setting.themecolor.cyan': 'Cyan', 26 | 'app.setting.themecolor.green': 'Polar Green', 27 | 'app.setting.themecolor.geekblue': 'Geek Blue', 28 | 'app.setting.themecolor.purple': 'Golden Purple' 29 | } 30 | -------------------------------------------------------------------------------- /web/src/locales/lang/en-US/store.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'store.page.update-time': 'publish at', 3 | 'store.page.local.title': 'Local Store', 4 | 'store.page.local.modal.title': 'ReadMe', 5 | 'store.page.remote.title': 'Remote Store', 6 | 'store.page.custom.title': 'Custom Store', 7 | 'store.page.custom.delete-tip': 'Are you sure delete this Probe?', 8 | 'store.page.custom.upload': 'Upload', 9 | 'store.page.custom.upload.modal-title': 'Upload Porbe', 10 | 'store.page.custom.upload.select-file': 'Select File', 11 | 'store.page.custom.upload.tips-title': 'The following Tips', 12 | 'store.page.custom.upload.tips-text1': `The name of the package must be named by the probe's id.`, 13 | 'store.page.custom.upload.tips-text2': 'Tips2', 14 | 'store.page.custom.upload.tips-text3': 'Tips3', 15 | 'store.page.custom.upload.tips-more': 'Read More', 16 | 'store.page.custom.readme.title': 'ReadMe', 17 | } 18 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | import antd from 'ant-design-vue/es/locale-provider/zh_CN' 2 | import momentCN from 'moment/locale/zh-cn' 3 | import global from './zh-CN/global' 4 | 5 | import menu from './zh-CN/menu' 6 | import setting from './zh-CN/setting' 7 | import user from './zh-CN/user' 8 | import dashboard from './zh-CN/dashboard' 9 | import form from './zh-CN/form' 10 | import result from './zh-CN/result' 11 | import account from './zh-CN/account' 12 | import cluster from './zh-CN/cluster' 13 | import check from './zh-CN/check' 14 | 15 | import store from './zh-CN/store' 16 | 17 | const components = { 18 | antLocale: antd, 19 | momentName: 'zh-cn', 20 | momentLocale: momentCN 21 | } 22 | 23 | export default { 24 | message: '-', 25 | 26 | 'layouts.usermenu.dialog.title': '信息', 27 | 'layouts.usermenu.dialog.content': '您确定要注销吗?', 28 | 'layouts.userLayout.title': 'TiDB automated checklist for hackathon 2021.', 29 | ...components, 30 | ...global, 31 | ...menu, 32 | ...setting, 33 | ...user, 34 | ...dashboard, 35 | ...form, 36 | ...result, 37 | ...account, 38 | ...cluster, 39 | ...check, 40 | ...store 41 | } 42 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/account.js: -------------------------------------------------------------------------------- 1 | import settings from './account/settings' 2 | 3 | export default { 4 | ...settings 5 | } 6 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/check.js: -------------------------------------------------------------------------------- 1 | import history from './check/history' 2 | import probe from './check/probe' 3 | import execute from './check/execute' 4 | 5 | export default { 6 | ...history, 7 | ...probe, 8 | ...execute 9 | } 10 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/check/execute.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'check.execute.title': '执行巡检', 3 | 'check.execute.times': '已巡检次数', 4 | 'check.execute.lastTime': '上次巡检时间', 5 | 'check.execute.runCheck': '执行巡检', 6 | 'check.execute.editProbe': '编辑指标', 7 | 'check.execute.checkResult': '巡检结果', 8 | 9 | // modal 10 | 'check.execute.modal.title': '执行巡检', 11 | 'check.execute.modal.contents': '确定执行巡检吗?', 12 | 13 | 14 | // notification 15 | 'check.execute.notification.success': '巡检已经成功执行。', 16 | 'check.execute.notification.timeout': '巡检执行超时', 17 | 'check.execute.notification.error': '无法连接到服务器', 18 | 'check.execute.notification.info.no_probe.message': '没有巡检指标', 19 | 'check.execute.notification.info.no_probe.description': '目前集群中没有任何巡检指标,请给集群添加指标!', 20 | 'check.execute.notification.error.disconnected': '服务器异常断开连接', 21 | 'check.execute.notification.error.conflict': '集群当前存在一个正在运行的巡检任务,请稍后再试', 22 | 23 | 24 | // List 25 | 'check.execute.list.check_threshold': '检查阈值', 26 | 'check.execute.list.result': '结果值', 27 | } 28 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/check/history.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'check.history.title': '巡检历史', 3 | 'check.history.timeSelect': '时间筛选', 4 | 'check.history.table.checkTime': '巡检时间', 5 | 'check.history.table.checkState': '状态', 6 | 'check.history.table.duration': '耗时', 7 | 'check.history.table.normal': '正常项', 8 | 'check.history.table.warning': '异常项', 9 | 'check.history.table.total': '总数', 10 | 'check.history.table.action': '操作', 11 | 'check.history.table.detail': '详情', 12 | 'check.history.table.download': '下载', 13 | 14 | // detail 15 | 'check.history.detail.title': '巡检详情', 16 | 'check.history.detail.download': '下载', 17 | 'check.history.detail.table.name': '指标名称', 18 | 'check.history.detail.table.tag': '标签', 19 | 'check.history.detail.table.item': '巡检项', 20 | 'check.history.detail.table.status': '状态', 21 | 'check.history.detail.table.operator': '运算符', 22 | 'check.history.detail.table.threshold': '阈值', 23 | 'check.history.detail.table.actual': '实际值', 24 | 'check.history.detail.table.duration': '耗时', 25 | 'check.history.detail.table.status.normal': '正常', 26 | 'check.history.detail.table.status.new_abnormal': '新增异常', 27 | 'check.history.detail.table.status.existing_abnormal': '已有异常', 28 | 'check.history.detail.table.status.script_error': '脚本错误', 29 | } 30 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/check/probe.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'check.probe.title': '集群指标', 3 | 'check.probe.add': '添加巡检项', 4 | 'check.probe.table.probeID': 'ID', 5 | 'check.probe.table.name': '指标名称', 6 | 'check.probe.table.tag': '标签', 7 | 'check.probe.table.description': '描述', 8 | 'check.probe.table.operator': '运算符', 9 | 'check.probe.table.threshold': '阈值', 10 | 'check.probe.table.isEnabled': '是否开启', 11 | 'check.probe.table.isEnabled.enabled': '启用', 12 | 'check.probe.table.isEnabled.disabled': '禁用', 13 | 'check.probe.table.search': '搜索', 14 | 'check.probe.table.reset': '重置', 15 | 'check.probe.table.action': '操作', 16 | 'check.probe.table.edit': '编辑', 17 | 'check.probe.table.delete': '删除', 18 | 19 | 'check.probe.table.delete.modal.title': '删除指标', 20 | 'check.probe.table.delete.modal.contents': '确定删除该指标吗?', 21 | 22 | // tag 23 | 'check.probe.tag.all': '全部', 24 | 'check.probe.tag.cluster': '集群', 25 | 'check.probe.tag.network': '网络', 26 | 'check.probe.tag.running_state': '运行状态', 27 | 'check.probe.tag.others': '其他', 28 | 29 | // add probe 30 | 'check.probe.add.title': '添加巡检项', 31 | 'check.probe.add.local': '本地', 32 | 'check.probe.add.remote': '远程', 33 | 'check.probe.add.custom': '自定义', 34 | 'check.probe.add.table.probeID': 'ID', 35 | 'check.probe.add.table.name': '指标名称', 36 | 'check.probe.add.table.tag': '标签', 37 | 'check.probe.add.table.operator': '运算符', 38 | 'check.probe.add.table.threshold': '阈值', 39 | 'check.probe.add.table.creator': '创建者', 40 | 'check.probe.add.table.updateTime': '更新时间', 41 | 'check.probe.add.table.action': '操作', 42 | 'check.probe.add.table.add': '添加', 43 | 'check.probe.add.table.search': '搜索', 44 | 'check.probe.add.table.reset': '重置', 45 | 46 | // module 47 | 'check.probe.module.title': '编辑巡检项配置', 48 | 'check.probe.module.id': 'ID', 49 | 'check.probe.module.name': '指标名称', 50 | 'check.probe.module.operator': '运算符', 51 | 'check.probe.module.threshold': '阈值', 52 | 'check.probe.module.submit': '提交', 53 | 'check.probe.module.cancel': '取消', 54 | } 55 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/cluster.js: -------------------------------------------------------------------------------- 1 | import list from './cluster/list' 2 | import info from './cluster/info' 3 | import scheduler from './cluster/scheduler' 4 | 5 | export default { 6 | ...list, 7 | ...info, 8 | ...scheduler 9 | } 10 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/cluster/info.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cluster.info.cluster-info': '集群详情', 3 | 'cluster.info.check': '一键巡检', 4 | 'cluster.info.name': '集群名称', 5 | 'cluster.info.version': '集群版本', 6 | 'cluster.info.create-time': '添加时间', 7 | 'cluster.info.description': '集群描述', 8 | 'cluster.info.owner': '集群用户', 9 | 'cluster.info.status': '集群状态', 10 | 'cluster.info.status.count': '累计巡检次数', 11 | 'cluster.info.status.total': '累计检查项目', 12 | 'cluster.info.status.count-today': '今日巡检次数', 13 | 'cluster.info.status.total-today': '今日巡检项数', 14 | 'cluster.info.status.last': '最后巡检时间', 15 | 'cluster.info.status.last-normal': '正常项数', 16 | 'cluster.info.status.last-warning': '告警项数', 17 | 'cluster.info.status.healthy': '集群健康度', 18 | 'cluster.info.status.healthy-update': '更新时间', 19 | 'cluster.info.recent': '最新告警趋势' 20 | } 21 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/cluster/list.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cluster.list.cluster-list': '集群列表', 3 | 'cluster.list.name': '集群名称', 4 | 'cluster.list.add-cluster': '添加集群', 5 | 'cluster.list.add-time': '添加时间', 6 | 'cluster.list.description': '集群描述', 7 | 'cluster.list.description.else': '暂无数据库简介', 8 | 'cluster.list.node-info': '节点信息', 9 | 'cluster.list.prometheus.warning': '无法连接到集群Prometheus服务器', 10 | 'cluster.list.last-check-time': '最后巡检时间', 11 | 'cluster.list.prometheus': 'Prometheus地址', 12 | 'cluster.list.user': '登录用户', 13 | 'cluster.list.passwd': '登录密码', 14 | 'cluster.list.input.name': '请输入TiDB数据库名称', 15 | 'cluster.list.input.prometheus': '请输入Prometheus地址', 16 | 'cluster.list.input.user': '请输入TiDB数据库用户', 17 | 'cluster.list.input.passwd': '请输入TiDB数据库密码', 18 | 'cluster.list.input.description': '请描述下TiDB数据库', 19 | 'cluster.list.setting.title': '更新集群', 20 | 'cluster.list.setting.btn': '更新', 21 | 'cluster.list.setting.confirm.title': '是否选择更新该集群?', 22 | 'cluster.list.setting.confirm.content': '一旦按下确认按钮,集群将会得到更新,并且该窗口会在1秒后关闭' 23 | } 24 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/cluster/scheduler.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cluster.scheduler.title': '自动巡检', 3 | 'cluster.scheduler.btn.add': '新增', 4 | 'cluster.scheduler.table.id': 'ID', 5 | 'cluster.scheduler.table.name': '说明', 6 | 'cluster.scheduler.table.create': '创建时间', 7 | 'cluster.scheduler.table.cron': '定时器', 8 | 'cluster.scheduler.table.status': '状态', 9 | 'cluster.scheduler.table.status.no': '未激活', 10 | 'cluster.scheduler.table.status.yes': '激活', 11 | 'cluster.scheduler.table.status.null': 'NA', 12 | 'cluster.scheduler.table.count': '巡检次数', 13 | 'cluster.scheduler.table.action': '查看详情', 14 | 'cluster.scheduler.table.action.history': '巡检历史', 15 | 'cluster.scheduler.table.operation': '操作', 16 | 'cluster.scheduler.modal.title': '添加自动巡检', 17 | 'cluster.scheduler.modal.name': '说明', 18 | 'cluster.scheduler.modal.place.name': '请输入名称', 19 | 'cluster.scheduler.modal.radio.1': '自定义', 20 | 'cluster.scheduler.modal.radio.2': '每晚8点', 21 | 'cluster.scheduler.modal.radio.3': '每周日0点', 22 | 'cluster.scheduler.switch.child.yes': '是', 23 | 'cluster.scheduler.switch.child.no': '否', 24 | 'cluster.scheduler.modal.active': '是否启用', 25 | 'cluster.scheduler.modal.cron': '定时器', 26 | 'cluster.scheduler.modal.edit.title': '编辑自动巡检', 27 | 'cluster.scheduler.btn.edit': '编辑', 28 | 'cluster.scheduler.btn.delete': '删除', 29 | 'cluster.scheduler.delete.msg': '你确定要删除这条规则吗?', 30 | 'cluster.scheduler.delete.ok': '确定', 31 | 'cluster.scheduler.delete.cancel': '取消' 32 | } 33 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/dashboard.js: -------------------------------------------------------------------------------- 1 | import analysis from './dashboard/analysis' 2 | 3 | export default { 4 | ...analysis 5 | } 6 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/dashboard/analysis.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'dashboard.analysis.test': '工专路 {no} 号店', 3 | 'dashboard.analysis.introduce': '指标说明', 4 | 'dashboard.analysis.total-sales': '总销售额', 5 | 'dashboard.analysis.day-sales': '日均销售额¥', 6 | 'dashboard.analysis.visits': '访问量', 7 | 'dashboard.analysis.visits-trend': '访问量趋势', 8 | 'dashboard.analysis.visits-ranking': '门店访问量排名', 9 | 'dashboard.analysis.day-visits': '日访问量', 10 | 'dashboard.analysis.week': '周同比', 11 | 'dashboard.analysis.day': '日同比', 12 | 'dashboard.analysis.payments': '支付笔数', 13 | 'dashboard.analysis.conversion-rate': '转化率', 14 | 'dashboard.analysis.operational-effect': '运营活动效果', 15 | 'dashboard.analysis.sales-trend': '销售趋势', 16 | 'dashboard.analysis.sales-ranking': '门店销售额排名', 17 | 'dashboard.analysis.all-year': '全年', 18 | 'dashboard.analysis.all-month': '本月', 19 | 'dashboard.analysis.all-week': '本周', 20 | 'dashboard.analysis.all-day': '今日', 21 | 'dashboard.analysis.search-users': '搜索用户数', 22 | 'dashboard.analysis.per-capita-search': '人均搜索次数', 23 | 'dashboard.analysis.online-top-search': '线上热门搜索', 24 | 'dashboard.analysis.the-proportion-of-sales': '销售额类别占比', 25 | 'dashboard.analysis.dropdown-option-one': '操作一', 26 | 'dashboard.analysis.dropdown-option-two': '操作二', 27 | 'dashboard.analysis.channel.all': '全部渠道', 28 | 'dashboard.analysis.channel.online': '线上', 29 | 'dashboard.analysis.channel.stores': '门店', 30 | 'dashboard.analysis.sales': '销售额', 31 | 'dashboard.analysis.traffic': '客流量', 32 | 'dashboard.analysis.table.rank': '排名', 33 | 'dashboard.analysis.table.search-keyword': '搜索关键词', 34 | 'dashboard.analysis.table.users': '用户数', 35 | 'dashboard.analysis.table.weekly-range': '周涨幅' 36 | } 37 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/form.js: -------------------------------------------------------------------------------- 1 | import basicForm from './form/basicForm' 2 | 3 | export default { 4 | ...basicForm 5 | } 6 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/global.js: -------------------------------------------------------------------------------- 1 | export default { 2 | submit: '提交', 3 | save: '保存', 4 | 'submit.ok': '提交成功', 5 | 'save.ok': '保存成功' 6 | } 7 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.home': '主页', 4 | 'menu.account': '用户', 5 | 'menu.account.center': '个人中心', 6 | 'menu.account.settings': '个人设置', 7 | 'menu.account.trigger': '触发报错', 8 | 'menu.account.logout': '退出登录', 9 | 'menu.cluster': '集群', 10 | 'menu.cluster.info': '集群信息', 11 | 'menu.cluster.check.history': '巡检历史', 12 | 'menu.cluster.check.probe': '巡检指标', 13 | 'menu.cluster.check.execute': '执行巡检', 14 | 'menu.cluster.scheduler': '定时任务', 15 | 'menu.cluster.settings': '集群设置', 16 | 'menu.cluster.user': '用户管理', 17 | 'menu.store': '商店', 18 | 'menu.store.local': '本地商店', 19 | 'menu.store.remote': '远程商店', 20 | 'menu.store.custom': '自定义商店', 21 | 'menu.setting': '设置', 22 | 'menu.setting.store': '商店镜像', 23 | } 24 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/result.js: -------------------------------------------------------------------------------- 1 | import success from './result/success' 2 | import fail from './result/fail' 3 | 4 | export default { 5 | ...success, 6 | ...fail 7 | } 8 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/result/fail.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.fail.error.title': '提交失败', 3 | 'result.fail.error.description': '请核对并修改以下信息后,再重新提交。', 4 | 'result.fail.error.hint-title': '您提交的内容有如下错误:', 5 | 'result.fail.error.hint-text1': '您的账户已被冻结', 6 | 'result.fail.error.hint-btn1': '立即解冻', 7 | 'result.fail.error.hint-text2': '您的账户还不具备申请资格', 8 | 'result.fail.error.hint-btn2': '立即升级', 9 | 'result.fail.error.btn-text': '返回修改' 10 | } 11 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/result/success.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'result.success.title': '提交成功', 3 | 'result.success.description': 4 | '提交结果页用于反馈一系列操作任务的处理结果, 如果仅是简单操作,使用 Message 全局提示反馈即可。 本文字区域可以展示简单的补充说明,如果有类似展示 “单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。', 5 | 'result.success.operate-title': '项目名称', 6 | 'result.success.operate-id': '项目 ID', 7 | 'result.success.principal': '负责人', 8 | 'result.success.operate-time': '生效时间', 9 | 'result.success.step1-title': '创建项目', 10 | 'result.success.step1-operator': '曲丽丽', 11 | 'result.success.step2-title': '部门初审', 12 | 'result.success.step2-operator': '周毛毛', 13 | 'result.success.step2-extra': '催一下', 14 | 'result.success.step3-title': '财务复核', 15 | 'result.success.step4-title': '完成', 16 | 'result.success.btn-return': '返回列表', 17 | 'result.success.btn-project': '查看项目', 18 | 'result.success.btn-print': '打印' 19 | } 20 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/setting.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.light': '亮色菜单风格', 4 | 'app.setting.pagestyle.dark': '暗色菜单风格', 5 | 'app.setting.pagestyle.realdark': '暗黑模式', 6 | 'app.setting.themecolor': '主题色', 7 | 'app.setting.navigationmode': '导航模式', 8 | 'app.setting.content-width': '内容区域宽度', 9 | 'app.setting.fixedheader': '固定 Header', 10 | 'app.setting.fixedsidebar': '固定侧边栏', 11 | 'app.setting.sidemenu': '侧边菜单布局', 12 | 'app.setting.topmenu': '顶部菜单布局', 13 | 'app.setting.content-width.fixed': 'Fixed', 14 | 'app.setting.content-width.fluid': 'Fluid', 15 | 'app.setting.othersettings': '其他设置', 16 | 'app.setting.weakmode': '色弱模式', 17 | 'app.setting.copy': '拷贝设置', 18 | 'app.setting.loading': '加载主题中', 19 | 'app.setting.copyinfo': '拷贝设置成功 src/config/defaultSettings.js', 20 | 'app.setting.production.hint': '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 21 | 'app.setting.themecolor.daybreak': '拂晓蓝', 22 | 'app.setting.themecolor.dust': '薄暮', 23 | 'app.setting.themecolor.volcano': '火山', 24 | 'app.setting.themecolor.sunset': '日暮', 25 | 'app.setting.themecolor.cyan': '明青', 26 | 'app.setting.themecolor.green': '极光绿', 27 | 'app.setting.themecolor.geekblue': '极客蓝', 28 | 'app.setting.themecolor.purple': '酱紫' 29 | } 30 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/store.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'store.page.update-time': '发布时间', 3 | 'store.page.local.title': '本地商店', 4 | 'store.page.local.modal.title': '说明文档', 5 | 'store.page.remote.title': '远程商店', 6 | 'store.page.custom.title': '自定义商店', 7 | 'store.page.custom.delete-tip': '确认要删除这个巡检项吗?', 8 | 'store.page.custom.upload': '上传', 9 | 'store.page.custom.upload.modal-title': '上传巡检指标', 10 | 'store.page.custom.upload.select-file': '选择文件', 11 | 'store.page.custom.upload.tips-title': '上传提示', 12 | 'store.page.custom.upload.tips-text1': `The name of the package must be named by the probe's id.`, 13 | 'store.page.custom.upload.tips-text2': 'Tips2', 14 | 'store.page.custom.upload.tips-text3': 'Tips3', 15 | 'store.page.custom.upload.tips-more': '了解更多', 16 | 'store.page.custom.readme.title': '说明文档', 17 | } 18 | -------------------------------------------------------------------------------- /web/src/locales/lang/zh-CN/user.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'user.login.userName': '用户名', 3 | 'user.login.password': '密码', 4 | 'user.login.username.placeholder': '账户: admin', 5 | 'user.login.password.placeholder': '密码: admin or ant.design', 6 | 'user.login.message-invalid-credentials': '账户或密码错误(admin/ant.design)', 7 | 'user.login.message-invalid-verification-code': '验证码错误', 8 | 'user.login.tab-login-credentials': '账户密码登录', 9 | 'user.login.tab-login-mobile': 'LDAP', 10 | 'user.login.mobile.placeholder': '手机号', 11 | 'user.login.mobile.verification-code.placeholder': '验证码', 12 | 'user.login.remember-me': '自动登录', 13 | 'user.login.forgot-password': '忘记密码', 14 | 'user.login.sign-in-with': '其他登录方式', 15 | 'user.login.signup': '注册账户', 16 | 'user.login.login': '登录', 17 | 'user.register.register': '注册', 18 | 'user.register.email.placeholder': '邮箱', 19 | 'user.register.password.placeholder': '请至少输入 6 个字符。请不要使用容易被猜到的密码。', 20 | 'user.register.password.popover-message': '请至少输入 6 个字符。请不要使用容易被猜到的密码。', 21 | 'user.register.confirm-password.placeholder': '确认密码', 22 | 'user.register.get-verification-code': '获取验证码', 23 | 'user.register.sign-in': '使用已有账户登录', 24 | 'user.register-result.msg': '你的账户:{email} 注册成功', 25 | 'user.register-result.activation-email': 26 | '激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。', 27 | 'user.register-result.back-home': '返回首页', 28 | 'user.register-result.view-mailbox': '查看邮箱', 29 | 'user.email.required': '请输入邮箱地址!', 30 | 'user.email.wrong-format': '邮箱地址格式错误!', 31 | 'user.userName.required': '请输入帐户名或邮箱地址', 32 | 'user.password.required': '请输入密码!', 33 | 'user.password.twice.msg': '两次输入的密码不匹配!', 34 | 'user.password.strength.msg': '密码强度不够 ', 35 | 'user.password.strength.strong': '强度:强', 36 | 'user.password.strength.medium': '强度:中', 37 | 'user.password.strength.low': '强度:低', 38 | 'user.password.strength.short': '强度:太短', 39 | 'user.confirm-password.required': '请确认密码!', 40 | 'user.phone-number.required': '请输入正确的手机号', 41 | 'user.phone-number.wrong-format': '手机号格式错误!', 42 | 'user.verification-code.required': '请输入验证码!' 43 | } 44 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | // with polyfills 2 | import 'core-js/stable' 3 | import 'regenerator-runtime/runtime' 4 | 5 | import Vue from 'vue' 6 | import App from './App.vue' 7 | import router from './router' 8 | import store from './store/' 9 | import i18n from './locales' 10 | import { VueAxios } from './utils/request' 11 | import hljs from 'highlight.js' 12 | import 'highlight.js/styles/github.css' 13 | import ProLayout, { PageHeaderWrapper } from '@ant-design-vue/pro-layout' 14 | import themePluginConfig from '../config/themePluginConfig' 15 | 16 | // mock 17 | // WARNING: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV. 18 | import './mock' 19 | 20 | import bootstrap from './core/bootstrap' 21 | import './core/lazy_use' // use lazy load components 22 | import './permission' // permission control 23 | import './utils/filter' // global filter 24 | import './global.less' // global style 25 | 26 | 27 | Vue.config.productionTip = false 28 | 29 | // mount axios to `Vue.$http` and `this.$http` 30 | Vue.use(VueAxios) 31 | // use pro-layout components 32 | Vue.component('pro-layout', ProLayout) 33 | Vue.component('page-container', PageHeaderWrapper) 34 | Vue.component('page-header-wrapper', PageHeaderWrapper) 35 | 36 | Vue.directive('highlight',function (el) { 37 | let blocks = el.querySelectorAll('pre code'); 38 | blocks.forEach((block)=>{ 39 | hljs.highlightBlock(block) 40 | }) 41 | }) 42 | 43 | 44 | window.umi_plugin_ant_themeVar = themePluginConfig.theme 45 | 46 | new Vue({ 47 | router, 48 | store, 49 | i18n, 50 | // init localstorage, vuex, Logo message 51 | created: bootstrap, 52 | render: h => h(App) 53 | }).$mount('#app') 54 | -------------------------------------------------------------------------------- /web/src/mock/index.js: -------------------------------------------------------------------------------- 1 | import { isIE } from '@/utils/util' 2 | 3 | // 判断环境不是 prod 或者 preview 是 true 时,加载 mock 服务 4 | if (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true') { 5 | if (isIE()) { 6 | console.error('[antd-pro] ERROR: `mockjs` NOT SUPPORT `IE` PLEASE DO NOT USE IN `production` ENV.') 7 | } 8 | // 使用同步加载依赖 9 | // 防止 vuex 中的 GetInfo 早于 mock 运行,导致无法 mock 请求返回结果 10 | console.log('[antd-pro] mock mounting') 11 | const Mock = require('mockjs2') 12 | require('./services/auth') 13 | require('./services/user') 14 | require('./services/manage') 15 | require('./services/other') 16 | require('./services/tagCloud') 17 | require('./services/article') 18 | 19 | Mock.setup({ 20 | timeout: 800 // setter delay time 21 | }) 22 | console.log('[antd-pro] mock mounted') 23 | } 24 | -------------------------------------------------------------------------------- /web/src/mock/services/auth.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs2' 2 | import { builder, getBody } from '../util' 3 | 4 | const username = ['admin', 'super'] 5 | // 强硬要求 ant.design 相同密码 6 | // '21232f297a57a5a743894a0e4a801fc3', 7 | const password = ['8914de686ab28dc22f30d3d8e107ff6c', '21232f297a57a5a743894a0e4a801fc3'] // admin, ant.design 8 | 9 | const login = (options) => { 10 | const body = getBody(options) 11 | console.log('mock: body', body) 12 | if (!username.includes(body.username) || !password.includes(body.password)) { 13 | return builder({ isLogin: true }, '账户或密码错误', 401) 14 | } 15 | 16 | return builder({ 17 | 'id': Mock.mock('@guid'), 18 | 'name': Mock.mock('@name'), 19 | 'username': 'admin', 20 | 'password': '', 21 | 'avatar': 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png', 22 | 'status': 1, 23 | 'telephone': '', 24 | 'lastLoginIp': '27.154.74.117', 25 | 'lastLoginTime': 1534837621348, 26 | 'creatorId': 'admin', 27 | 'createTime': 1497160610259, 28 | 'deleted': 0, 29 | 'roleId': 'admin', 30 | 'lang': 'en-US', 31 | 'token': '4291d7da9005377ec9aec4a71ea837f' 32 | }, '', 200, { 'Custom-Header': Mock.mock('@guid') }) 33 | } 34 | 35 | const logout = () => { 36 | return builder({}, '[测试接口] 注销成功') 37 | } 38 | 39 | const smsCaptcha = () => { 40 | return builder({ captcha: Mock.mock('@integer(10000, 99999)') }) 41 | } 42 | 43 | const twofactor = () => { 44 | return builder({ stepCode: Mock.mock('@integer(0, 1)') }) 45 | } 46 | 47 | Mock.mock(/\/auth\/login/, 'post', login) 48 | Mock.mock(/\/auth\/logout/, 'post', logout) 49 | Mock.mock(/\/account\/sms/, 'post', smsCaptcha) 50 | Mock.mock(/\/auth\/2step-code/, 'post', twofactor) 51 | -------------------------------------------------------------------------------- /web/src/mock/util.js: -------------------------------------------------------------------------------- 1 | const responseBody = { 2 | message: '', 3 | timestamp: 0, 4 | result: null, 5 | code: 0 6 | } 7 | 8 | export const 9 | builder = (data, message, code = 0, headers = {}) => { 10 | responseBody.result = data 11 | if (message !== undefined && message !== null) { 12 | responseBody.message = message 13 | } 14 | if (code !== undefined && code !== 0) { 15 | responseBody.code = code 16 | responseBody._status = code 17 | } 18 | if (headers !== null && typeof headers === 'object' && Object.keys(headers).length > 0) { 19 | responseBody._headers = headers 20 | } 21 | responseBody.timestamp = new Date().getTime() 22 | return responseBody 23 | } 24 | 25 | export const getQueryParameters = (options) => { 26 | const url = options.url 27 | const search = url.split('?')[1] 28 | if (!search) { 29 | return {} 30 | } 31 | return JSON.parse('{"' + decodeURIComponent(search) 32 | .replace(/"/g, '\\"') 33 | .replace(/&/g, '","') 34 | .replace(/=/g, '":"') + '"}') 35 | } 36 | 37 | export const getBody = (options) => { 38 | return options.body && JSON.parse(options.body) 39 | } 40 | -------------------------------------------------------------------------------- /web/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { constantRouterMap } from '@/config/router.config' 4 | 5 | // hack router push callback 6 | const originalPush = Router.prototype.push 7 | Router.prototype.push = function push (location, onResolve, onReject) { 8 | if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject) 9 | return originalPush.call(this, location).catch(err => err) 10 | } 11 | 12 | Vue.use(Router) 13 | 14 | export default new Router({ 15 | mode: 'history', 16 | routes: constantRouterMap 17 | }) 18 | -------------------------------------------------------------------------------- /web/src/store/app-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const baseMixin = { 4 | computed: { 5 | ...mapState({ 6 | layout: state => state.app.layout, 7 | navTheme: state => state.app.theme, 8 | primaryColor: state => state.app.color, 9 | colorWeak: state => state.app.weak, 10 | fixedHeader: state => state.app.fixedHeader, 11 | fixedSidebar: state => state.app.fixedSidebar, 12 | contentWidth: state => state.app.contentWidth, 13 | autoHideHeader: state => state.app.autoHideHeader, 14 | 15 | isMobile: state => state.app.isMobile, 16 | sideCollapsed: state => state.app.sideCollapsed, 17 | multiTab: state => state.app.multiTab 18 | }), 19 | isTopMenu () { 20 | return this.layout === 'topmenu' 21 | } 22 | }, 23 | methods: { 24 | isSideMenu () { 25 | return !this.isTopMenu 26 | } 27 | } 28 | } 29 | 30 | export { 31 | baseMixin 32 | } 33 | -------------------------------------------------------------------------------- /web/src/store/device-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const deviceMixin = { 4 | computed: { 5 | ...mapState({ 6 | isMobile: state => state.app.isMobile 7 | }) 8 | } 9 | } 10 | 11 | export { deviceMixin } 12 | -------------------------------------------------------------------------------- /web/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | isMobile: state => state.app.isMobile, 3 | lang: state => state.app.lang, 4 | theme: state => state.app.theme, 5 | color: state => state.app.color, 6 | token: state => state.user.token, 7 | avatar: state => state.user.avatar, 8 | nickname: state => state.user.name, 9 | welcome: state => state.user.welcome, 10 | roles: state => state.user.roles, 11 | userInfo: state => state.user.info, 12 | addRouters: state => state.permission.addRouters, 13 | multiTab: state => state.app.multiTab 14 | } 15 | 16 | export default getters 17 | -------------------------------------------------------------------------------- /web/src/store/i18n-mixin.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | const i18nMixin = { 4 | computed: { 5 | ...mapState({ 6 | currentLang: state => state.app.lang 7 | }) 8 | }, 9 | methods: { 10 | setLang (lang) { 11 | this.$store.dispatch('setLang', lang) 12 | } 13 | } 14 | } 15 | 16 | export default i18nMixin 17 | -------------------------------------------------------------------------------- /web/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import app from './modules/app' 5 | import user from './modules/user' 6 | 7 | // default router permission control 8 | import permission from './modules/permission' 9 | 10 | // dynamic router permission control (Experimental) 11 | // import permission from './modules/async-router' 12 | import getters from './getters' 13 | 14 | Vue.use(Vuex) 15 | 16 | export default new Vuex.Store({ 17 | modules: { 18 | app, 19 | user, 20 | permission 21 | }, 22 | state: {}, 23 | mutations: {}, 24 | actions: {}, 25 | getters 26 | }) 27 | -------------------------------------------------------------------------------- /web/src/store/modules/async-router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 向后端请求用户的菜单,动态生成路由 3 | */ 4 | import { constantRouterMap } from '@/config/router.config' 5 | import { generatorDynamicRouter } from '@/router/generator-routers' 6 | 7 | const permission = { 8 | state: { 9 | routers: constantRouterMap, 10 | addRouters: [] 11 | }, 12 | mutations: { 13 | SET_ROUTERS: (state, routers) => { 14 | state.addRouters = routers 15 | state.routers = constantRouterMap.concat(routers) 16 | } 17 | }, 18 | actions: { 19 | GenerateRoutes ({ commit }, data) { 20 | debugger 21 | return new Promise(resolve => { 22 | const { token } = data 23 | generatorDynamicRouter(token).then(routers => { 24 | commit('SET_ROUTERS', routers) 25 | resolve() 26 | }) 27 | })`` 28 | } 29 | } 30 | } 31 | 32 | export default permission 33 | -------------------------------------------------------------------------------- /web/src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRouterMap, constantRouterMap } from '@/config/router.config' 2 | import cloneDeep from 'lodash.clonedeep' 3 | 4 | /** 5 | * 过滤账户是否拥有某一个权限,并将菜单从加载列表移除 6 | * 7 | * @param permission 8 | * @param route 9 | * @returns {boolean} 10 | */ 11 | function hasPermission (permission, route) { 12 | if (route.meta && route.meta.permission) { 13 | let flag = false 14 | for (let i = 0, len = permission.length; i < len; i++) { 15 | flag = route.meta.permission.includes(permission[i]) 16 | if (flag) { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | return true 23 | } 24 | 25 | /** 26 | * 单账户多角色时,使用该方法可过滤角色不存在的菜单 27 | * 28 | * @param roles 29 | * @param route 30 | * @returns {*} 31 | */ 32 | // eslint-disable-next-line 33 | function hasRole(roles, route) { 34 | if (route.meta && route.meta.roles) { 35 | return route.meta.roles.includes(roles.id) 36 | } else { 37 | return true 38 | } 39 | } 40 | 41 | function filterAsyncRouter (routerMap, roles) { 42 | const accessedRouters = routerMap.filter(route => { 43 | if (hasPermission(roles.permissionList, route)) { 44 | if (route.children && route.children.length) { 45 | route.children = filterAsyncRouter(route.children, roles) 46 | } 47 | return true 48 | } 49 | return false 50 | }) 51 | return accessedRouters 52 | } 53 | 54 | const permission = { 55 | state: { 56 | routers: constantRouterMap, 57 | addRouters: [] 58 | }, 59 | mutations: { 60 | SET_ROUTERS: (state, routers) => { 61 | state.addRouters = routers 62 | state.routers = constantRouterMap.concat(routers) 63 | } 64 | }, 65 | actions: { 66 | GenerateRoutes ({ commit }, data) { 67 | return new Promise(resolve => { 68 | const { roles } = data 69 | const routerMap = cloneDeep(asyncRouterMap) 70 | const accessedRouters = filterAsyncRouter(routerMap, roles) 71 | commit('SET_ROUTERS', accessedRouters) 72 | resolve() 73 | }) 74 | } 75 | } 76 | } 77 | 78 | export default permission 79 | -------------------------------------------------------------------------------- /web/src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const ACCESS_TOKEN = 'Access-Token' 2 | 3 | export const SIDEBAR_TYPE = 'sidebar_type' 4 | export const TOGGLE_MOBILE_TYPE = 'is_mobile' 5 | export const TOGGLE_NAV_THEME = 'nav_theme' 6 | export const TOGGLE_LAYOUT = 'layout' 7 | export const TOGGLE_FIXED_HEADER = 'fixed_header' 8 | export const TOGGLE_FIXED_SIDEBAR = 'fixed_sidebar' 9 | export const TOGGLE_CONTENT_WIDTH = 'content_width' 10 | export const TOGGLE_HIDE_HEADER = 'auto_hide_header' 11 | export const TOGGLE_COLOR = 'color' 12 | export const TOGGLE_WEAK = 'weak' 13 | export const TOGGLE_MULTI_TAB = 'multi_tab' 14 | export const APP_LANGUAGE = 'app_language' 15 | 16 | export const CONTENT_WIDTH_TYPE = { 17 | Fluid: 'Fluid', 18 | Fixed: 'Fixed' 19 | } 20 | 21 | export const NAV_THEME = { 22 | LIGHT: 'light', 23 | DARK: 'dark' 24 | } 25 | -------------------------------------------------------------------------------- /web/src/utils/axios.js: -------------------------------------------------------------------------------- 1 | const VueAxios = { 2 | vm: {}, 3 | // eslint-disable-next-line no-unused-vars 4 | install (Vue, instance) { 5 | if (this.installed) { 6 | return 7 | } 8 | this.installed = true 9 | 10 | if (!instance) { 11 | // eslint-disable-next-line no-console 12 | console.error('You have to install axios') 13 | return 14 | } 15 | 16 | Vue.axios = instance 17 | 18 | Object.defineProperties(Vue.prototype, { 19 | axios: { 20 | get: function get () { 21 | return instance 22 | } 23 | }, 24 | $http: { 25 | get: function get () { 26 | return instance 27 | } 28 | } 29 | }) 30 | } 31 | } 32 | 33 | export { 34 | VueAxios 35 | } 36 | -------------------------------------------------------------------------------- /web/src/utils/domUtil.js: -------------------------------------------------------------------------------- 1 | import config from '@/config/defaultSettings' 2 | 3 | export const setDocumentTitle = function (title) { 4 | document.title = title 5 | const ua = navigator.userAgent 6 | // eslint-disable-next-line 7 | const regex = /\bMicroMessenger\/([\d\.]+)/ 8 | if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) { 9 | const i = document.createElement('iframe') 10 | i.src = '/favicon.ico' 11 | i.style.display = 'none' 12 | i.onload = function () { 13 | setTimeout(function () { 14 | i.remove() 15 | }, 9) 16 | } 17 | document.body.appendChild(i) 18 | } 19 | } 20 | 21 | export const domTitle = config.title 22 | -------------------------------------------------------------------------------- /web/src/utils/filter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import moment from 'moment' 3 | import 'moment/locale/zh-cn' 4 | moment.locale('zh-cn') 5 | 6 | Vue.filter('NumberFormat', function (value) { 7 | if (!value) { 8 | return '0' 9 | } 10 | const intPartFormat = value.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') // 将整数部分逢三一断 11 | return intPartFormat 12 | }) 13 | 14 | Vue.filter('dayjs', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') { 15 | return moment(dataStr).format(pattern) 16 | }) 17 | 18 | Vue.filter('moment', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') { 19 | return moment(dataStr).format(pattern) 20 | }) 21 | -------------------------------------------------------------------------------- /web/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "@/store"; 3 | import storage from "store"; 4 | import notification from "ant-design-vue/es/notification"; 5 | import message from "ant-design-vue/es/message"; 6 | import { VueAxios } from "./axios"; 7 | import { ACCESS_TOKEN } from "@/store/mutation-types"; 8 | 9 | // 创建 axios 实例 10 | const request = axios.create({ 11 | // API 请求的默认前缀 12 | baseURL: process.env.VUE_APP_API_BASE_URL, 13 | timeout: 6000, // 请求超时时间 14 | }); 15 | 16 | // 异常拦截处理器 17 | const errorHandler = (error) => { 18 | if (error.response) { 19 | const data = error.response.data; 20 | // 从 localstorage 获取 token 21 | const token = storage.get(ACCESS_TOKEN); 22 | if (error.response.status === 400 && error.response.data.msg) { 23 | message.error(error.response.data.msg); 24 | } 25 | if (error.response.status === 403 && error.response.data.msg) { 26 | message.error(error.response.data.msg); 27 | // notification.error({ 28 | // message: "Forbidden", 29 | // description: data.message, 30 | // }); 31 | } 32 | if ( 33 | error.response.status === 401 && 34 | !(data.result && data.result.isLogin) 35 | ) { 36 | message.error(error.response.data.msg); 37 | // notification.error({ 38 | // message: "Unauthorized", 39 | // description: "Authorization verification failed", 40 | // }); 41 | if (token) { 42 | store.dispatch("Logout").then(() => { 43 | setTimeout(() => { 44 | window.location.reload(); 45 | }, 1500); 46 | }); 47 | } 48 | } 49 | } 50 | return Promise.reject(error); 51 | }; 52 | 53 | // request interceptor 54 | request.interceptors.request.use((config) => { 55 | const token = storage.get(ACCESS_TOKEN); 56 | // 如果 token 存在 57 | // 让每个请求携带自定义 token 请根据实际情况自行修改 58 | if (token) { 59 | config.headers["Access-Token"] = token; 60 | } 61 | return config; 62 | }, errorHandler); 63 | 64 | // response interceptor 65 | request.interceptors.response.use((response) => { 66 | if (response.status === 202 && response.data.msg) { 67 | message.warning(response.data.msg); 68 | } 69 | return response.data; 70 | }, errorHandler); 71 | 72 | const installer = { 73 | vm: {}, 74 | install(Vue) { 75 | Vue.use(VueAxios, request); 76 | }, 77 | }; 78 | 79 | export default request; 80 | 81 | export { installer as VueAxios, request as axios }; 82 | -------------------------------------------------------------------------------- /web/src/utils/routeConvert.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash.clonedeep' 2 | 3 | export function convertRoutes (nodes) { 4 | if (!nodes) return null 5 | 6 | nodes = cloneDeep(nodes) 7 | 8 | let queue = Array.isArray(nodes) ? nodes.concat() : [nodes] 9 | 10 | while (queue.length) { 11 | const levelSize = queue.length 12 | 13 | for (let i = 0; i < levelSize; i++) { 14 | const node = queue.shift() 15 | 16 | if (!node.children || !node.children.length) continue 17 | 18 | node.children.forEach(child => { 19 | // 转化相对路径 20 | if (child.path[0] !== '/' && !child.path.startsWith('http')) { 21 | child.path = node.path.replace(/(\w*)[/]*$/, `$1/${child.path}`) 22 | } 23 | }) 24 | 25 | queue = queue.concat(node.children) 26 | } 27 | } 28 | 29 | return nodes 30 | } 31 | -------------------------------------------------------------------------------- /web/src/utils/screenLog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export const printANSI = () => { 3 | // console.clear() 4 | console.log('[antd pro] created()') 5 | // ASCII - ANSI Shadow 6 | let text = ` 7 | █████╗ ███╗ ██╗████████╗██████╗ ██████╗ ██████╗ ██████╗ 8 | ██╔══██╗████╗ ██║╚══██╔══╝██╔══██╗ ██╔══██╗██╔══██╗██╔═══██╗ 9 | ███████║██╔██╗ ██║ ██║ ██║ ██║ ██████╔╝██████╔╝██║ ██║ 10 | ██╔══██║██║╚██╗██║ ██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║ ██║ 11 | ██║ ██║██║ ╚████║ ██║ ██████╔╝ ██║ ██║ ██║╚██████╔╝ 12 | ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ 13 | \t\t\t\t\tPublished ${APP_VERSION}-${GIT_HASH} @ antdv.com 14 | \t\t\t\t\tBuild date: ${BUILD_DATE}` 15 | console.log(`%c${text}`, 'color: #fc4d50') 16 | console.log('%c感谢使用 antd pro!', 'color: #000; font-size: 14px; font-family: Hiragino Sans GB,Microsoft YaHei,\\\\5FAE\\8F6F\\96C5\\9ED1,Droid Sans Fallback,Source Sans,Wenquanyi Micro Hei,WenQuanYi Micro Hei Mono,WenQuanYi Zen Hei,Apple LiGothic Medium,SimHei,ST Heiti,WenQuanYi Zen Hei Sharp,sans-serif;') 17 | console.log('%cThanks for using antd pro!', 'color: #fff; font-size: 14px; font-weight: 300; text-shadow:#000 1px 0 0,#000 0 1px 0,#000 -1px 0 0,#000 0 -1px 0;') 18 | } 19 | -------------------------------------------------------------------------------- /web/src/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | word-break: break-all; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | position: relative; 10 | max-height: @line * 1.5em; 11 | margin-right: -1em; 12 | padding-right: 1em; 13 | overflow: hidden; 14 | line-height: 1.5em; 15 | text-align: justify; 16 | &::before { 17 | position: absolute; 18 | right: 14px; 19 | bottom: 0; 20 | padding: 0 1px; 21 | background: @bg; 22 | content: '...'; 23 | } 24 | &::after { 25 | position: absolute; 26 | right: 14px; 27 | width: 1em; 28 | height: 1em; 29 | margin-top: 0.2em; 30 | background: white; 31 | content: ''; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &::before, 40 | &::after { 41 | display: table; 42 | content: ' '; 43 | } 44 | &::after { 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | visibility: hidden; 49 | } 50 | } -------------------------------------------------------------------------------- /web/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /web/src/views/cluster/components/Info.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /web/src/views/exception/403.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /web/src/views/exception/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /web/src/views/exception/500.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /web/src/views/user/RegisterResult.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 41 | 42 | 45 | --------------------------------------------------------------------------------