This commit is contained in:
whiteshader 2021-09-16 11:33:38 +08:00
commit 1c8632cdfd
862 changed files with 80961 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
######################################################################
# Build Tools
.gradle
/build/
**/node_modules
!gradle/wrapper/gradle-wrapper.jar
target/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
# IDE
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
nbproject/private/
build/*
nbbuild/
dist/
nbdist/
.nb-gradle/
######################################################################
# Others
*.log
*.xml.versionsBackup
*.swp
!*/build/*.java
!*/build/*.html
!*/build/*.xml

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018 RuoYi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

85
README.md Normal file
View File

@ -0,0 +1,85 @@
## 平台简介
若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 前端采用Vue、Element UI。
* 后端采用Spring Boot、Spring Security、Redis & Jwt。
* 权限认证使用Jwt支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
* 高效率开发,使用代码生成器可以一键生成前后端代码。
* 提供了单应用版本[RuoYi-Vue-fast](https://github.com/yangzongzhuan/RuoYi-Vue-fast)Oracle版本[RuoYi-Vue-Oracle](https://github.com/yangzongzhuan/RuoYi-Vue-Oracle),保持同步更新。
* 不分离版本,请移步[RuoYi](https://gitee.com/y_project/RuoYi),微服务版本,请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)
* 特别鸣谢:[element](https://github.com/ElemeFE/element)[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)[eladmin-web](https://github.com/elunez/eladmin-web)。
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)  
* 阿里云优惠券:[点我领取](https://www.aliyun.com/minisite/goods?userCode=brki8iof&share_source=copy_link),腾讯云优惠券:[点我领取](https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console)  
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 代码生成前后端代码的生成java、html、xml、sql支持CRUD下载 。
14. 系统接口根据业务代码自动生成相关的api接口文档。
15. 服务监控监视当前系统CPU、内存、磁盘、堆栈等相关信息。
16. 缓存监控:对系统的缓存信息查询,命令统计等。
17. 在线构建器拖动表单元素生成相应的HTML代码。
18. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。
## 在线体验
- admin/admin123
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
演示地址http://vue.ruoyi.vip
文档地址http://doc.ruoyi.vip
## 演示图
<table>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
</tr>
</table>
## 若依前后端分离交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) 点击按钮入群。

12
bin/clean.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 清理工程target生成路径。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean
pause

12
bin/package.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 打包Web工程生成war/jar包文件。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean package -Dmaven.test.skip=true
pause

14
bin/run.bat Normal file
View File

@ -0,0 +1,14 @@
@echo off
echo.
echo [信息] 使用Jar命令运行Web工程。
echo.
cd %~dp0
cd ../ruoyi-admin/target
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar %JAVA_OPTS% ruoyi-admin.jar
cd bin
pause

Binary file not shown.

266
pom.xml Normal file
View File

@ -0,0 +1,266 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.7.0</version>
<name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>若依管理系统</description>
<properties>
<ruoyi.version>3.7.0</ruoyi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<druid.version>1.2.6</druid.version>
<bitwalker.version>1.21</bitwalker.version>
<swagger.version>3.0.0</swagger.version>
<kaptcha.version>2.3.2</kaptcha.version>
<mybatis-spring-boot.version>2.1.4</mybatis-spring-boot.version>
<pagehelper.boot.version>1.3.1</pagehelper.boot.version>
<fastjson.version>1.2.76</fastjson.version>
<oshi.version>5.8.0</oshi.version>
<jna.version>5.8.0</jna.version>
<commons.io.version>2.11.0</commons.io.version>
<commons.fileupload.version>1.4</commons.fileupload.version>
<commons.collections.version>3.2.2</commons.collections.version>
<poi.version>4.1.2</poi.version>
<velocity.version>1.7</velocity.version>
<jwt.version>0.9.1</jwt.version>
</properties>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.13.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>${bitwalker.version}</version>
</dependency>
<!-- SpringBoot集成mybatis框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot.version}</version>
</dependency>
<!-- pagehelper 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.boot.version}</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>${oshi.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>${jna.version}</version>
</dependency>
<!-- Swagger3依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- io常用工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<!-- 文件上传工具类 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons.fileupload.version}</version>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>${velocity.version}</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- collections工具类 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons.collections.version}</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
<!-- 定时任务-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-quartz</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-generator</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 核心模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 系统模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${ruoyi.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>ruoyi-admin</module>
<module>ruoyi-framework</module>
<module>ruoyi-system</module>
<module>ruoyi-quartz</module>
<module>ruoyi-generator</module>
<module>ruoyi-common</module>
</modules>
<packaging>pom</packaging>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>

16
react-ui/.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

8
react-ui/.eslintignore Normal file
View File

@ -0,0 +1,8 @@
/lambda/
/scripts
/config
.history
public
dist
.umi
mock

11
react-ui/.eslintrc.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
page: true,
REACT_APP_ENV: true,
},
rules: {
'no-console': 0,
},
};

40
react-ui/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
# roadhog-api-doc ignore
/src/utils/request-temp.js
_roadhog-api-doc
# production
/dist
/.vscode
# misc
.DS_Store
npm-debug.log*
yarn-error.log
/coverage
.idea
yarn.lock
package-lock.json
*bak
.vscode
# visual studio code
.history
*.log
functions/*
.temp/**
# umi
.umi
.umi-production
# screenshot
screenshot
.firebase
.eslintcache
build

23
react-ui/.prettierignore Normal file
View File

@ -0,0 +1,23 @@
**/*.svg
package.json
.umi
.umi-production
/dist
.dockerignore
.DS_Store
.eslintignore
*.png
*.toml
docker
.editorconfig
Dockerfile*
.gitignore
.prettierignore
LICENSE
.eslintcache
*.lock
yarn-error.log
.history
CNAME
/build
/public

5
react-ui/.prettierrc.js Normal file
View File

@ -0,0 +1,5 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
};

5
react-ui/.stylelintrc.js Normal file
View File

@ -0,0 +1,5 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.stylelint,
};

201
react-ui/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

57
react-ui/README.md Normal file
View File

@ -0,0 +1,57 @@
# Ant Design Pro
This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
## Environment Prepare
Install `node_modules`:
```bash
npm install
```
or
```bash
yarn
```
## Provided Scripts
Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
Scripts provided in `package.json`. It's safe to modify or add additional script:
### Start project
```bash
npm start
```
### Build project
```bash
npm run build
```
### Check code style
```bash
npm run lint
```
You can also use script to auto fix some lint error:
```bash
npm run lint:fix
```
### Test code
```bash
npm test
```
## More
You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).

View File

@ -0,0 +1,15 @@
// https://umijs.org/config/
import { defineConfig } from 'umi';
export default defineConfig({
plugins: [
// https://github.com/zthxxx/react-dev-inspector
'react-dev-inspector/plugins/umi/react-inspector',
],
// https://github.com/zthxxx/react-dev-inspector#inspector-loader-props
inspectorConfig: {
exclude: [],
babelPlugins: [],
babelOptions: {},
},
});

534
react-ui/config/config.ts Normal file
View File

@ -0,0 +1,534 @@
// https://umijs.org/config/
import { defineConfig } from 'umi';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
const { REACT_APP_ENV } = process.env;
export default defineConfig({
hash: true,
antd: {},
dva: {
hmr: true,
},
history: {
type: 'browser',
},
locale: {
// default zh-CN
default: 'zh-CN',
antd: true,
// default true, when it is true, will use `navigator.language` overwrite default
baseNavigator: true,
},
dynamicImport: {
loading: '@/components/PageLoading/index',
},
targets: {
ie: 11,
},
// umi routes: https://umijs.org/docs/routing
routes: [
{
path: '/',
component: '../layouts/BlankLayout',
routes: [
{
path: '/user',
component: '../layouts/UserLayout',
routes: [
{
path: '/user/login',
name: 'login',
component: './User/login',
},
{
path: '/user',
redirect: '/user/login',
},
{
name: 'register-result',
icon: 'smile',
path: '/user/register-result',
component: './user/register-result',
},
{
name: 'register',
icon: 'smile',
path: '/user/register',
component: './user/register',
},
{
component: '404',
},
],
},
{
path: '/',
component: '../layouts/SecurityBasicLayout',
Routes: ['src/pages/Authorized'],
routes: [
{
path: '/',
redirect: '/dashboard/analysis',
},
{
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
routes: [
{
path: '/',
redirect: '/dashboard/analysis',
},
{
name: 'analysis',
icon: 'smile',
path: '/dashboard/analysis',
component: './dashboard/analysis',
},
{
name: 'monitor',
icon: 'smile',
path: '/dashboard/monitor',
component: './dashboard/monitor',
},
{
name: 'workplace',
icon: 'smile',
path: '/dashboard/workplace',
component: './dashboard/workplace',
},
],
},
{
name: 'test',
icon: 'BugOutlined',
path: '/test',
routes: [
{
path: '/',
redirect: '/test/mqtt',
},
{
name: 'mqtt',
icon: 'PartitionOutlined',
path: '/test/mqtt',
component: './mqtt',
},
{
name: 'userInfo',
icon: 'PartitionOutlined',
path: '/test/user',
component: 'system/user/index',
},
],
},
{
name: 'system',
icon: 'BugOutlined',
path: '/system',
routes: [
{
path: '/',
redirect: '/system/user',
},
{
name: 'user',
icon: 'PartitionOutlined',
path: '/system/user',
component: 'system/user/index',
},
{
name: 'menu',
icon: 'PartitionOutlined',
path: '/system/menu',
component: 'system/menu/index',
},
{
name: 'role',
icon: 'PartitionOutlined',
path: '/system/role',
component: 'system/role/index',
},
{
name: 'dept',
icon: 'PartitionOutlined',
path: '/system/dept',
component: 'system/dept/index',
},
{
name: 'post',
icon: 'PartitionOutlined',
path: '/system/post',
component: 'system/post/index',
},
{
name: 'dict',
icon: 'PartitionOutlined',
path: '/system/dict',
component: 'system/dict/index',
},
{
name: 'dictData',
icon: 'PartitionOutlined',
path: '/system/dict-data/index/:id?',
component: 'system/dictData/index',
},
{
name: 'config',
icon: 'PartitionOutlined',
path: '/system/config',
component: 'system/config/index',
},
{
name: 'notice',
icon: 'PartitionOutlined',
path: '/system/notice',
component: 'system/notice/index',
},
{
name: 'log',
icon: 'BugOutlined',
path: '/system/log/',
routes: [
{
path: '/',
redirect: '/system/log/operlog',
},
{
name: 'operlog',
icon: 'PartitionOutlined',
path: '/system/log/operlog',
component: 'monitor/operlog',
},
{
name: 'loginInfo',
icon: 'PartitionOutlined',
path: '/system/log/logininfor',
component: 'monitor/logininfor',
},
],
},
],
},
{
name: 'monitor',
icon: 'BugOutlined',
path: '/monitor',
routes: [
{
path: '/',
redirect: '/monitor/online',
},
{
name: 'onlineUser',
icon: 'PartitionOutlined',
path: '/monitor/online',
component: 'monitor/online',
},
{
name: 'job',
icon: 'PartitionOutlined',
path: '/monitor/job',
component: 'monitor/job',
},
{
name: 'joblog',
icon: 'PartitionOutlined',
path: '/monitor/job-log/index/:jobId?',
component: 'monitor/joblog',
},
{
name: 'druid',
icon: 'PartitionOutlined',
path: '/monitor/druid',
component: 'monitor/druid',
},
{
name: 'serverInfo',
icon: 'PartitionOutlined',
path: '/monitor/server',
component: 'monitor/server',
},
{
name: 'cacheInfo',
icon: 'PartitionOutlined',
path: '/monitor/cache',
component: 'monitor/cache',
},
],
},
{
name: 'tool',
icon: 'BugOutlined',
path: '/tool',
routes: [
{
path: '/',
redirect: '/tool/gen',
},
{
name: 'gen',
icon: 'PartitionOutlined',
path: '/tool/gen',
component: 'tool/gen/index',
},
{
name: 'genEdit',
icon: 'PartitionOutlined',
path: '/tool/gen/edit',
component: 'tool/gen/edit',
},
{
name: 'genImport',
icon: 'PartitionOutlined',
path: '/tool/gen/import',
component: 'tool/gen/import',
},
{
name: 'buildForm',
icon: 'PartitionOutlined',
path: '/tool/build',
component: 'tool/build',
},
{
name: 'swagger',
icon: 'PartitionOutlined',
path: '/tool/swagger',
component: 'tool/swagger',
},
],
},
{
path: '/form',
icon: 'form',
name: 'form',
routes: [
{
path: '/',
redirect: '/form/basic-form',
},
{
name: 'basic-form',
icon: 'smile',
path: '/form/basic-form',
component: './form/basic-form',
},
{
name: 'step-form',
icon: 'smile',
path: '/form/step-form',
component: './form/step-form',
},
{
name: 'advanced-form',
icon: 'smile',
path: '/form/advanced-form',
component: './form/advanced-form',
},
],
},
{
path: '/list',
icon: 'table',
name: 'list',
routes: [
{
path: '/list/search',
name: 'search-list',
component: './list/search',
routes: [
{
path: '/list/search',
redirect: '/list/search/articles',
},
{
name: 'articles',
icon: 'smile',
path: '/list/search/articles',
component: './list/search/articles',
},
{
name: 'projects',
icon: 'smile',
path: '/list/search/projects',
component: './list/search/projects',
},
{
name: 'applications',
icon: 'smile',
path: '/list/search/applications',
component: './list/search/applications',
},
],
},
{
path: '/',
redirect: '/list/table-list',
},
{
name: 'table-list',
icon: 'smile',
path: '/list/table-list',
component: './list/table-list',
},
{
name: 'basic-list',
icon: 'smile',
path: '/list/basic-list',
component: './list/basic-list',
},
{
name: 'card-list',
icon: 'smile',
path: '/list/card-list',
component: './list/card-list',
},
],
},
{
path: '/profile',
name: 'profile',
icon: 'profile',
routes: [
{
path: '/',
redirect: '/profile/basic',
},
{
name: 'basic',
icon: 'smile',
path: '/profile/basic',
component: './profile/basic',
},
{
name: 'advanced',
icon: 'smile',
path: '/profile/advanced',
component: './profile/advanced',
},
],
},
{
name: 'result',
icon: 'CheckCircleOutlined',
path: '/result',
routes: [
{
path: '/',
redirect: '/result/success',
},
{
name: 'success',
icon: 'smile',
path: '/result/success',
component: './result/success',
},
{
name: 'fail',
icon: 'smile',
path: '/result/fail',
component: './result/fail',
},
],
},
{
name: 'exception',
icon: 'warning',
path: '/exception',
routes: [
{
path: '/',
redirect: '/exception/403',
},
{
name: '403',
icon: 'smile',
path: '/exception/403',
component: './exception/403',
},
{
name: '404',
icon: 'smile',
path: '/exception/404',
component: './exception/404',
},
{
name: '500',
icon: 'smile',
path: '/exception/500',
component: './exception/500',
},
],
},
{
name: 'account',
icon: 'user',
path: '/account',
routes: [
{
path: '/',
redirect: '/account/center',
},
{
name: 'center',
icon: 'smile',
path: '/account/center',
component: './account/center',
},
{
name: 'settings',
icon: 'smile',
path: '/account/settings',
component: './account/settings',
},
],
},
{
name: 'editor',
icon: 'highlight',
path: '/editor',
routes: [
{
path: '/',
redirect: '/editor/flow',
},
{
name: 'flow',
icon: 'smile',
path: '/editor/flow',
component: './editor/flow',
},
{
name: 'mind',
icon: 'smile',
path: '/editor/mind',
component: './editor/mind',
},
{
name: 'koni',
icon: 'smile',
path: '/editor/koni',
component: './editor/koni',
},
],
},
{
component: '404',
},
],
},
],
},
],
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
theme: {
'primary-color': defaultSettings.primaryColor,
},
title: false,
ignoreMomentLocale: true,
proxy: proxy[REACT_APP_ENV || 'dev'],
manifest: {
basePath: '/',
},
esbuild: {},
});

View File

@ -0,0 +1,24 @@
import { Settings as ProSettings } from '@ant-design/pro-layout';
type DefaultSettings = Partial<ProSettings> & {
pwa: boolean;
};
const proSettings: DefaultSettings = {
navTheme: 'dark',
// 拂晓蓝
// primaryColor: '#1890ff',
primaryColor: '#13C2C2',
layout: 'side',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: true,
colorWeak: false,
title: 'Ant Design Pro',
pwa: false,
iconfontUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js',
};
export type { DefaultSettings };
export default proSettings;

34
react-ui/config/proxy.ts Normal file
View File

@ -0,0 +1,34 @@
/**
*
* The agent cannot take effect in the production environment
* so there is no configuration of the production environment
* For details, please see
* https://pro.ant.design/docs/deploy
*/
export default {
dev: {
'/api/': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
'/profile/avatar/' : {
target: 'http://localhost:8080',
changeOrigin: true,
}
},
test: {
'/api/': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^': '' },
},
},
pre: {
'/api/': {
target: 'your pre url',
changeOrigin: true,
pathRewrite: { '^': '' },
},
},
};

73
react-ui/config/routes.ts Normal file
View File

@ -0,0 +1,73 @@
export default [
{
path: '/',
component: '../layouts/BlankLayout',
routes: [
{
path: '/user',
component: '../layouts/UserLayout',
routes: [
{
name: 'login',
path: '/user/login',
component: './User/login',
},
],
},
{
path: '/',
component: '../layouts/SecurityLayout',
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
authority: ['admin', 'user'],
routes: [
{
path: '/',
redirect: '/welcome',
},
{
path: '/welcome',
name: 'welcome',
icon: 'smile',
component: './Welcome',
},
{
path: '/admin',
name: 'admin',
icon: 'crown',
component: './Admin',
authority: ['admin'],
routes: [
{
path: '/admin/sub-page',
name: 'sub-page',
icon: 'smile',
component: './Welcome',
authority: ['admin'],
},
],
},
{
name: 'list.table-list',
icon: 'table',
path: '/list',
component: './TableList',
},
{
component: './404',
},
],
},
{
component: './404',
},
],
},
],
},
{
component: './404',
},
];

9
react-ui/jest.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
testURL: 'http://localhost:8000',
testEnvironment: './tests/PuppeteerEnvironment',
verbose: false,
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
localStorage: null,
},
};

10
react-ui/jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@ -0,0 +1,171 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Request, Response } from 'express';
import { parse } from 'url';
import { TableListItem, TableListParams } from '@/pages/TableList/data';
// mock tableListDataSource
const genList = (current: number, pageSize: number) => {
const tableListDataSource: TableListItem[] = [];
for (let i = 0; i < pageSize; i += 1) {
const index = (current - 1) * 10 + i;
tableListDataSource.push({
key: index,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${index}`,
owner: '曲丽丽',
desc: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
});
}
tableListDataSource.reverse();
return tableListDataSource;
};
let tableListDataSource = genList(1, 100);
function getRule(req: Request, res: Response, u: string) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
const params = (parse(realUrl, true).query as unknown) as TableListParams;
let dataSource = [...tableListDataSource].slice(
((current as number) - 1) * (pageSize as number),
(current as number) * (pageSize as number),
);
const sorter = JSON.parse(params.sorter as any);
if (sorter) {
dataSource = dataSource.sort((prev, next) => {
let sortNumber = 0;
Object.keys(sorter).forEach((key) => {
if (sorter[key] === 'descend') {
if (prev[key] - next[key] > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (prev[key] - next[key] > 0) {
sortNumber += 1;
} else {
sortNumber += -1;
}
});
return sortNumber;
});
}
if (params.filter) {
const filter = JSON.parse(params.filter as any) as {
[key: string]: string[];
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return Object.keys(filter).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}
if (params.name) {
dataSource = dataSource.filter((data) => data.name.includes(params.name || ''));
}
const result = {
data: dataSource,
total: tableListDataSource.length,
success: true,
pageSize,
current: parseInt(`${params.currentPage}`, 10) || 1,
};
return res.json(result);
}
function postRule(req: Request, res: Response, u: string, b: Request) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const body = (b && b.body) || req.body;
const { method, name, desc, key } = body;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {
const i = Math.ceil(Math.random() * 10000);
const newRule = {
key: tableListDataSource.length,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name,
owner: '曲丽丽',
desc,
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 2,
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
};
tableListDataSource.unshift(newRule);
return res.json(newRule);
})();
return;
case 'update':
(() => {
let newRule = {};
tableListDataSource = tableListDataSource.map((item) => {
if (item.key === key) {
newRule = { ...item, desc, name };
return { ...item, desc, name };
}
return item;
});
return res.json(newRule);
})();
return;
default:
break;
}
const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};
res.json(result);
}
export default {
'GET /api/rule': getRule,
'POST /api/rule': postRule,
};

105
react-ui/mock/notices.ts Normal file
View File

@ -0,0 +1,105 @@
import { Request, Response } from 'express';
const getNotices = (req: Request, res: Response) => {
res.json([
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: 'notification',
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: 'notification',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: 'notification',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: 'event',
},
{
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: 'event',
},
{
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: 'event',
},
{
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: 'event',
},
]);
};
export default {
'GET /api/notices': getNotices,
};

5
react-ui/mock/route.ts Normal file
View File

@ -0,0 +1,5 @@
export default {
'/api/auth_routes': {
'/form/advanced-form': { authority: ['admin', 'user'] },
},
};

165
react-ui/mock/user.ts Normal file
View File

@ -0,0 +1,165 @@
import { Request, Response } from 'express';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
async function getFakeCaptcha(req: Request, res: Response) {
await waitTime(2000);
return res.json('captcha-xxx');
}
// 代码中会兼容本地 service mock 以及部署站点的静态数据
export default {
// 支持值为 Object 和 Array
'GET /api/getInfo': {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userId: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁集团某某某事业群某某平台部某某技术部UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
// GET POST 可省略
'GET /api/users': [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
],
'POST /api/login/account': async (req: Request, res: Response) => {
const { password, userName, type } = req.body;
await waitTime(2000);
if (password === 'admin123' && userName === 'admin') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
return;
}
if (password === 'ant' && userName === 'user') {
res.send({
status: 'ok',
type,
currentAuthority: 'user',
});
return;
}
if (type === 'mobile') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
return;
}
res.send({
status: 'error',
type,
currentAuthority: 'guest',
});
},
'POST /api/register': (req: Request, res: Response) => {
res.send({ status: 'ok', currentAuthority: 'user' });
},
'GET /api/500': (req: Request, res: Response) => {
res.status(500).send({
timestamp: 1513932555104,
status: 500,
error: 'error',
message: 'error',
path: '/base/category/list',
});
},
'GET /api/404': (req: Request, res: Response) => {
res.status(404).send({
timestamp: 1513932643431,
status: 404,
error: 'Not Found',
message: 'No message available',
path: '/base/category/list/2121212',
});
},
'GET /api/403': (req: Request, res: Response) => {
res.status(403).send({
timestamp: 1513932555104,
status: 403,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/401': (req: Request, res: Response) => {
res.status(401).send({
timestamp: 1513932555104,
status: 401,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/login/captcha': getFakeCaptcha,
};

129
react-ui/package.json Normal file
View File

@ -0,0 +1,129 @@
{
"name": "ant-design-pro",
"version": "4.5.0",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
"build": "umi build",
"deploy": "npm run site && npm run gh-pages",
"dev": "npm run start:dev",
"fetch:blocks": "pro fetch-blocks && npm run prettier",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "umi g tmp",
"lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "prettier --check \"src/**/*\" --end-of-line auto",
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
"precommit": "lint-staged",
"prettier": "prettier -c --write \"src/**/*\"",
"start": "cross-env UMI_ENV=dev umi dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev umi dev",
"start:no-ui": "cross-env UMI_UI=none UMI_ENV=dev umi dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev umi dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev umi dev",
"pretest": "node ./tests/beforeTest",
"test": "umi test",
"test:all": "node ./tests/run-tests.js",
"test:component": "umi test ./src/components",
"tsc": "tsc --noEmit"
},
"lint-staged": {
"**/*.less": "stylelint --syntax less",
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"dependencies": {
"@ant-design/icons": "^4.6.0",
"@ant-design/pro-descriptions": "^1.9.6",
"@ant-design/pro-form": "^1.35.0",
"@ant-design/pro-layout": "^6.24.0",
"@ant-design/pro-table": "^2.49.0",
"@antv/data-set": "^0.11.0",
"@antv/l7": "^2.5.8",
"@antv/l7-maps": "^2.5.8",
"@antv/l7-react": "^2.3.3",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.isequal": "^4.5.5",
"@umijs/route-utils": "^1.0.33",
"antd": "^4.16.0",
"bizcharts": "^3.5.3-beta.0",
"bizcharts-plugin-slider": "^2.1.1-beta.1",
"classnames": "^2.2.6",
"dva": "^2.4.0",
"gg-editor": "^2.0.2",
"lodash": "^4.17.11",
"lodash-decorators": "^6.0.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"mockjs": "^1.0.1-beta3",
"moment": "^2.25.3",
"mqtt": "^4.2.8",
"numeral": "^2.0.6",
"nzh": "^1.0.3",
"omit.js": "^2.0.2",
"prop-types": "^15.5.10",
"react": "^17.0.0",
"react-dev-inspector": "^1.6.0",
"react-dom": "^17.0.0",
"react-fittext": "^1.0.0",
"react-helmet-async": "^1.0.4",
"react-router": "^5.2.0",
"umi": "^3.5.16",
"umi-request": "^1.3.9"
},
"devDependencies": {
"@ant-design/pro-cli": "^2.1.5",
"@types/classnames": "^2.3.1",
"@types/express": "^4.17.0",
"@types/history": "^4.7.2",
"@types/jest": "^26.0.0",
"@types/lodash": "^4.14.144",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.0",
"@umijs/fabric": "^2.5.1",
"@umijs/plugin-blocks": "^2.0.5",
"@umijs/plugin-esbuild": "^1.0.1",
"@umijs/preset-ant-design-pro": "^1.2.0",
"@umijs/preset-react": "^1.4.8",
"@umijs/yorkie": "^2.0.3",
"carlo": "^0.9.46",
"chalk": "^4.0.0",
"cross-env": "^7.0.0",
"cross-port-killer": "^1.1.1",
"detect-installer": "^1.0.1",
"enzyme": "^3.11.0",
"eslint": "^7.1.0",
"express": "^4.17.1",
"gh-pages": "^3.0.0",
"jsdom-global": "^3.0.2",
"lint-staged": "^10.0.0",
"mockjs": "^1.0.1-beta3",
"prettier": "^2.0.1",
"puppeteer-core": "^7.0.1",
"stylelint": "^13.0.0",
"typescript": "^4.3.5"
},
"engines": {
"node": ">=10.0.0"
},
"checkFiles": [
"src/**/*.js*",
"src/**/*.ts*",
"src/**/*.less",
"config/**/*.js*",
"scripts/**/*.js"
]
}

1
react-ui/public/CNAME Normal file
View File

@ -0,0 +1 @@
preview.pro.ant.design

BIN
react-ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
react-ui/public/home_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,5 @@
<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="#070707" d="m6.717392,13.773912l5.6,0c2.8,0 4.7,1.9 4.7,4.7c0,2.8 -2,4.7 -4.9,4.7l-2.5,0l0,4.3l-2.9,0l0,-13.7zm2.9,2.2l0,4.9l1.9,0c1.6,0 2.6,-0.9 2.6,-2.4c0,-1.6 -0.9,-2.4 -2.6,-2.4l-1.9,0l0,-0.1zm8.9,11.5l2.7,0l0,-5.7c0,-1.4 0.8,-2.3 2.2,-2.3c0.4,0 0.8,0.1 1,0.2l0,-2.4c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-1.2,0 -2.1,0.7 -2.4,2l-0.1,0l0,-1.9l-2.7,0l0,10.2l0.1,0zm11.7,0.1c-3.1,0 -5,-2 -5,-5.3c0,-3.3 2,-5.3 5,-5.3s5,2 5,5.3c0,3.4 -1.9,5.3 -5,5.3zm0,-2.1c1.4,0 2.2,-1.1 2.2,-3.2c0,-2 -0.8,-3.2 -2.2,-3.2c-1.4,0 -2.2,1.2 -2.2,3.2c0,2.1 0.8,3.2 2.2,3.2z" class="st0" id="Ant-Design-Pro"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 677 B

22
react-ui/src/app.tsx Normal file
View File

@ -0,0 +1,22 @@
// import React from 'react';
// import RunTimeLayoutConfig from '@ant-design/pro-layout';
// export const layout: RunTimeLayoutConfig = ({ initialState }) => {
// return {
// menu: {
// // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
// params: {
// userId: initialState?.currentUser?.userid,
// },
// request: async (params, defaultMenuData) => {
// // initialState.currentUser 中包含了所有用户信息
// const menuData = await fetchMenuData();
// return menuData;
// },
// },
// };
// };
export async function getInitialState() {
console.log('get init state')
return {};
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Result } from 'antd';
import check from './CheckPermissions';
import type { IAuthorityType } from './CheckPermissions';
import type AuthorizedRoute from './AuthorizedRoute';
import type Secured from './Secured';
import type { CurrentUser } from '@/models/user';
import { FormattedMessage } from 'umi';
type AuthorizedProps = {
authority: IAuthorityType;
noMatch?: React.ReactNode;
currentUser?: CurrentUser;
};
type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
Secured: typeof Secured;
check: typeof check;
AuthorizedRoute: typeof AuthorizedRoute;
};
const Authorized: React.FunctionComponent<AuthorizedProps> = ({
children,
authority,
currentUser,
noMatch = (
<Result
status="403"
title="403"
subTitle={
<FormattedMessage
id="pages.authorized.forbiden"
defaultMessage="Sorry, you are not authorized to access this page."
/>
}
/>
),
}) => {
const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
const dom = check(currentUser, authority, childrenRender, noMatch);
return <>{dom}</>;
};
export default Authorized as IAuthorizedType;

View File

@ -0,0 +1,33 @@
import { Redirect, Route } from 'umi';
import React from 'react';
import Authorized from './Authorized';
import type { IAuthorityType } from './CheckPermissions';
type AuthorizedRouteProps = {
currentAuthority: string;
component: React.ComponentClass<any, any>;
render: (props: any) => React.ReactNode;
redirectPath: string;
authority: IAuthorityType;
};
const AuthorizedRoute: React.SFC<AuthorizedRouteProps> = ({
component: Component,
render,
authority,
redirectPath,
...rest
}) => (
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route
{...rest}
render={(props: any) => (Component ? <Component {...props} /> : render(props))}
/>
</Authorized>
);
export default AuthorizedRoute;

View File

@ -0,0 +1,98 @@
import React from 'react';
// import { CURRENT } from './renderAuthorize';
// eslint-disable-next-line import/no-cycle
import PromiseRender from './PromiseRender';
import type { CurrentUser } from '@/models/user';
export type IAuthorityType =
| undefined
| string
| string[]
| Promise<boolean>
| ((currentAuthority: string | string[]) => IAuthorityType);
/**
* Common check permissions method
*
* @param { | Role judgment } authority
* @param { | Your permission description } currentAuthority
* @param { | Passing components } target
* @param { | no pass components } Exception
*/
const checkPermissions = <T, K>(
authority: IAuthorityType,
currentAuthority: string | string[],
target: T,
Exception: K,
): T | K | React.ReactNode => {
// 没有判定权限.默认查看所有
// Retirement authority, return target;
if (!authority) {
return target;
}
// 数组处理
if (Array.isArray(authority)) {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some((item) => authority.includes(item))) {
return target;
}
} else if (authority.includes(currentAuthority)) {
return target;
}
return Exception;
}
// string 处理
if (typeof authority === 'string') {
if (Array.isArray(currentAuthority)) {
if (
currentAuthority.some((item) => {
if (authority === item) {
return true;
}
if (item === '*:*:*') {
return true;
}
return false;
})
) {
return target;
}
} else if (authority === currentAuthority) {
return target;
}
if (authority === '!*') {
return Exception;
}
return Exception;
}
// Promise 处理
if (authority instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
}
// Function 处理
if (typeof authority === 'function') {
const bool = authority(currentAuthority);
// 函数执行后返回值是 Promise
if (bool instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
}
if (bool) {
return target;
}
return Exception;
}
throw new Error('unsupported parameters');
};
export { checkPermissions };
function check<T, K>(
user: CurrentUser | undefined,
authority: IAuthorityType,
target: T,
Exception: K,
): T | K | React.ReactNode {
return checkPermissions<T, K>(authority, user?.permissions || [], target, Exception);
}
export default check;

View File

@ -0,0 +1,96 @@
import React from 'react';
import { Spin } from 'antd';
import isEqual from 'lodash/isEqual';
import { isComponentClass } from './Secured';
// eslint-disable-next-line import/no-cycle
type PromiseRenderProps<T, K> = {
ok: T;
error: K;
promise: Promise<boolean>;
};
type PromiseRenderState = {
component: React.ComponentClass | React.FunctionComponent;
};
export default class PromiseRender<T, K> extends React.Component<
PromiseRenderProps<T, K>,
PromiseRenderState
> {
state: PromiseRenderState = {
component: () => null,
};
componentDidMount(): void {
this.setRenderComponent(this.props);
}
shouldComponentUpdate = (
nextProps: PromiseRenderProps<T, K>,
nextState: PromiseRenderState,
): boolean => {
const { component } = this.state;
if (!isEqual(nextProps, this.props)) {
this.setRenderComponent(nextProps);
}
if (nextState.component !== component) return true;
return false;
};
// set render Component : ok or error
setRenderComponent(props: PromiseRenderProps<T, K>): void {
const ok = this.checkIsInstantiation(props.ok);
const error = this.checkIsInstantiation(props.error);
props.promise
.then(() => {
this.setState({
component: ok,
});
return true;
})
.catch(() => {
this.setState({
component: error,
});
});
}
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
checkIsInstantiation = (
target: React.ReactNode | React.ComponentClass,
): React.FunctionComponent => {
if (isComponentClass(target)) {
const Target = target as React.ComponentClass;
return (props: any) => <Target {...props} />;
}
if (React.isValidElement(target)) {
return (props: any) => React.cloneElement(target, props);
}
return () => target as React.ReactNode & null;
};
render() {
const { component: Component } = this.state;
const { ok, error, promise, ...rest } = this.props;
return Component ? (
<Component {...rest} />
) : (
<div
style={{
width: '100%',
height: '100%',
margin: 'auto',
paddingTop: 50,
textAlign: 'center',
}}
>
<Spin size="large" />
</div>
);
}
}

View File

@ -0,0 +1,57 @@
import React from 'react';
import CheckPermissions from './CheckPermissions';
/** 默认不能访问任何页面 default is "NULL" */
const Exception403 = () => 403;
export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
if (!component) return false;
const proto = Object.getPrototypeOf(component);
if (proto === React.Component || proto === Function.prototype) return true;
return isComponentClass(proto);
};
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
if (isComponentClass(target)) {
const Target = target as React.ComponentClass;
return (props: any) => <Target {...props} />;
}
if (React.isValidElement(target)) {
return (props: any) => React.cloneElement(target, props);
}
return () => target;
};
/**
* 访 view authority string, () => boolean | Promise e.g. 'user' user 访
* e.g. 'user,admin' user admin 访 e.g. ()=>boolean true能访问,false不能访问 e.g. Promise then 访
* catch不能访问 e.g. authority support incoming string, () => boolean | Promise e.g. 'user' only user
* user can access e.g. 'user, admin' user and admin can access e.g. () => boolean true to be able
* to visit, return false can not be accessed e.g. Promise then can not access the visit to catch
*
* @param {string | function | Promise} authority
* @param {ReactNode} error
*/
const authorize = (authority: string, error?: React.ReactNode) => {
/**
* Conversion into a class staticContext造成报错 String parameters can cause staticContext
* not found error
*/
let classError: boolean | React.FunctionComponent = false;
if (error) {
classError = (() => error) as React.FunctionComponent;
}
if (!authority) {
throw new Error('authority is required');
}
return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
const component = CheckPermissions(authority, target, classError || Exception403);
return checkIsInstantiation(component);
};
};
export default authorize;

View File

@ -0,0 +1,11 @@
import Authorized from './Authorized';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';
Authorized.Secured = Secured;
Authorized.check = check;
const RenderAuthorize = renderAuthorize(Authorized);
export default RenderAuthorize;

View File

@ -0,0 +1,31 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/no-mutable-exports */
let CURRENT: string | string[] = 'NULL';
type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
/**
* Use authority or getAuthority
*
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
currentAuthority: CurrentAuthorityType,
): T => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority as string[];
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default <T>(Authorized: T) => renderAuthorize<T>(Authorized);

View File

@ -0,0 +1,92 @@
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
import { Avatar, Menu, Spin } from 'antd';
import React from 'react';
import type { ConnectProps } from 'umi';
import { history, connect } from 'umi';
import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
export type GlobalHeaderRightProps = {
currentUser?: CurrentUser;
menu?: boolean;
} & Partial<ConnectProps>;
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
onMenuClick = (event: {
key: React.Key;
keyPath: React.Key[];
item: React.ReactInstance;
domEvent: React.MouseEvent<HTMLElement>;
}) => {
const { key } = event;
if (key === 'logout') {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'login/logout',
});
}
return;
}
history.push(`/account/${key}`);
};
render(): React.ReactNode {
const {
currentUser = {
avatar: '',
userName: '',
},
menu,
} = this.props;
const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
{menu && (
<Menu.Item key="center">
<UserOutlined />
</Menu.Item>
)}
{menu && (
<Menu.Item key="settings">
<SettingOutlined />
</Menu.Item>
)}
{menu && <Menu.Divider />}
<Menu.Item key="logout">
<LogoutOutlined />
退
</Menu.Item>
</Menu>
);
return currentUser && currentUser.userName ? (
<HeaderDropdown overlay={menuHeaderDropdown}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="default" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
<span className={`${styles.name} anticon`}>{currentUser.userName}</span>
</span>
</HeaderDropdown>
) : (
<span className={`${styles.action} ${styles.account}`}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
}
}
export default connect(({ user }: ConnectState) => ({
currentUser: user.currentUser,
}))(AvatarDropdown);

View File

@ -0,0 +1,168 @@
import React, { Component } from 'react';
import type { ConnectProps } from 'umi';
import { connect } from 'umi';
import { Tag, message } from 'antd';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import type { NoticeItem } from '@/models/global';
import type { CurrentUser } from '@/models/user';
import type { ConnectState } from '@/models/connect';
import NoticeIcon from '../NoticeIcon';
import styles from './index.less';
export type GlobalHeaderRightProps = {
notices?: NoticeItem[];
currentUser?: CurrentUser;
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onNoticeClear?: (tabName?: string) => void;
} & Partial<ConnectProps>;
class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
componentDidMount() {
// const { dispatch } = this.props;
// if (dispatch) {
// dispatch({
// type: 'global/fetchNotices',
// });
// }
}
changeReadState = (clickedItem: NoticeItem): void => {
const { id } = clickedItem;
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'global/changeNoticeReadState',
payload: id,
});
}
};
handleNoticeClear = (title: string, key: string) => {
const { dispatch } = this.props;
message.success(`${'清空了'} ${title}`);
if (dispatch) {
dispatch({
type: 'global/clearNotices',
payload: key,
});
}
};
getNoticeData = (): Record<string, NoticeItem[]> => {
const { notices = [] } = this.props;
if (!notices || notices.length === 0 || !Array.isArray(notices)) {
return {};
}
const newNotices = notices.map((notice) => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime as string).fromNow();
}
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = {
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
}[newNotice.status];
newNotice.extra = (
<Tag
color={color}
style={{
marginRight: 0,
}}
>
{newNotice.extra}
</Tag>
);
}
return newNotice;
});
return groupBy(newNotices, 'type');
};
getUnreadData = (noticeData: Record<string, NoticeItem[]>) => {
const unreadMsg: Record<string, number> = {};
Object.keys(noticeData).forEach((key) => {
const value = noticeData[key];
if (!unreadMsg[key]) {
unreadMsg[key] = 0;
}
if (Array.isArray(value)) {
unreadMsg[key] = value.filter((item) => !item.read).length;
}
});
return unreadMsg;
};
render() {
const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
const noticeData = this.getNoticeData();
const unreadMsg = this.getUnreadData(noticeData);
return (
<NoticeIcon
className={styles.action}
count={currentUser && currentUser.unreadCount}
onItemClick={(item) => {
this.changeReadState(item as NoticeItem);
}}
loading={fetchingNotices}
clearText="清空"
viewMoreText="查看更多"
onClear={this.handleNoticeClear}
onPopupVisibleChange={onNoticeVisibleChange}
onViewMore={() => message.info('Click on view more')}
clearClose
>
<NoticeIcon.Tab
tabKey="notification"
count={unreadMsg.notification}
list={noticeData.notification}
title="通知"
emptyText="你已查看所有通知"
showViewMore
/>
<NoticeIcon.Tab
tabKey="message"
count={unreadMsg.message}
list={noticeData.message}
title="消息"
emptyText="您已读完所有消息"
showViewMore
/>
<NoticeIcon.Tab
tabKey="event"
title="待办"
emptyText="你已完成所有待办"
count={unreadMsg.event}
list={noticeData.event}
showViewMore
/>
</NoticeIcon>
);
}
}
export default connect(({ user, global, loading }: ConnectState) => ({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
fetchingNotices: loading.effects['global/fetchNotices'],
notices: global.notices,
}))(GlobalHeaderRight);

View File

@ -0,0 +1,86 @@
import { Tooltip, Tag } from 'antd';
import type { Settings as ProSettings } from '@ant-design/pro-layout';
import { QuestionCircleOutlined } from '@ant-design/icons';
import React from 'react';
import type { ConnectProps } from 'umi';
import { connect, SelectLang } from 'umi';
import type { ConnectState } from '@/models/connect';
import Avatar from './AvatarDropdown';
import HeaderSearch from '../HeaderSearch';
import styles from './index.less';
import NoticeIconView from './NoticeIconView';
export type GlobalHeaderRightProps = {
theme?: ProSettings['navTheme'] | 'realDark';
} & Partial<ConnectProps> &
Partial<ProSettings>;
const ENVTagColor = {
dev: 'orange',
test: 'green',
pre: '#87d068',
};
const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = (props) => {
const { theme, layout } = props;
let className = styles.right;
if (theme === 'dark' && layout === 'top') {
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
<HeaderSearch
className={`${styles.action} ${styles.search}`}
placeholder="站内搜索"
defaultValue="umi ui"
options={[
{
label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>,
value: 'umi ui',
},
{
label: <a href="next.ant.design">Ant Design</a>,
value: 'Ant Design',
},
{
label: <a href="https://protable.ant.design/">Pro Table</a>,
value: 'Pro Table',
},
{
label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
value: 'Pro Layout',
},
]} // onSearch={value => {
// //console.log('input', value);
// }}
/>
<Tooltip title="使用文档">
<a
style={{
color: 'inherit',
}}
target="_blank"
href="https://pro.ant.design/docs/getting-started"
rel="noopener noreferrer"
className={styles.action}
>
<QuestionCircleOutlined />
</a>
</Tooltip>
<NoticeIconView />
<Avatar menu />
{REACT_APP_ENV && (
<span>
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
</span>
)}
<SelectLang className={styles.action} />
</div>
);
};
export default connect(({ settings }: ConnectState) => ({
theme: settings.navTheme,
layout: settings.layout,
}))(GlobalHeaderRight);

View File

@ -0,0 +1,82 @@
@import '~antd/es/style/themes/default.less';
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.right {
display: flex;
float: right;
height: 48px;
margin-left: auto;
overflow: hidden;
.action {
display: flex;
align-items: center;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
> span {
vertical-align: middle;
}
&:hover {
background: @pro-header-hover-bg;
}
&:global(.opened) {
background: @pro-header-hover-bg;
}
}
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.account {
.avatar {
margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
margin-right: 8px;
color: @primary-color;
vertical-align: top;
background: rgba(255, 255, 255, 0.85);
}
}
}
.dark {
.action {
color: rgba(255, 255, 255, 0.85);
> span {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&:global(.opened) {
background: @primary-color;
}
}
}
:global(.ant-pro-global-header) {
.dark {
.action {
color: @text-color;
> span {
color: @text-color;
}
&:hover {
color: rgba(255, 255, 255, 0.85);
> span {
color: rgba(255, 255, 255, 0.85);
}
}
}
}
}

View File

@ -0,0 +1,16 @@
@import '~antd/es/style/themes/default.less';
.container > * {
background-color: @popover-bg;
border-radius: 4px;
box-shadow: @shadow-1-down;
}
@media screen and (max-width: @screen-xs) {
.container {
width: 100% !important;
}
.container > * {
border-radius: 0 !important;
}
}

View File

@ -0,0 +1,17 @@
import type { DropDownProps } from 'antd/es/dropdown';
import { Dropdown } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
export type HeaderDropdownProps = {
overlayClassName?: string;
overlay: React.ReactNode | (() => React.ReactNode) | any;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
);
export default HeaderDropdown;

View File

@ -0,0 +1,30 @@
@import '~antd/es/style/themes/default.less';
.headerSearch {
.input {
width: 0;
min-width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
transition: width 0.3s, margin-left 0.3s;
:global(.ant-select-selection) {
background: transparent;
}
input {
padding-right: 0;
padding-left: 0;
border: 0;
box-shadow: none !important;
}
&,
&:hover,
&:focus {
border-bottom: 1px solid @border-color-base;
}
&.show {
width: 210px;
margin-left: 8px;
}
}
}

View File

@ -0,0 +1,105 @@
import { SearchOutlined } from '@ant-design/icons';
import { AutoComplete, Input } from 'antd';
import useMergedState from 'rc-util/es/hooks/useMergedState';
import type { AutoCompleteProps } from 'antd/es/auto-complete';
import React, { useRef } from 'react';
import classNames from 'classnames';
import styles from './index.less';
export type HeaderSearchProps = {
onSearch?: (value?: string) => void;
onChange?: (value?: string) => void;
onVisibleChange?: (b: boolean) => void;
className?: string;
placeholder?: string;
options: AutoCompleteProps['options'];
defaultOpen?: boolean;
open?: boolean;
defaultValue?: string;
value?: string;
};
const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
const {
className,
defaultValue,
onVisibleChange,
placeholder,
open,
defaultOpen,
...restProps
} = props;
const inputRef = useRef<Input | null>(null);
const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
value: props.value,
onChange: props.onChange,
});
const [searchMode, setSearchMode] = useMergedState(defaultOpen ?? false, {
value: props.open,
onChange: onVisibleChange,
});
const inputClass = classNames(styles.input, {
[styles.show]: searchMode,
});
return (
<div
className={classNames(className, styles.headerSearch)}
onClick={() => {
setSearchMode(true);
if (searchMode && inputRef.current) {
inputRef.current.focus();
}
}}
onTransitionEnd={({ propertyName }) => {
if (propertyName === 'width' && !searchMode) {
if (onVisibleChange) {
onVisibleChange(searchMode);
}
}
}}
>
<SearchOutlined
key="Icon"
style={{
cursor: 'pointer',
}}
/>
<AutoComplete
key="AutoComplete"
className={inputClass}
value={value}
style={{
height: 28,
marginTop: -6,
}}
options={restProps.options}
onChange={setValue}
>
<Input
ref={inputRef}
defaultValue={defaultValue}
aria-label={placeholder}
placeholder={placeholder}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (restProps.onSearch) {
restProps.onSearch(value);
}
}
}}
onBlur={() => {
setSearchMode(false);
}}
/>
</AutoComplete>
</div>
);
};
export default HeaderSearch;

View File

@ -0,0 +1,77 @@
import * as React from 'react';
import { message } from 'antd';
import { injectIntl } from 'react-intl';
import CopyableIcon from './CopyableIcon';
import type { ThemeType } from './index';
import type { CategoriesKeys } from './fields';
interface CategoryProps {
title: CategoriesKeys;
icons: string[];
theme: ThemeType;
newIcons: string[];
intl: any;
onSelect: (name: string) => any;
}
interface CategoryState {
justCopied: string | null;
}
class Category extends React.Component<CategoryProps, CategoryState> {
copyId?: number;
state = {
justCopied: null,
};
componentWillUnmount() {
window.clearTimeout(this.copyId);
}
onSelect = (name: string) => {
const { onSelect } = this.props;
if (onSelect) {
onSelect(name);
}
message.success(
<span>
<code className="copied-code">{name}</code> selected 🎉
</span>,
);
this.setState({ justCopied: name }, () => {
this.copyId = window.setTimeout(() => {
this.setState({ justCopied: null });
}, 2000);
});
};
render() {
const {
icons,
title,
newIcons,
theme,
intl: { messages },
} = this.props;
const items = icons.map((name) => (
<CopyableIcon
key={name}
name={name}
theme={theme}
isNew={newIcons.indexOf(name) >= 0}
justCopied={this.state.justCopied}
onSelect={this.onSelect}
/>
));
return (
<div>
<h3>{messages[`app.docs.components.icon.category.${title}`]}</h3>
<ul className="anticons-list">{items}</ul>
</div>
);
}
}
export default injectIntl(Category);

View File

@ -0,0 +1,45 @@
import * as React from 'react';
import { Badge } from 'antd';
import classNames from 'classnames';
import * as AntdIcons from '@ant-design/icons';
import type { ThemeType } from './index';
const allIcons: Record<string, any> = AntdIcons;
export interface CopyableIconProps {
name: string;
isNew: boolean;
theme: ThemeType;
justCopied: string | null;
onSelect: (name: string) => any;
}
const CopyableIcon: React.FC<CopyableIconProps> = ({
name,
isNew,
justCopied,
theme,
onSelect,
}) => {
const className = classNames({
copied: justCopied === name,
[theme]: !!theme,
});
return (
<li
className={className}
onClick={() => {
if (onSelect) {
onSelect(name);
}
}}
>
{React.createElement(allIcons[name])}
<span className="anticon-class">
<Badge dot={isNew}>{name}</Badge>
</span>
</li>
);
};
export default CopyableIcon;

View File

@ -0,0 +1,248 @@
import React, { Component } from 'react';
import { Upload, Tooltip, Popover, Modal, Progress, message, Spin, Result } from 'antd';
import { injectIntl } from 'react-intl';
import * as AntdIcons from '@ant-design/icons';
const allIcons: {
[key: string]: any;
} = AntdIcons;
const { Dragger } = Upload;
interface AntdIconClassifier {
load: Function;
predict: Function;
}
declare global {
interface Window {
antdIconClassifier: AntdIconClassifier;
}
}
interface PicSearcherProps {
intl: any;
}
interface PicSearcherState {
loading: boolean;
modalVisible: boolean;
popoverVisible: boolean;
icons: string[];
fileList: any[];
error: boolean;
modelLoaded: boolean;
}
interface iconObject {
type: string;
score: number;
}
class PicSearcher extends Component<PicSearcherProps, PicSearcherState> {
state = {
loading: false,
modalVisible: false,
popoverVisible: false,
icons: [],
fileList: [],
error: false,
modelLoaded: false,
};
componentDidMount() {
this.loadModel();
this.setState({ popoverVisible: !localStorage.getItem('disableIconTip') });
}
componentWillUnmount() {
document.removeEventListener('paste', this.onPaste);
}
loadModel = () => {
const script = document.createElement('script');
script.onload = async () => {
await window.antdIconClassifier.load();
this.setState({ modelLoaded: true });
document.addEventListener('paste', this.onPaste);
};
script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js';
document.head.appendChild(script);
};
onPaste = (event: ClipboardEvent) => {
const items = event.clipboardData && event.clipboardData.items;
let file = null;
if (items && items.length) {
for (let i = 0; i < items.length; i += 1) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile();
break;
}
}
}
if (file) this.uploadFile(file);
};
uploadFile = (file: File) => {
this.setState(() => ({ loading: true }));
const reader: FileReader = new FileReader();
reader.onload = () => {
this.toImage(reader.result).then(this.predict);
this.setState(() => ({
fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }],
}));
};
reader.readAsDataURL(file);
};
toImage = (url: any) =>
new Promise((resolve) => {
const img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
img.onload = function onload() {
resolve(img);
};
});
predict = (imgEl: any) => {
try {
let icons = window.antdIconClassifier.predict(imgEl);
// if (gtag && icons.length >= 1) {
// gtag('event', 'icon', {
// event_category: 'search-by-image',
// event_label: icons[0].className,
// });
// }
icons = icons.map((i: any) => ({ score: i.score, type: i.className.replace(/\s/g, '-') }));
this.setState(() => ({ icons, loading: false, error: false }));
} catch (err) {
this.setState(() => ({ loading: false, error: true }));
}
};
toggleModal = () => {
this.setState((prev) => ({
modalVisible: !prev.modalVisible,
popoverVisible: false,
fileList: [],
icons: [],
}));
if (!localStorage.getItem('disableIconTip')) {
localStorage.setItem('disableIconTip', 'true');
}
};
onCopied = (text: string) => {
message.success(
<span>
<code className="copied-code">{text}</code> copied 🎉
</span>,
);
};
render() {
const {
intl: { messages },
} = this.props;
const { modalVisible, popoverVisible, icons, fileList, loading, modelLoaded, error } =
this.state;
return (
<div className="icon-pic-searcher">
<Popover
content={messages[`app.docs.components.icon.pic-searcher.intro`]}
visible={popoverVisible}
>
<AntdIcons.CameraOutlined className="icon-pic-btn" onClick={this.toggleModal} />
</Popover>
<Modal
title={messages[`app.docs.components.icon.pic-searcher.title`]}
visible={modalVisible}
onCancel={this.toggleModal}
footer={null}
>
{modelLoaded || (
<Spin
spinning={!modelLoaded}
tip={messages['app.docs.components.icon.pic-searcher.modelloading']}
>
<div style={{ height: 100 }} />
</Spin>
)}
{modelLoaded && (
<Dragger
accept="image/jpeg, image/png"
listType="picture"
customRequest={(o: any) => this.uploadFile(o.file)}
fileList={fileList}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<p className="ant-upload-drag-icon">
<AntdIcons.InboxOutlined />
</p>
<p className="ant-upload-text">
{messages['app.docs.components.icon.pic-searcher.upload-text']}
</p>
<p className="ant-upload-hint">
{messages['app.docs.components.icon.pic-searcher.upload-hint']}
</p>
</Dragger>
)}
<Spin spinning={loading} tip={messages['app.docs.components.icon.pic-searcher.matching']}>
<div className="icon-pic-search-result">
{icons.length > 0 && (
<div className="result-tip">
{messages['app.docs.components.icon.pic-searcher.result-tip']}
</div>
)}
<table>
{icons.length > 0 && (
<thead>
<tr>
<th className="col-icon">
{messages['app.docs.components.icon.pic-searcher.th-icon']}
</th>
<th>{messages['app.docs.components.icon.pic-searcher.th-score']}</th>
</tr>
</thead>
)}
<tbody>
{icons.map((icon: iconObject) => {
const { type } = icon;
const iconName = `${type
.split('-')
.map((str) => `${str[0].toUpperCase()}${str.slice(1)}`)
.join('')}Outlined`;
return (
<tr key={iconName}>
<td className="col-icon">
{/* <CopyToClipboard text={`<${iconName} />`} onCopy={this.onCopied}> */}
<Tooltip title={icon.type} placement="right">
{React.createElement(allIcons[iconName])}
</Tooltip>
{/* </CopyToClipboard> */}
</td>
<td>
<Progress percent={Math.ceil(icon.score * 100)} />
</td>
</tr>
);
})}
</tbody>
</table>
{error && (
<Result
status="500"
title="503"
subTitle={messages['app.docs.components.icon.pic-searcher.server-error']}
/>
)}
</div>
</Spin>
</Modal>
</div>
);
}
}
export default injectIntl(PicSearcher);

View File

@ -0,0 +1,221 @@
import * as AntdIcons from '@ant-design/icons/lib/icons';
const all = Object.keys(AntdIcons)
.map((n) => n.replace(/(Outlined|Filled|TwoTone)$/, ''))
.filter((n, i, arr) => arr.indexOf(n) === i);
const direction = [
'StepBackward',
'StepForward',
'FastBackward',
'FastForward',
'Shrink',
'ArrowsAlt',
'Down',
'Up',
'Left',
'Right',
'CaretUp',
'CaretDown',
'CaretLeft',
'CaretRight',
'UpCircle',
'DownCircle',
'LeftCircle',
'RightCircle',
'DoubleRight',
'DoubleLeft',
'VerticalLeft',
'VerticalRight',
'VerticalAlignTop',
'VerticalAlignMiddle',
'VerticalAlignBottom',
'Forward',
'Backward',
'Rollback',
'Enter',
'Retweet',
'Swap',
'SwapLeft',
'SwapRight',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'PlayCircle',
'UpSquare',
'DownSquare',
'LeftSquare',
'RightSquare',
'Login',
'Logout',
'MenuFold',
'MenuUnfold',
'BorderBottom',
'BorderHorizontal',
'BorderInner',
'BorderOuter',
'BorderLeft',
'BorderRight',
'BorderTop',
'BorderVerticle',
'PicCenter',
'PicLeft',
'PicRight',
'RadiusBottomleft',
'RadiusBottomright',
'RadiusUpleft',
'RadiusUpright',
'Fullscreen',
'FullscreenExit',
];
const suggestion = [
'Question',
'QuestionCircle',
'Plus',
'PlusCircle',
'Pause',
'PauseCircle',
'Minus',
'MinusCircle',
'PlusSquare',
'MinusSquare',
'Info',
'InfoCircle',
'Exclamation',
'ExclamationCircle',
'Close',
'CloseCircle',
'CloseSquare',
'Check',
'CheckCircle',
'CheckSquare',
'ClockCircle',
'Warning',
'IssuesClose',
'Stop',
];
const editor = [
'Edit',
'Form',
'Copy',
'Scissor',
'Delete',
'Snippets',
'Diff',
'Highlight',
'AlignCenter',
'AlignLeft',
'AlignRight',
'BgColors',
'Bold',
'Italic',
'Underline',
'Strikethrough',
'Redo',
'Undo',
'ZoomIn',
'ZoomOut',
'FontColors',
'FontSize',
'LineHeight',
'Dash',
'SmallDash',
'SortAscending',
'SortDescending',
'Drag',
'OrderedList',
'UnorderedList',
'RadiusSetting',
'ColumnWidth',
'ColumnHeight',
];
const data = [
'AreaChart',
'PieChart',
'BarChart',
'DotChart',
'LineChart',
'RadarChart',
'HeatMap',
'Fall',
'Rise',
'Stock',
'BoxPlot',
'Fund',
'Sliders',
];
const logo = [
'Android',
'Apple',
'Windows',
'Ie',
'Chrome',
'Github',
'Aliwangwang',
'Dingding',
'WeiboSquare',
'WeiboCircle',
'TaobaoCircle',
'Html5',
'Weibo',
'Twitter',
'Wechat',
'Youtube',
'AlipayCircle',
'Taobao',
'Skype',
'Qq',
'MediumWorkmark',
'Gitlab',
'Medium',
'Linkedin',
'GooglePlus',
'Dropbox',
'Facebook',
'Codepen',
'CodeSandbox',
'CodeSandboxCircle',
'Amazon',
'Google',
'CodepenCircle',
'Alipay',
'AntDesign',
'AntCloud',
'Aliyun',
'Zhihu',
'Slack',
'SlackSquare',
'Behance',
'BehanceSquare',
'Dribbble',
'DribbbleSquare',
'Instagram',
'Yuque',
'Alibaba',
'Yahoo',
'Reddit',
'Sketch',
];
const datum = [...direction, ...suggestion, ...editor, ...data, ...logo];
const other = all.filter((n) => !datum.includes(n));
export const categories = {
direction,
suggestion,
editor,
data,
logo,
other,
};
export default categories;
export type Categories = typeof categories;
export type CategoriesKeys = keyof Categories;

View File

@ -0,0 +1,163 @@
import * as React from 'react';
import Icon, * as AntdIcons from '@ant-design/icons';
import { Radio, Input, Empty, Tabs, Row, Col } from 'antd';
import type { RadioChangeEvent } from 'antd/es/radio/interface';
import { injectIntl } from 'react-intl';
import debounce from 'lodash/debounce';
import Category from './Category';
import type { Categories, CategoriesKeys } from './fields';
import { categories } from './fields';
import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons';
import { Fragment } from 'react';
const { TabPane } = Tabs;
export enum ThemeType {
Filled = 'Filled',
Outlined = 'Outlined',
TwoTone = 'TwoTone',
}
const allIcons: Record<string, any> = AntdIcons;
interface IconSelectorProps {
intl: any;
onSelect: any;
}
interface IconSelectorState {
theme: ThemeType;
searchKey: string;
}
class IconSelector extends React.PureComponent<IconSelectorProps, IconSelectorState> {
static categories: Categories = categories;
static newIconNames: string[] = [];
state: IconSelectorState = {
theme: ThemeType.Outlined,
searchKey: '',
};
constructor(props: IconSelectorProps) {
super(props);
this.handleSearchIcon = debounce(this.handleSearchIcon, 300);
}
handleChangeTheme = (e: RadioChangeEvent) => {
this.setState({
theme: e.target.value as ThemeType,
});
};
handleSearchIcon = (searchKey: string) => {
this.setState((prevState) => ({
...prevState,
searchKey,
}));
};
getTitle(cate: string) {
const titles = {
direction: '方向性图标',
suggestion: '提示建议性图标',
editor: '编辑类图标',
data: '数据类图标',
logo: '品牌和标识',
other: '网站通用图标',
};
return titles[cate];
}
renderTabs() {
const { searchKey = '', theme } = this.state;
const { onSelect } = this.props;
const categoriesResult = Object.keys(categories)
.map((key: any) => {
let iconList = categories[key];
if (searchKey) {
const matchKey = searchKey
.replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name)
.replace(/(Filled|Outlined|TwoTone)$/, '')
.toLowerCase();
iconList = iconList.filter((iconName: string) =>
iconName.toLowerCase().includes(matchKey),
);
}
// CopyrightCircle is same as Copyright, don't show it
iconList = iconList.filter((icon: string) => icon !== 'CopyrightCircle');
return {
category: key,
icons: iconList
.map((iconName: ThemeType) => iconName + theme)
.filter((iconName: string | number) => allIcons[iconName]),
};
})
.filter(({ icons }) => !!icons.length)
.map(({ category, icons }) => (
<TabPane tab={this.getTitle(category)} key={category}>
<Category
key={category}
title={category as CategoriesKeys}
theme={theme}
icons={icons}
newIcons={IconSelector.newIconNames}
onSelect={(name) => {
if (onSelect) {
onSelect(name, allIcons[name]);
}
}}
/>
</TabPane>
));
return categoriesResult.length === 0 ? <Empty style={{ margin: '2em 0' }} /> : categoriesResult;
}
render() {
const {
intl: { messages },
} = this.props;
return (
<Fragment>
<Row gutter={[16, 16]}>
<Col span={4}>
<Radio.Group
value={this.state.theme}
onChange={this.handleChangeTheme}
size="large"
buttonStyle="solid"
>
<Radio.Button value={ThemeType.Outlined}>
<Icon component={OutlinedIcon} /> {messages['app.docs.components.icon.outlined']}
</Radio.Button>
<Radio.Button value={ThemeType.Filled}>
<Icon component={FilledIcon} /> {messages['app.docs.components.icon.filled']}
</Radio.Button>
<Radio.Button value={ThemeType.TwoTone}>
<Icon component={TwoToneIcon} /> {messages['app.docs.components.icon.two-tone']}
</Radio.Button>
</Radio.Group>
</Col>
<Col span={18}>
<Input.Search
style={{ margin: '0 10px', flex: 1 }}
allowClear
onChange={(e) => this.handleSearchIcon(e.currentTarget.value)}
size="large"
autoFocus
// suffix={<IconPicSearcher />}
/>
</Col>
</Row>
<Row>
<Tabs defaultActiveKey="1">{this.renderTabs()}</Tabs>
</Row>
{/* {this.renderCategories()} */}
</Fragment>
);
}
}
export default injectIntl(IconSelector);

View File

@ -0,0 +1,81 @@
ul.anticons-list {
margin: 6px 0;
overflow: hidden;
direction: ltr;
list-style: none;
li {
position: relative;
float: left;
width: 16.66%;
height: 60px;
margin: 30px 0;
padding: 4px 0 0;
overflow: hidden;
color: #555;
text-align: center;
list-style: none;
background-color: inherit;
border-radius: 3px;
cursor: pointer;
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
.rtl & {
margin: 2px 0;
padding: 3px 0 0;
}
.anticon {
margin: 2px 0 3px;
font-size: 26px;
transition: transform 0.3s ease-in-out;
will-change: transform;
}
.anticon-class {
display: block;
font-family: 'Lucida Console', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
white-space: nowrap;
text-align: center;
transform: scale(0.83);
.ant-badge {
transition: color 0.3s ease-in-out;
}
}
&:hover {
color: #fff;
background-color: @primary-color;
.anticon {
transform: scale(1.4);
}
.ant-badge {
color: #fff;
}
}
&.TwoTone:hover {
background-color: #8ecafe;
}
&.copied:hover {
color: rgba(255, 255, 255, 0.2);
}
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: #fff;
line-height: 110px;
text-align: center;
opacity: 0;
transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
content: 'Copied!';
}
&.copied::after {
top: -10px;
opacity: 1;
}
}
}

View File

@ -0,0 +1,46 @@
import * as React from 'react';
export const FilledIcon: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (
props,
) => {
const path =
'M864 64H160C107 64 64 107 64 160v' +
'704c0 53 43 96 96 96h704c53 0 96-43 96-96V16' +
'0c0-53-43-96-96-96z';
return (
<svg {...(props as any)} viewBox="0 0 1024 1024">
<path d={path} />
</svg>
);
};
export const OutlinedIcon: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (
props,
) => {
const path =
'M864 64H160C107 64 64 107 64 160v7' +
'04c0 53 43 96 96 96h704c53 0 96-43 96-96V160c' +
'0-53-43-96-96-96z m-12 800H172c-6.6 0-12-5.4-' +
'12-12V172c0-6.6 5.4-12 12-12h680c6.6 0 12 5.4' +
' 12 12v680c0 6.6-5.4 12-12 12z';
return (
<svg {...(props as any)} viewBox="0 0 1024 1024">
<path d={path} />
</svg>
);
};
export const TwoToneIcon: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (
props,
) => {
const path =
'M16 512c0 273.932 222.066 496 496 49' +
'6s496-222.068 496-496S785.932 16 512 16 16 238.' +
'066 16 512z m496 368V144c203.41 0 368 164.622 3' +
'68 368 0 203.41-164.622 368-368 368z';
return (
<svg {...(props as any)} viewBox="0 0 1024 1024">
<path d={path} />
</svg>
);
};

View File

@ -0,0 +1,103 @@
@import '~antd/es/style/themes/default.less';
.list {
max-height: 400px;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
.item {
padding-right: 24px;
padding-left: 24px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
.meta {
width: 100%;
}
.avatar {
margin-top: 4px;
background: @component-background;
}
.iconElement {
font-size: 32px;
}
&.read {
opacity: 0.4;
}
&:last-child {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.title {
margin-bottom: 8px;
font-weight: normal;
}
.description {
font-size: 12px;
line-height: @line-height-base;
}
.datetime {
margin-top: 4px;
font-size: 12px;
line-height: @line-height-base;
}
.extra {
float: right;
margin-top: -1.5px;
margin-right: 0;
color: @text-color-secondary;
font-weight: normal;
}
}
.loadMore {
padding: 8px 0;
color: @primary-6;
text-align: center;
cursor: pointer;
&.loadedAll {
color: rgba(0, 0, 0, 0.25);
cursor: unset;
}
}
}
.notFound {
padding: 73px 0 88px;
color: @text-color-secondary;
text-align: center;
img {
display: inline-block;
height: 76px;
margin-bottom: 16px;
}
}
.bottomBar {
height: 46px;
color: @text-color;
line-height: 46px;
text-align: center;
border-top: 1px solid @border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
transition: all 0.3s;
div {
display: inline-block;
width: 50%;
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:only-child {
width: 100%;
}
&:not(:only-child):last-child {
border-left: 1px solid @border-color-split;
}
}
}

View File

@ -0,0 +1,116 @@
import { Avatar, List } from 'antd';
import React from 'react';
import classNames from 'classnames';
import type { NoticeIconData } from './index';
import styles from './NoticeList.less';
export type NoticeIconTabProps = {
count?: number;
name?: string;
showClear?: boolean;
showViewMore?: boolean;
style?: React.CSSProperties;
title: string;
tabKey: string;
data?: NoticeIconData[];
onClick?: (item: NoticeIconData) => void;
onClear?: () => void;
emptyText?: string;
clearText?: string;
viewMoreText?: string;
list: NoticeIconData[];
onViewMore?: (e: any) => void;
};
const NoticeList: React.SFC<NoticeIconTabProps> = ({
data = [],
onClick,
onClear,
title,
onViewMore,
emptyText,
showClear = true,
clearText,
viewMoreText,
showViewMore = false,
}) => {
if (!data || data.length === 0) {
return (
<div className={styles.notFound}>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
alt="not found"
/>
<div>{emptyText}</div>
</div>
);
}
return (
<div>
<List<NoticeIconData>
className={styles.list}
dataSource={data}
renderItem={(item, i) => {
const itemCls = classNames(styles.item, {
[styles.read]: item.read,
});
// eslint-disable-next-line no-nested-ternary
const leftIcon = item.avatar ? (
typeof item.avatar === 'string' ? (
<Avatar className={styles.avatar} src={item.avatar} />
) : (
<span className={styles.iconElement}>{item.avatar}</span>
)
) : null;
return (
<List.Item
className={itemCls}
key={item.key || i}
onClick={() => {
onClick?.(item);
}}
>
<List.Item.Meta
className={styles.meta}
avatar={leftIcon}
title={
<div className={styles.title}>
{item.title}
<div className={styles.extra}>{item.extra}</div>
</div>
}
description={
<div>
<div className={styles.description}>{item.description}</div>
<div className={styles.datetime}>{item.datetime}</div>
</div>
}
/>
</List.Item>
);
}}
/>
<div className={styles.bottomBar}>
{showClear ? (
<div onClick={onClear}>
{clearText} {title}
</div>
) : null}
{showViewMore ? (
<div
onClick={(e) => {
if (onViewMore) {
onViewMore(e);
}
}}
>
{viewMoreText}
</div>
) : null}
</div>
</div>
);
};
export default NoticeList;

View File

@ -0,0 +1,35 @@
@import '~antd/es/style/themes/default.less';
.popover {
position: relative;
width: 336px;
}
.noticeButton {
display: inline-block;
cursor: pointer;
transition: all 0.3s;
}
.icon {
padding: 4px;
vertical-align: middle;
}
.badge {
font-size: 16px;
}
.tabs {
:global {
.ant-tabs-nav-list {
margin: auto;
}
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-bar {
margin-bottom: 0;
}
}
}

View File

@ -0,0 +1,142 @@
import { BellOutlined } from '@ant-design/icons';
import { Badge, Spin, Tabs } from 'antd';
import useMergedState from 'rc-util/es/hooks/useMergedState';
import React from 'react';
import classNames from 'classnames';
import type { NoticeIconTabProps } from './NoticeList';
import NoticeList from './NoticeList';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
const { TabPane } = Tabs;
export type NoticeIconData = {
avatar?: string | React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
datetime?: React.ReactNode;
extra?: React.ReactNode;
style?: React.CSSProperties;
key?: string | number;
read?: boolean;
};
export type NoticeIconProps = {
count?: number;
bell?: React.ReactNode;
className?: string;
loading?: boolean;
onClear?: (tabName: string, tabKey: string) => void;
onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void;
onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
onTabChange?: (tabTile: string) => void;
style?: React.CSSProperties;
onPopupVisibleChange?: (visible: boolean) => void;
popupVisible?: boolean;
clearText?: string;
viewMoreText?: string;
clearClose?: boolean;
emptyImage?: string;
children: React.ReactElement<NoticeIconTabProps>[];
};
const NoticeIcon: React.FC<NoticeIconProps> & {
Tab: typeof NoticeList;
} = (props) => {
const getNotificationBox = (): React.ReactNode => {
const {
children,
loading,
onClear,
onTabChange,
onItemClick,
onViewMore,
clearText,
viewMoreText,
} = props;
if (!children) {
return null;
}
const panes: React.ReactNode[] = [];
React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
if (!child) {
return;
}
const { list, title, count, tabKey, showClear, showViewMore } = child.props;
const len = list && list.length ? list.length : 0;
const msgCount = count || count === 0 ? count : len;
const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
panes.push(
<TabPane tab={tabTitle} key={tabKey}>
<NoticeList
{...child.props}
clearText={clearText}
viewMoreText={viewMoreText}
data={list}
onClear={(): void => {
onClear?.(title, tabKey);
}}
onClick={(item): void => {
onItemClick?.(item, child.props);
}}
onViewMore={(event): void => {
onViewMore?.(child.props, event);
}}
showClear={showClear}
showViewMore={showViewMore}
title={title}
/>
</TabPane>,
);
});
return (
<Spin spinning={loading} delay={300}>
<Tabs className={styles.tabs} onChange={onTabChange}>
{panes}
</Tabs>
</Spin>
);
};
const { className, count, bell } = props;
const [visible, setVisible] = useMergedState<boolean>(false, {
value: props.popupVisible,
onChange: props.onPopupVisibleChange,
});
const noticeButtonClass = classNames(className, styles.noticeButton);
const notificationBox = getNotificationBox();
const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
const trigger = (
<span className={classNames(noticeButtonClass, { opened: visible })}>
<Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
{NoticeBellIcon}
</Badge>
</span>
);
if (!notificationBox) {
return trigger;
}
return (
<HeaderDropdown
placement="bottomRight"
overlay={notificationBox}
overlayClassName={styles.popover}
trigger={['click']}
visible={visible}
onVisibleChange={setVisible}
>
{trigger}
</HeaderDropdown>
);
};
NoticeIcon.defaultProps = {
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
};
NoticeIcon.Tab = NoticeList;
export default NoticeIcon;

View File

@ -0,0 +1,5 @@
import { PageLoading } from '@ant-design/pro-layout';
// loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
export default PageLoading;

View File

@ -0,0 +1,115 @@
import React from 'react';
import type { TreeSelectProps } from 'antd';
import ProField from '@ant-design/pro-field';
import type { ProSchema } from '@ant-design/pro-utils';
import { runFunction } from '@ant-design/pro-utils';
import type { ProFormItemProps } from '@ant-design/pro-form/lib/interface'
import type { ExtendsProps } from '@ant-design/pro-form/lib/BaseForm/createField';
import createField from '@ant-design/pro-form/lib/BaseForm/createField';
export type ProFormTreeSelectProps<T = any> = ProFormItemProps<
TreeSelectProps<T> & {
/**
*
*
* @default false
*/
searchOnFocus?: boolean;
/**
*
*
* @default false
*/
resetAfterSelect?: boolean;
/** 自定义选项渲染 */
optionItemRender?: (item: T) => React.ReactNode;
}
> & {
valueEnum?: ProSchema['valueEnum'];
params?: ProSchema['params'];
request?: ProSchema['request'];
options?: TreeSelectProps<any>['options'] | string[];
multiple?: TreeSelectProps<any>['multiple'] | false;
showSearch?: TreeSelectProps<any>['showSearch'];
readonly?: boolean;
};
/**
*
*
* @param
*/
const ProFormTreeSelectComponents = React.forwardRef<any, ProFormTreeSelectProps<any>>(
(
{ fieldProps, children, params, proFieldProps, multiple, valueEnum, request, showSearch, options },
ref,
) => {
return (
<ProField
mode="edit"
valueEnum={runFunction(valueEnum)}
request={request}
params={params}
valueType="select"
fieldProps={{
options,
multiple,
showSearch,
...fieldProps,
}}
ref={ref}
{...proFieldProps}
>
{children}
</ProField>
);
},
);
const SearchTreeSelect = React.forwardRef<any, ProFormTreeSelectProps<any>>(
({ fieldProps, children, params, proFieldProps, multiple, valueEnum, request, options }, ref) => {
const props: Omit<TreeSelectProps<any>, 'options'> & {
options?: ProFormTreeSelectProps['options'];
} = {
options,
multiple: false,
labelInValue: true,
showSearch: true,
showArrow: false,
autoClearSearchValue: true,
...fieldProps,
};
return (
<ProField
mode="edit"
valueEnum={runFunction(valueEnum)}
request={request}
params={params}
valueType="select"
fieldProps={props}
ref={ref}
{...proFieldProps}
>
{children}
</ProField>
);
},
);
const ProFormSelect = createField<ProFormTreeSelectProps>(ProFormTreeSelectComponents, {
customLightMode: true,
}) as <T>(props: ProFormTreeSelectProps<T> & ExtendsProps) => React.ReactElement;
const ProFormSearchTreeSelect = createField<ProFormTreeSelectProps>(SearchTreeSelect, {
customLightMode: true,
}) as <T>(props: ProFormTreeSelectProps<T> & ExtendsProps) => React.ReactElement;
const WrappedProFormTreeSelect = ProFormSelect as (<T = any>(
props: ProFormTreeSelectProps<T>,
) => React.ReactElement) & {
SearchTreeSelect: typeof ProFormSearchTreeSelect;
};
WrappedProFormTreeSelect.SearchTreeSelect = ProFormSearchTreeSelect;
export default WrappedProFormTreeSelect;

View File

@ -0,0 +1 @@
export default undefined;

View File

@ -0,0 +1,57 @@
const { uniq } = require('lodash');
const RouterConfig = require('../../config/config').default.routes;
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
function formatter(routes, parentPath = '') {
const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
let result = [];
routes.forEach((item) => {
if (item.path) {
result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
}
if (item.routes) {
result = result.concat(
formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
);
}
});
return uniq(result.filter((item) => !!item));
}
beforeEach(async () => {
await page.goto(`${BASE_URL}`);
await page.evaluate(() => {
localStorage.setItem('antd-pro-authority', '["admin"]');
});
});
describe('Ant Design Pro E2E test', () => {
const testPage = (path) => async () => {
await page.goto(`${BASE_URL}${path}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0,
);
expect(haveFooter).toBeTruthy();
};
const routers = formatter(RouterConfig);
routers.forEach((route) => {
it(`test pages ${route}`, testPage(route));
});
it('topmenu should have footer', async () => {
const params = '?navTheme=light&layout=topmenu';
await page.goto(`${BASE_URL}${params}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0,
);
expect(haveFooter).toBeTruthy();
});
});

55
react-ui/src/global.less Normal file
View File

@ -0,0 +1,55 @@
@import '~antd/es/style/themes/default.less';
@import './components/IconSelector/style.less';
html,
body,
#root {
height: 100%;
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
// 兼容IE11
@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
body .ant-design-pro > .ant-layout {
min-height: 100vh;
}
}

85
react-ui/src/global.tsx Normal file
View File

@ -0,0 +1,85 @@
import { Button, message, notification } from 'antd';
import React from 'react';
import { useIntl } from 'umi';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
const isHttps = document.location.protocol === 'https:';
// if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
});
// Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', (event: Event) => {
const e = event as CustomEvent;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail && e.detail.waiting;
if (!worker) {
return true;
}
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (msgEvent) => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
});
// Refresh current page to use the updated HTML and other assets after SW has skiped waiting
window.location.reload(true);
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.close(key);
reloadSW();
}}
>
{useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
</Button>
);
notification.open({
message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
btn,
key,
onClose: async () => null,
});
});
} else if ('serviceWorker' in navigator && isHttps) {
// unregister service worker
const { serviceWorker } = navigator;
if (serviceWorker.getRegistrations) {
serviceWorker.getRegistrations().then((sws) => {
sws.forEach((sw) => {
sw.unregister();
});
});
}
serviceWorker.getRegistration().then((sw) => {
if (sw) sw.unregister();
});
// remove all caches
if (window.caches) {
caches.keys().then((keys) => {
keys.forEach((key) => {
caches.delete(key);
});
});
}
}

View File

@ -0,0 +1,195 @@
/**
* Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
*
* @see You can view component api by: https://github.com/ant-design/ant-design-pro-layout
*/
import type {
MenuDataItem,
BasicLayoutProps as ProLayoutProps,
Settings,
} from '@ant-design/pro-layout';
import ProLayout, { DefaultFooter, SettingDrawer } from '@ant-design/pro-layout';
import React, { useEffect, useMemo, useRef } from 'react';
import type { Dispatch } from 'umi';
import { Link, useIntl, connect, history } from 'umi';
import { GithubOutlined } from '@ant-design/icons';
import { Result, Button } from 'antd';
import Authorized from '@/utils/Authorized';
import RightContent from '@/components/GlobalHeader/RightContent';
import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
import { getMatchMenu } from '@umijs/route-utils';
import logo from '../assets/logo.svg';
const noMatch = (
<Result
status={403}
title="403"
subTitle="Sorry, you are not authorized to access this page."
extra={
<Button type="primary">
<Link to="/user/login">Go Login</Link>
</Button>
}
/>
);
export type BasicLayoutProps = {
breadcrumbNameMap: Record<string, MenuDataItem>;
route: ProLayoutProps['route'] & {
authority: string[];
};
currentUser: CurrentUser;
settings: Settings;
dispatch: Dispatch;
} & ProLayoutProps;
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
breadcrumbNameMap: Record<string, MenuDataItem>;
};
const defaultFooterDom = (
<DefaultFooter
copyright={`${new Date().getFullYear()} 蚂蚁集团体验技术部出品`}
links={[
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
blankTarget: true,
},
{
key: 'ruoyi-react',
title: <GithubOutlined />,
href: 'https://gitee.com/whiteshader/ruoyi-react',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
/>
);
const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
const {
dispatch,
children,
settings,
currentUser,
location = {
pathname: '/',
},
} = props;
const menuDataRef = useRef<MenuDataItem[]>([]);
useEffect(() => {
if (dispatch) {
dispatch({
type: 'user/initSession',
});
}
}, [dispatch]);
/** Init variables */
/** Use Authorized check all menu item */
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
menuList.map((item) => {
const localItem = {
...item,
children: item.children ? menuDataRender(item.children) : undefined,
};
return Authorized.check(currentUser, item.authority, localItem, null) as MenuDataItem;
});
const handleMenuCollapse = (payload: boolean): void => {
if (dispatch) {
dispatch({
type: 'global/changeLayoutCollapsed',
payload,
});
}
}; // get children authority
const authorized = useMemo(
() =>
getMatchMenu(location.pathname || '/', menuDataRef.current).pop() || {
authority: undefined,
},
[location.pathname],
);
const { formatMessage } = useIntl();
return (
<>
<ProLayout
logo={logo}
formatMessage={formatMessage}
{...props}
{...settings}
onCollapse={handleMenuCollapse}
onMenuHeaderClick={() => history.push('/')}
menuItemRender={(menuItemProps, defaultDom) => {
if (
menuItemProps.isUrl ||
!menuItemProps.path ||
location.pathname === menuItemProps.path
) {
return defaultDom;
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
}),
},
...routers,
]}
itemRender={(route, params, routes, paths) => {
const first = routes.indexOf(route) === 0;
return first ? (
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
) : (
<span>{route.breadcrumbName}</span>
);
}}
footerRender={() => {
if (settings.footerRender || settings.footerRender === undefined) {
return defaultFooterDom;
}
return null;
}}
menuDataRender={menuDataRender}
rightContentRender={() => <RightContent />}
postMenuData={(menuData) => {
menuDataRef.current = menuData || [];
return menuData || [];
}}
>
<Authorized authority={authorized!.authority} noMatch={noMatch}>
{children}
</Authorized>
</ProLayout>
<SettingDrawer
settings={settings}
onSettingChange={(config) =>
dispatch({
type: 'settings/changeSetting',
payload: config,
})
}
/>
</>
);
};
export default connect(({ global, user, settings }: ConnectState) => ({
collapsed: global.collapsed,
currentUser: user.currentUser,
settings,
}))(BasicLayout);

View File

@ -0,0 +1,10 @@
import React from 'react';
import { Inspector } from 'react-dev-inspector';
const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment;
const Layout: React.FC = ({ children }) => {
return <InspectorWrapper>{children}</InspectorWrapper>;
};
export default Layout;

View File

@ -0,0 +1,248 @@
/**
* Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
*
* @see You can view component api by: https://github.com/ant-design/ant-design-pro-layout
*/
import type {
MenuDataItem,
BasicLayoutProps as ProLayoutProps,
Settings,
} from '@ant-design/pro-layout';
import { PageLoading } from '@ant-design/pro-layout';
import ProLayout, { DefaultFooter, SettingDrawer } from '@ant-design/pro-layout';
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import type { Dispatch } from 'umi';
import { FormattedMessage } from 'umi';
import { Redirect } from 'umi';
import { Link, useIntl, connect, history } from 'umi';
import { GithubOutlined } from '@ant-design/icons';
import { Result, Button } from 'antd';
import Authorized from '@/utils/Authorized';
import RightContent from '@/components/GlobalHeader/RightContent';
import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
// import { getMatchMenu } from '@umijs/route-utils';
import { getAccessToken, clearToken } from '@/utils/authority';
import { getMatchMenuItem, getRoutersData } from '@/services/routers';
import logo from '../assets/logo.svg';
import { createIcon } from '@/utils/IconUtil';
/**
*
* @author whiteshader@163.com
*
* */
const noMatch = (
<Result
status={403}
title="403"
subTitle={
<FormattedMessage
id="pages.authorized.forbiden"
defaultMessage="Sorry, you are not authorized to access this page."
/>
}
extra={
<Button type="primary" onClick={() => history.goBack()}>
<FormattedMessage id="pages.goback" defaultMessage="Go Back" />
</Button>
}
/>
);
export type SecurityBasicLayoutProps = {
breadcrumbNameMap: Record<string, MenuDataItem>;
route: ProLayoutProps['route'] & {
authority: string[];
};
currentUser: CurrentUser;
settings: Settings;
dispatch: Dispatch;
} & ProLayoutProps;
export type BasicLayoutContext = { [K in 'location']: SecurityBasicLayoutProps[K] } & {
breadcrumbNameMap: Record<string, MenuDataItem>;
};
const defaultFooterDom = (
<DefaultFooter
copyright={`${new Date().getFullYear()} 蚂蚁集团体验技术部出品`}
links={[
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
blankTarget: true,
},
{
key: 'ruoyi-react',
title: <GithubOutlined />,
href: 'https://gitee.com/whiteshader/ruoyi-react',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
/>
);
const SecurityBasicLayout: React.FC<SecurityBasicLayoutProps> = (props) => {
const {
dispatch,
children,
settings,
loading,
location = {
pathname: '/',
},
currentUser,
} = props;
const [isReady, setIsReady] = useState<boolean>(false);
const [remoteMenuData, setRemoteMenuData] = useState<MenuDataItem[]>([]);
const menuDataRef = useRef<MenuDataItem[]>([]);
useEffect(() => {
if (dispatch) {
setIsReady(true);
dispatch({
type: 'user/initSession',
});
}
}, [dispatch]);
const handleMenuCollapse = (payload: boolean): void => {
if (dispatch) {
dispatch({
type: 'global/changeLayoutCollapsed',
payload,
});
}
}; // get children authority
/** Use Authorized check all menu item */
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
menuList.map((item) => {
const localItem = {
...item,
icon: createIcon(item.icon),
children: item.children ? menuDataRender(item.children) : undefined,
};
// console.log(localItem)
return Authorized.check(currentUser, item.authority, localItem, null) as MenuDataItem;
});
const accessToken = getAccessToken();
const isLogin = !!accessToken;
const queryString = new URLSearchParams({ redirect: window.location.href }).toString();
const { formatMessage } = useIntl();
const authorized = useMemo(() => {
if (location.pathname === '/') {
return { authority: undefined };
}
const item = getMatchMenuItem(location.pathname || '/', remoteMenuData).pop() || {
authority: '!*',
};
return item;
}, [location, remoteMenuData]);
if ((!isLogin && loading) || !isReady) {
return <PageLoading />;
}
if (!isLogin && window.location.pathname !== '/user/login') {
clearToken();
return <Redirect to={`/user/login?${queryString}`} />;
}
return (
<>
<ProLayout
logo={logo}
formatMessage={formatMessage}
{...props}
{...settings}
onCollapse={handleMenuCollapse}
onMenuHeaderClick={() => history.push('/')}
menuItemRender={(menuItemProps, defaultDom) => {
if (
menuItemProps.isUrl ||
!menuItemProps.path ||
location.pathname === menuItemProps.path
) {
return defaultDom;
}
return (
<Fragment>
{createIcon(menuItemProps.icon)} <Link to={menuItemProps.path}>{defaultDom}</Link>
</Fragment>
);
}}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
}),
},
...routers,
]}
menu={{
locale: false,
request: async (): Promise<MenuDataItem[]> => {
const res = await getRoutersData();
setRemoteMenuData(res);
// console.log(params, defaultMenuData);
return res;
},
}}
// itemRender={(route, params, routes, paths) => {
// const first = routes.indexOf(route) === 0;
// return first ? (
// <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
// ) : (
// <span>{route.breadcrumbName}</span>
// );
// }}
footerRender={() => {
if (settings.footerRender || settings.footerRender === undefined) {
return defaultFooterDom;
}
return null;
}}
menuDataRender={menuDataRender}
rightContentRender={() => <RightContent />}
postMenuData={(menuData) => {
menuDataRef.current = menuData || [];
return menuData || [];
}}
>
<Authorized authority={authorized.authority} noMatch={noMatch} currentUser={currentUser}>
{children}
</Authorized>
</ProLayout>
<SettingDrawer
settings={settings}
onSettingChange={(config) =>
dispatch({
type: 'settings/changeSetting',
payload: config,
})
}
/>
</>
);
};
export default connect(({ global, user, loading, settings }: ConnectState) => ({
collapsed: global.collapsed,
currentUser: user.currentUser,
settings,
loading: loading.models.user,
}))(SecurityBasicLayout);

View File

@ -0,0 +1,61 @@
import React from 'react';
import { PageLoading } from '@ant-design/pro-layout';
import type { ConnectProps } from 'umi';
import { Redirect, connect } from 'umi';
import { stringify } from 'querystring';
import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
import { getAccessToken, clearToken } from '@/utils/authority'
type SecurityLayoutProps = {
loading?: boolean;
currentUser?: CurrentUser;
} & ConnectProps;
type SecurityLayoutState = {
isReady: boolean;
};
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
state: SecurityLayoutState = {
isReady: false,
};
componentDidMount() {
this.setState({
isReady: true,
});
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'user/initSession',
});
}
}
render() {
const { isReady } = this.state;
const { children, loading } = this.props;
// You can replace it to your authentication rule (such as check token exists)
// 你可以把它替换成你自己的登录认证规则 (比如判断 token 是否存在)
const accessToken = getAccessToken();
// const isLogin = (currentUser && currentUser.userId && accessToken)?true:false;
const isLogin = !!accessToken;
const queryString = stringify({
redirect: window.location.href,
});
if ((!isLogin && loading) || !isReady) {
return <PageLoading />;
}
if (!isLogin && window.location.pathname !== '/user/login') {
clearToken();
return <Redirect to={`/user/login?${queryString}`} />;
}
return children;
}
}
export default connect(({ user, loading }: ConnectState) => ({
currentUser: user.currentUser,
loading: loading.models.user,
}))(SecurityLayout);

View File

@ -0,0 +1,71 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 32px 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 32px 0 24px;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 44px;
margin-right: 16px;
vertical-align: top;
}
.title {
position: relative;
top: 2px;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}

View File

@ -0,0 +1,70 @@
import type { MenuDataItem } from '@ant-design/pro-layout';
import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import type { ConnectProps } from 'umi';
import { Link, SelectLang, useIntl, connect, FormattedMessage } from 'umi';
import React from 'react';
import type { ConnectState } from '@/models/connect';
import logo from '../assets/logo.svg';
import styles from './UserLayout.less';
export type UserLayoutProps = {
breadcrumbNameMap: Record<string, MenuDataItem>;
} & Partial<ConnectProps>;
const UserLayout: React.FC<UserLayoutProps> = (props) => {
const {
route = {
routes: [],
},
} = props;
const { routes = [] } = route;
const {
children,
location = {
pathname: '',
},
} = props;
const { formatMessage } = useIntl();
const { breadcrumb } = getMenuData(routes);
const title = getPageTitle({
pathname: location.pathname,
formatMessage,
breadcrumb,
...props,
});
return (
<HelmetProvider>
<Helmet>
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className={styles.container}>
<div className={styles.lang}>
<SelectLang />
</div>
<div className={styles.content}>
<div className={styles.top}>
<div className={styles.header}>
<Link to="/">
<img alt="logo" className={styles.logo} src={logo} />
<span className={styles.title}>Ant Design</span>
</Link>
</div>
<div className={styles.desc}>
<FormattedMessage
id="pages.layouts.userLayout.title"
defaultMessage="Ant Design 是西湖区最具影响力的 Web 设计规范"
/>
</div>
</div>
{children}
</div>
<DefaultFooter />
</div>
</HelmetProvider>
);
};
export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout);

View File

@ -0,0 +1,24 @@
import component from './en-US/component';
import globalHeader from './en-US/globalHeader';
import menu from './en-US/menu';
import pwa from './en-US/pwa';
import settingDrawer from './en-US/settingDrawer';
import settings from './en-US/settings';
import pages from './en-US/pages';
export default {
'navBar.lang': 'Languages',
'layout.user.link.help': 'Help',
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'app.preview.down.block': 'Download this page to your local project',
'app.welcome.link.fetch-blocks': 'Get all block',
'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': 'Expand',
'component.tagSelect.collapse': 'Collapse',
'component.tagSelect.all': 'All',
};

View File

@ -0,0 +1,17 @@
export default {
'component.globalHeader.search': 'Search',
'component.globalHeader.search.example1': 'Search example 1',
'component.globalHeader.search.example2': 'Search example 2',
'component.globalHeader.search.example3': 'Search example 3',
'component.globalHeader.help': 'Help',
'component.globalHeader.notification': 'Notification',
'component.globalHeader.notification.empty': 'You have viewed all notifications.',
'component.globalHeader.message': 'Message',
'component.globalHeader.message.empty': 'You have viewed all messsages.',
'component.globalHeader.event': 'Event',
'component.globalHeader.event.empty': 'You have viewed all events.',
'component.noticeIcon.clear': 'Clear',
'component.noticeIcon.cleared': 'Cleared',
'component.noticeIcon.empty': 'No notifications',
'component.noticeIcon.view-more': 'View more',
};

View File

@ -0,0 +1,55 @@
export default {
'menu.welcome': 'Welcome',
'menu.more-blocks': 'More Blocks',
'menu.home': 'Home',
'menu.admin': 'Admin',
'menu.admin.sub-page': 'Sub-Page',
'menu.login': 'Login',
'menu.register': 'Register',
'menu.register.result': 'Register Result',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Analysis',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Workplace',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Form',
'menu.form.basic-form': 'Basic Form',
'menu.form.step-form': 'Step Form',
'menu.form.step-form.info': 'Step Form(write transfer information)',
'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
'menu.form.step-form.result': 'Step Form(finished)',
'menu.form.advanced-form': 'Advanced Form',
'menu.list': 'List',
'menu.list.table-list': 'Search Table',
'menu.list.basic-list': 'Basic List',
'menu.list.card-list': 'Card List',
'menu.list.search-list': 'Search List',
'menu.list.search-list.articles': 'Search List(articles)',
'menu.list.search-list.projects': 'Search List(projects)',
'menu.list.search-list.applications': 'Search List(applications)',
'menu.profile': 'Profile',
'menu.profile.basic': 'Basic Profile',
'menu.profile.advanced': 'Advanced Profile',
'menu.result': 'Result',
'menu.result.success': 'Success',
'menu.result.fail': 'Fail',
'menu.exception': 'Exception',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Trigger',
'menu.account': 'Account',
'menu.account.center': 'Account Center',
'menu.account.settings': 'Account Settings',
'menu.account.trigger': 'Trigger Error',
'menu.account.logout': 'Logout',
'menu.editor': 'Graphic Editor',
'menu.editor.flow': 'Flow Editor',
'menu.editor.mind': 'Mind Editor',
'menu.editor.koni': 'Koni Editor',
'menu.test': 'Test',
'menu.test.mqtt': 'Mqtt',
'menu.System': 'System',
};

View File

@ -0,0 +1,70 @@
export default {
'pages.layouts.userLayout.title':
'Ant Design is the most influential web design specification in Xihu district',
'pages.login.accountLogin.tab': 'Account Login',
'pages.login.accountLogin.errorMessage': 'Incorrect username/passwordadmin/ant.design)',
'pages.login.username.placeholder': 'Username: admin or user',
'pages.login.username.required': 'Please input your username!',
'pages.login.password.placeholder': 'Password: ant.design',
'pages.login.password.required': 'Please input your password!',
'pages.login.phoneLogin.tab': 'Phone Login',
'pages.login.phoneLogin.errorMessage': 'Verification Code Error',
'pages.login.phoneNumber.placeholder': 'Phone Number',
'pages.login.phoneNumber.required': 'Please input your phone number!',
'pages.login.phoneNumber.invalid': 'Phone number is invalid!',
'pages.login.captcha.placeholder': 'Verification Code',
'pages.login.captcha.required': 'Please input verification code!',
'pages.login.phoneLogin.getVerificationCode': 'Get Code',
'pages.getCaptchaSecondText': 'sec(s)',
'pages.login.rememberMe': 'Remember me',
'pages.login.forgotPassword': 'Forgot Password ?',
'pages.login.submit': 'Submit',
'pages.login.loginWith': 'Login with :',
'pages.login.registerAccount': 'Register Account',
'pages.authorized.forbiden': 'Sorry, you are not authorized to access this page.',
'pages.welcome.advancedComponent': 'Advanced Component',
'pages.welcome.link': 'Welcome',
'pages.welcome.advancedLayout': 'Advanced Layout',
'pages.welcome.alertMessage': 'Faster and stronger heavy-duty components have been released.',
'pages.admin.subPage.title': 'This page can only be viewed by Admin',
'pages.admin.subPage.alertMessage':
'Umi ui is now released, welcome to use npm run ui to start the experience.',
'pages.searchTable.createForm.newRule': 'New Rule',
'pages.searchTable.updateForm.ruleConfig': 'Rule configuration',
'pages.searchTable.updateForm.basicConfig': 'Basic Information',
'pages.searchTable.updateForm.ruleName.nameLabel': 'Rule Name',
'pages.searchTable.updateForm.ruleName.nameRules': 'Please enter the rule name!',
'pages.searchTable.updateForm.ruleDesc.descLabel': 'Rule Description',
'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'Please enter at least five characters',
'pages.searchTable.updateForm.ruleDesc.descRules':
'Please enter a rule description of at least five characters!',
'pages.searchTable.updateForm.ruleProps.title': 'Configure Properties',
'pages.searchTable.updateForm.object': 'Monitoring Object',
'pages.searchTable.updateForm.ruleProps.templateLabel': 'Rule Template',
'pages.searchTable.updateForm.ruleProps.typeLabel': 'Rule Type',
'pages.searchTable.updateForm.schedulingPeriod.title': 'Set Scheduling Period',
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Starting Time',
'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'Please choose a start time!',
'pages.searchTable.titleDesc': 'Description',
'pages.searchTable.ruleName': 'Rule name is required',
'pages.searchTable.titleCallNo': 'Number of Service Calls',
'pages.searchTable.titleStatus': 'Status',
'pages.searchTable.nameStatus.default': 'default',
'pages.searchTable.nameStatus.running': 'running',
'pages.searchTable.nameStatus.online': 'online',
'pages.searchTable.nameStatus.abnormal': 'abnormal',
'pages.searchTable.titleUpdatedAt': 'Last Scheduled at',
'pages.searchTable.exception': 'Please enter the reason for the exception!',
'pages.searchTable.titleOption': 'Option',
'pages.searchTable.config': 'Configuration',
'pages.searchTable.subscribeAlert': 'Subscribe to alerts',
'pages.searchTable.title': 'Enquiry Form',
'pages.searchTable.new': 'New',
'pages.searchTable.chosen': 'chosen',
'pages.searchTable.item': 'item',
'pages.searchTable.totalServiceCalls': 'Total Number of Service Calls',
'pages.searchTable.tenThousand': '0000',
'pages.searchTable.batchDeletion': 'bacth deletion',
'pages.searchTable.batchApproval': 'batch approval',
'pages.goback': 'Go Back',
};

View File

@ -0,0 +1,6 @@
export default {
'app.pwa.offline': 'You are offline now',
'app.pwa.serviceworker.updated': 'New content is available',
'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page',
'app.pwa.serviceworker.updated.ok': 'Refresh',
};

View File

@ -0,0 +1,31 @@
export default {
'app.setting.pagestyle': 'Page style setting',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.light': 'Light style',
'app.setting.content-width': 'Content Width',
'app.setting.content-width.fixed': 'Fixed',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Navigation Mode',
'app.setting.sidemenu': 'Side Menu Layout',
'app.setting.topmenu': 'Top Menu Layout',
'app.setting.fixedheader': 'Fixed Header',
'app.setting.fixedsidebar': 'Fixed Sidebar',
'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
'app.setting.hideheader': 'Hidden Header when scrolling',
'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
'app.setting.othersettings': 'Other Settings',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copy Setting',
'app.setting.copyinfo': 'copy successplease replace defaultSettings in src/models/setting.js',
'app.setting.production.hint':
'Setting panel shows in development environment only, please manually modify',
};

View File

@ -0,0 +1,60 @@
export default {
'app.settings.menuMap.basic': 'Basic Settings',
'app.settings.menuMap.security': 'Security Settings',
'app.settings.menuMap.binding': 'Account Binding',
'app.settings.menuMap.notification': 'New Message Notification',
'app.settings.basic.avatar': 'Avatar',
'app.settings.basic.change-avatar': 'Change avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Please input your email!',
'app.settings.basic.nickname': 'Nickname',
'app.settings.basic.nickname-message': 'Please input your Nickname!',
'app.settings.basic.profile': 'Personal profile',
'app.settings.basic.profile-message': 'Please input your personal profile!',
'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
'app.settings.basic.country': 'Country/Region',
'app.settings.basic.country-message': 'Please input your country!',
'app.settings.basic.geographic': 'Province or city',
'app.settings.basic.geographic-message': 'Please input your geographic info!',
'app.settings.basic.address': 'Street Address',
'app.settings.basic.address-message': 'Please input your address!',
'app.settings.basic.phone': 'Phone Number',
'app.settings.basic.phone-message': 'Please input your phone!',
'app.settings.basic.update': 'Update Information',
'app.settings.security.strong': 'Strong',
'app.settings.security.medium': 'Medium',
'app.settings.security.weak': 'Weak',
'app.settings.security.password': 'Account Password',
'app.settings.security.password-description': 'Current password strength',
'app.settings.security.phone': 'Security Phone',
'app.settings.security.phone-description': 'Bound phone',
'app.settings.security.question': 'Security Question',
'app.settings.security.question-description':
'The security question is not set, and the security policy can effectively protect the account security',
'app.settings.security.email': 'Backup Email',
'app.settings.security.email-description': 'Bound Email',
'app.settings.security.mfa': 'MFA Device',
'app.settings.security.mfa-description':
'Unbound MFA device, after binding, can be confirmed twice',
'app.settings.security.modify': 'Modify',
'app.settings.security.set': 'Set',
'app.settings.security.bind': 'Bind',
'app.settings.binding.taobao': 'Binding Taobao',
'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
'app.settings.binding.alipay': 'Binding Alipay',
'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
'app.settings.binding.dingding': 'Binding DingTalk',
'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
'app.settings.binding.bind': 'Bind',
'app.settings.notification.password': 'Account Password',
'app.settings.notification.password-description':
'Messages from other users will be notified in the form of a station letter',
'app.settings.notification.messages': 'System Messages',
'app.settings.notification.messages-description':
'System messages will be notified in the form of a station letter',
'app.settings.notification.todo': 'To-do Notification',
'app.settings.notification.todo-description':
'The to-do list will be notified in the form of a letter from the station',
'app.settings.open': 'Open',
'app.settings.close': 'Close',
};

View File

@ -0,0 +1,25 @@
import component from './id-ID/component';
import globalHeader from './id-ID/globalHeader';
import menu from './id-ID/menu';
import pwa from './id-ID/pwa';
import settingDrawer from './id-ID/settingDrawer';
import settings from './id-ID/settings';
import pages from './id-ID/pages';
export default {
'navbar.lang': 'Bahasa',
'layout.user.link.help': 'Bantuan',
'layout.user.link.privacy': 'Privasi',
'layout.user.link.terms': 'Ketentuan',
'app.preview.down.block': 'Unduh halaman ini dalam projek lokal anda',
'app.welcome.link.fetch-blocks': 'Dapatkan semua blok',
'app.welcome.link.block-list':
'Buat standar dengan cepat, halaman-halaman berdasarkan pengembangan `block`',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': 'Perluas',
'component.tagSelect.collapse': 'Lipat',
'component.tagSelect.all': 'Semua',
};

View File

@ -0,0 +1,17 @@
export default {
'component.globalHeader.search': 'Pencarian',
'component.globalHeader.search.example1': 'Contoh 1 Pencarian',
'component.globalHeader.search.example2': 'Contoh 2 Pencarian',
'component.globalHeader.search.example3': 'Contoh 3 Pencarian',
'component.globalHeader.help': 'Bantuan',
'component.globalHeader.notification': 'Notifikasi',
'component.globalHeader.notification.empty': 'Anda telah membaca semua notifikasi',
'component.globalHeader.message': 'Pesan',
'component.globalHeader.message.empty': 'Anda telah membaca semua pesan.',
'component.globalHeader.event': 'Acara',
'component.globalHeader.event.empty': 'Anda telah melihat semua acara.',
'component.noticeIcon.clear': 'Kosongkan',
'component.noticeIcon.cleared': 'Berhasil dikosongkan',
'component.noticeIcon.empty': 'Tidak ada pemberitahuan',
'component.noticeIcon.view-more': 'Melihat lebih',
};

View File

@ -0,0 +1,52 @@
export default {
'menu.welcome': 'Selamat Datang',
'menu.more-blocks': 'Blocks Lainnya',
'menu.home': 'Halaman Awal',
'menu.admin': 'Admin',
'menu.admin.sub-page': 'Sub-Halaman',
'menu.login': 'Masuk',
'menu.register': 'Pendaftaran',
'menu.register.result': 'Hasil Pendaftaran',
'menu.dashboard': 'Dasbor',
'menu.dashboard.analysis': 'Analisis',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Workplace',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Form',
'menu.form.basic-form': 'Form Dasar',
'menu.form.step-form': 'Form Bertahap',
'menu.form.step-form.info': 'Form Bertahap(menulis informasi yang dibagikan)',
'menu.form.step-form.confirm': 'Form Bertahap(konfirmasi informasi yang dibagikan)',
'menu.form.step-form.result': 'Form Bertahap(selesai)',
'menu.form.advanced-form': 'Form Lanjutan',
'menu.list': 'Daftar',
'menu.list.table-list': 'Tabel Pencarian',
'menu.list.basic-list': 'Daftar Dasar',
'menu.list.card-list': 'Daftar Kartu',
'menu.list.search-list': 'Daftar Pencarian',
'menu.list.search-list.articles': 'Daftar Pencarian(artikel)',
'menu.list.search-list.projects': 'Daftar Pencarian(projek)',
'menu.list.search-list.applications': 'Daftar Pencarian(aplikasi)',
'menu.profile': 'Profil',
'menu.profile.basic': 'Profil Dasar',
'menu.profile.advanced': 'Profile Lanjutan',
'menu.result': 'Hasil',
'menu.result.success': 'Sukses',
'menu.result.fail': 'Gagal',
'menu.exception': 'Pengecualian',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Jalankan',
'menu.account': 'Akun',
'menu.account.center': 'Detail Akun',
'menu.account.settings': 'Pengaturan Akun',
'menu.account.trigger': 'Mengaktivasi Error',
'menu.account.logout': 'Keluar',
'menu.editor': 'Penyusun Grafis',
'menu.editor.flow': 'Penyusun Alur',
'menu.editor.mind': 'Penyusun Mind',
'menu.editor.koni': 'Penyusun Koni',
};

View File

@ -0,0 +1,70 @@
export default {
'pages.layouts.userLayout.title':
'Ant Design adalah spesifikasi desain Web yang paling berpengaruh di Kabupaten Xihu',
'pages.login.accountLogin.tab': 'Login dengan akun',
'pages.login.accountLogin.errorMessage': 'Nama pengguna dan kata sandi salahadmin/ant.design)',
'pages.login.username.placeholder': 'nama pengguna: admin atau user',
'pages.login.username.required': 'Nama pengguna harus diisi!',
'pages.login.password.placeholder': 'kata sandi: ant.design',
'pages.login.password.required': 'Kata sandi harus diisi!',
'pages.login.phoneLogin.tab': 'Login dengan ponsel',
'pages.login.phoneLogin.errorMessage': 'Kesalahan kode verifikasi',
'pages.login.phoneNumber.placeholder': 'masukkan nomor telepon',
'pages.login.phoneNumber.required': 'Nomor ponsel harus diisi!',
'pages.login.phoneNumber.invalid': 'Nomor ponsel tidak valid!',
'pages.login.captcha.placeholder': 'kode verifikasi',
'pages.login.captcha.required': 'Kode verifikasi diperlukan!',
'pages.login.phoneLogin.getVerificationCode': 'Dapatkan kode',
'pages.getCaptchaSecondText': 'detik tersisa',
'pages.login.rememberMe': 'Ingat saya',
'pages.login.forgotPassword': 'Lupa Kata Sandi?',
'pages.login.submit': 'Masuk',
'pages.login.loginWith': 'Masuk dengan :',
'pages.login.registerAccount': 'Daftar Akun',
'pages.welcome.advancedComponent': 'Formulir Lanjutan',
'pages.welcome.link': 'Selamat datang',
'pages.welcome.advancedLayout': 'Tata letak Lanjutan',
'pages.welcome.alertMessage':
'Komponen heavy-duty yang lebih cepat dan lebih kuat telah dirilis.',
'pages.admin.subPage.title': 'Halaman ini hanya dapat dilihat oleh admin',
'pages.admin.subPage.alertMessage':
'umi ui telah dirilis, silahkan gunakan npm run ui untuk memulai pengalaman.',
'pages.searchTable.createForm.newRule': 'Aturan baru',
'pages.searchTable.updateForm.ruleConfig': 'Konfigurasi aturan',
'pages.searchTable.updateForm.basicConfig': 'Informasi dasar',
'pages.searchTable.updateForm.ruleName.nameLabel': 'Nama aturan',
'pages.searchTable.updateForm.ruleName.nameRules': 'Harap masukkan nama aturan!',
'pages.searchTable.updateForm.ruleDesc.descLabel': 'Deskripsi aturan',
'pages.searchTable.updateForm.ruleDesc.descPlaceholder':
'Harap masukkan setidaknya lima karakter',
'pages.searchTable.updateForm.ruleDesc.descRules':
'Harap masukkan deskripsi aturan setidaknya lima karakter!',
'pages.searchTable.updateForm.ruleProps.title': 'Properti aturan',
'pages.searchTable.updateForm.object': 'Objek pemantauan',
'pages.searchTable.updateForm.ruleProps.templateLabel': 'Template aturan',
'pages.searchTable.updateForm.ruleProps.typeLabel': 'Jenis aturan',
'pages.searchTable.updateForm.schedulingPeriod.title': 'Periode penjadwalan',
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Waktu mulai',
'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'Pilih waktu mulai!',
'pages.searchTable.titleDesc': 'deskripsi',
'pages.searchTable.ruleName': 'Nama aturan wajib diisi',
'pages.searchTable.titleCallNo': 'Jumlah panggilan',
'pages.searchTable.titleStatus': 'Status',
'pages.searchTable.nameStatus.default': 'default',
'pages.searchTable.nameStatus.running': 'menyala',
'pages.searchTable.nameStatus.online': 'online',
'pages.searchTable.nameStatus.abnormal': 'abnormal',
'pages.searchTable.titleUpdatedAt': 'Waktu terjadwal',
'pages.searchTable.exception': 'Harap masukkan alasan pengecualian!',
'pages.searchTable.titleOption': 'Pengoperasian',
'pages.searchTable.config': 'Konfigurasi',
'pages.searchTable.subscribeAlert': 'Berlangganan notifikasi',
'pages.searchTable.title': 'Formulir pertanyaan',
'pages.searchTable.new': 'Baru',
'pages.searchTable.chosen': 'Terpilih',
'pages.searchTable.item': 'item',
'pages.searchTable.totalServiceCalls': 'Jumlah total panggilan layanan',
'pages.searchTable.tenThousand': '0000',
'pages.searchTable.batchDeletion': 'Penghapusan batch',
'pages.searchTable.batchApproval': 'Persetujuan batch',
};

View File

@ -0,0 +1,7 @@
export default {
'app.pwa.offline': 'Koneksi anda terputus',
'app.pwa.serviceworker.updated': 'Konten baru sudah tersedia',
'app.pwa.serviceworker.updated.hint':
'Silahkan klik tombol "Refresh" untuk memuat ulang halaman ini',
'app.pwa.serviceworker.updated.ok': 'Memuat ulang',
};

View File

@ -0,0 +1,32 @@
export default {
'app.setting.pagestyle': 'Pengaturan style Halaman',
'app.setting.pagestyle.dark': 'Style Gelap',
'app.setting.pagestyle.light': 'Style Cerah',
'app.setting.content-width': 'Lebar Konten',
'app.setting.content-width.fixed': 'Tetap',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (bawaan)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Mode Navigasi',
'app.setting.sidemenu': 'Susunan Menu Samping',
'app.setting.topmenu': 'Susunan Menu Atas',
'app.setting.fixedheader': 'Header Tetap',
'app.setting.fixedsidebar': 'Sidebar Tetap',
'app.setting.fixedsidebar.hint': 'Berjalan pada Susunan Menu Samping',
'app.setting.hideheader': 'Sembunyikan Header ketika gulir ke bawah',
'app.setting.hideheader.hint': 'Bekerja ketika Header tersembunyi dimunculkan',
'app.setting.othersettings': 'Pengaturan Lainnya',
'app.setting.weakmode': 'Mode Lemah',
'app.setting.copy': 'Salin Pengaturan',
'app.setting.copyinfo':
'Berhasil disalintolong ubah defaultSettings pada src/models/setting.js',
'app.setting.production.hint':
'Panel pengaturan hanya muncul pada lingkungan pengembangan, silahkan modifikasi secara menual',
};

View File

@ -0,0 +1,60 @@
export default {
'app.settings.menuMap.basic': 'Pengaturan Dasar',
'app.settings.menuMap.security': 'Pengaturan Keamanan',
'app.settings.menuMap.binding': 'Pengikatan Akun',
'app.settings.menuMap.notification': 'Notifikasi Pesan Baru',
'app.settings.basic.avatar': 'Avatar',
'app.settings.basic.change-avatar': 'Ubah avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Tolong masukkan email!',
'app.settings.basic.nickname': 'Nickname',
'app.settings.basic.nickname-message': 'Tolong masukkan Nickname!',
'app.settings.basic.profile': 'Profil Personal',
'app.settings.basic.profile-message': 'Tolong masukkan profil personal!',
'app.settings.basic.profile-placeholder': 'Perkenalan Singkat tentang Diri Anda',
'app.settings.basic.country': 'Negara/Wilayah',
'app.settings.basic.country-message': 'Tolong masukkan negara anda!',
'app.settings.basic.geographic': 'Provinsi atau kota',
'app.settings.basic.geographic-message': 'Tolong masukkan info geografis anda!',
'app.settings.basic.address': 'Alamat Jalan',
'app.settings.basic.address-message': 'Tolong masukkan Alamat Jalan anda!',
'app.settings.basic.phone': 'Nomor Ponsel',
'app.settings.basic.phone-message': 'Tolong masukkan Nomor Ponsel anda!',
'app.settings.basic.update': 'Perbarui Informasi',
'app.settings.security.strong': 'Kuat',
'app.settings.security.medium': 'Sedang',
'app.settings.security.weak': 'Lemah',
'app.settings.security.password': 'Kata Sandi Akun',
'app.settings.security.password-description': 'Kekuatan Kata Sandi saat ini',
'app.settings.security.phone': 'Keamanan Ponsel',
'app.settings.security.phone-description': 'Mengikat Ponsel',
'app.settings.security.question': 'Pertanyaan Keamanan',
'app.settings.security.question-description':
'Pertanyaan Keamanan belum diatur, dan kebijakan keamanan dapat melindungi akun secara efektif',
'app.settings.security.email': 'Email Cadangan',
'app.settings.security.email-description': 'Mengikat Email',
'app.settings.security.mfa': 'Perangka MFA',
'app.settings.security.mfa-description':
'Tidak mengikat Perangkat MFA, setelah diikat, dapat dikonfirmasi dua kali',
'app.settings.security.modify': 'Modifikasi',
'app.settings.security.set': 'Setel',
'app.settings.security.bind': 'Ikat',
'app.settings.binding.taobao': 'Mengikat Taobao',
'app.settings.binding.taobao-description': 'Tidak mengikat akun Taobao saat ini',
'app.settings.binding.alipay': 'Mengikat Alipay',
'app.settings.binding.alipay-description': 'Tidak mengikat akun Alipay saat ini',
'app.settings.binding.dingding': 'Mengikat DingTalk',
'app.settings.binding.dingding-description': 'Tidak mengikat akun DingTalk',
'app.settings.binding.bind': 'Ikat',
'app.settings.notification.password': 'Kata Sandi Akun',
'app.settings.notification.password-description':
'Pesan dari pengguna lain akan diberitahu dalam bentuk surat',
'app.settings.notification.messages': 'Pesan Sistem',
'app.settings.notification.messages-description':
'Pesan sistem akan diberitahu dalam bentuk surat',
'app.settings.notification.todo': 'Notifikasi daftar To-do',
'app.settings.notification.todo-description':
'Daftar to-do akan diberitahukan dalam bentuk surat dari stasiun',
'app.settings.open': 'Buka',
'app.settings.close': 'Tutup',
};

View File

@ -0,0 +1,24 @@
import globalHeader from './ja-JP/globalHeader';
import menu from './ja-JP/menu';
import settingDrawer from './ja-JP/settingDrawer';
import settings from './ja-JP/settings';
import pwa from './ja-JP/pwa';
import component from './ja-JP/component';
import pages from './ja-JP/pages';
export default {
'navBar.lang': '言語',
'layout.user.link.help': 'ヘルプ',
'layout.user.link.privacy': 'プライバシー',
'layout.user.link.terms': '利用規約',
'app.preview.down.block': 'このページをローカルプロジェクトにダウンロードしてください',
'app.welcome.link.fetch-blocks': '',
'app.welcome.link.block-list': '',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': '展開',
'component.tagSelect.collapse': '折りたたむ',
'component.tagSelect.all': 'すべて',
};

View File

@ -0,0 +1,17 @@
export default {
'component.globalHeader.search': '検索',
'component.globalHeader.search.example1': '検索例1',
'component.globalHeader.search.example2': '検索例2',
'component.globalHeader.search.example3': '検索例3',
'component.globalHeader.help': 'ヘルプ',
'component.globalHeader.notification': '通知',
'component.globalHeader.notification.empty': 'すべての通知を表示しました。',
'component.globalHeader.message': 'メッセージ',
'component.globalHeader.message.empty': 'すべてのメッセージを表示しました。',
'component.globalHeader.event': 'イベント',
'component.globalHeader.event.empty': 'すべてのイベントを表示しました。',
'component.noticeIcon.clear': 'クリア',
'component.noticeIcon.cleared': 'クリア済み',
'component.noticeIcon.empty': '通知なし',
'component.noticeIcon.view-more': 'もっと見る',
};

View File

@ -0,0 +1,52 @@
export default {
'menu.welcome': 'ようこそ',
'menu.more-blocks': 'その他のブロック',
'menu.home': 'ホーム',
'menu.admin': '管理者',
'menu.admin.sub-page': 'サブページ',
'menu.login': 'ログイン',
'menu.register': '登録',
'menu.register.result': '登録結果',
'menu.dashboard': 'ダッシュボード',
'menu.dashboard.analysis': '分析',
'menu.dashboard.monitor': 'モニター',
'menu.dashboard.workplace': '職場',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'フォーム',
'menu.form.basic-form': '基本フォーム',
'menu.form.step-form': 'ステップフォーム',
'menu.form.step-form.info': 'ステップフォーム(転送情報の書き込み)',
'menu.form.step-form.confirm': 'ステップフォーム(転送情報の確認)',
'menu.form.step-form.result': 'ステップフォーム(完成)',
'menu.form.advanced-form': '高度なフォーム',
'menu.list': 'リスト',
'menu.list.table-list': '検索テーブル',
'menu.list.basic-list': '基本リスト',
'menu.list.card-list': 'カードリスト',
'menu.list.search-list': '検索リスト',
'menu.list.search-list.articles': '検索リスト(記事)',
'menu.list.search-list.projects': '検索リスト(プロジェクト)',
'menu.list.search-list.applications': '検索リスト(アプリ)',
'menu.profile': 'プロフィール',
'menu.profile.basic': '基本プロフィール',
'menu.profile.advanced': '高度なプロフィール',
'menu.result': '結果',
'menu.result.success': '成功',
'menu.result.fail': '失敗',
'menu.exception': '例外',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'トリガー',
'menu.account': 'アカウント',
'menu.account.center': 'アカウントセンター',
'menu.account.settings': 'アカウント設定',
'menu.account.trigger': 'トリガーエラー',
'menu.account.logout': 'ログアウト',
'menu.editor': 'グラフィックエディタ',
'menu.editor.flow': 'フローエディタ',
'menu.editor.mind': 'マインドエディター',
'menu.editor.koni': 'コニエディター',
};

View File

@ -0,0 +1,67 @@
export default {
'pages.layouts.userLayout.title': 'Ant Designは、西湖区で最も影響力のあるWebデザイン仕様です。',
'pages.login.accountLogin.tab': 'アカウントログイン',
'pages.login.accountLogin.errorMessage':
'ユーザー名/パスワードが正しくありませんadmin/ant.design)',
'pages.login.username.placeholder': 'ユーザー名adminまたはuser',
'pages.login.username.required': 'ユーザー名を入力してください!',
'pages.login.password.placeholder': 'パスワードant.design',
'pages.login.password.required': 'パスワードを入力してください!',
'pages.login.phoneLogin.tab': '電話ログイン',
'pages.login.phoneLogin.errorMessage': '検証コードエラー',
'pages.login.phoneNumber.placeholder': '電話番号',
'pages.login.phoneNumber.required': '電話番号を入力してください!',
'pages.login.phoneNumber.invalid': '電話番号が無効です!',
'pages.login.captcha.placeholder': '確認コード',
'pages.login.captcha.required': '確認コードを入力してください!',
'pages.login.phoneLogin.getVerificationCode': '確認コードを取得',
'pages.getCaptchaSecondText': '秒',
'pages.login.rememberMe': 'Remember me',
'pages.login.forgotPassword': 'パスワードをお忘れですか?',
'pages.login.submit': 'ログイン',
'pages.login.loginWith': 'その他のログイン方法:',
'pages.login.registerAccount': 'アカウント登録',
'pages.welcome.advancedComponent': '高度なコンポーネント',
'pages.welcome.link': 'ようこそ',
'pages.welcome.advancedLayout': '高度なレイアウト',
'pages.welcome.alertMessage': 'より高速で強力な頑丈なコンポーネントがリリースされました。',
'pages.admin.subPage.title': 'このページは管理者のみが表示できます',
'pages.admin.subPage.alertMessage':
'Umi uiがリリースされました。npm run uiを使用して体験してください。',
'pages.searchTable.createForm.newRule': '新しいルール',
'pages.searchTable.updateForm.ruleConfig': 'ルール構成',
'pages.searchTable.updateForm.basicConfig': '基本情報',
'pages.searchTable.updateForm.ruleName.nameLabel': 'ルール名',
'pages.searchTable.updateForm.ruleName.nameRules': 'ルール名を入力してください!',
'pages.searchTable.updateForm.ruleDesc.descLabel': 'ルールの説明',
'pages.searchTable.updateForm.ruleDesc.descPlaceholder': '5文字以上入力してください',
'pages.searchTable.updateForm.ruleDesc.descRules': '5文字以上のルールの説明を入力してください',
'pages.searchTable.updateForm.ruleProps.title': 'プロパティの構成',
'pages.searchTable.updateForm.object': '監視対象',
'pages.searchTable.updateForm.ruleProps.templateLabel': 'ルールテンプレート',
'pages.searchTable.updateForm.ruleProps.typeLabel': 'ルールタイプ',
'pages.searchTable.updateForm.schedulingPeriod.title': 'スケジュール期間の設定',
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': '開始時間',
'pages.searchTable.updateForm.schedulingPeriod.timeRules': '開始時間を選択してください!',
'pages.searchTable.titleDesc': '説明',
'pages.searchTable.ruleName': 'ルール名が必要です',
'pages.searchTable.titleCallNo': 'サービスコール数',
'pages.searchTable.titleStatus': 'ステータス',
'pages.searchTable.nameStatus.default': 'デフォルト',
'pages.searchTable.nameStatus.running': '起動中',
'pages.searchTable.nameStatus.online': 'オンライン',
'pages.searchTable.nameStatus.abnormal': '異常',
'pages.searchTable.titleUpdatedAt': '最終スケジュール',
'pages.searchTable.exception': '例外の理由を入力してください!',
'pages.searchTable.titleOption': 'オプション',
'pages.searchTable.config': '構成',
'pages.searchTable.subscribeAlert': 'アラートを購読する',
'pages.searchTable.title': 'お問い合わせフォーム',
'pages.searchTable.new': '新しい',
'pages.searchTable.chosen': '選んだ項目',
'pages.searchTable.item': '項目',
'pages.searchTable.totalServiceCalls': 'サービスコールの総数',
'pages.searchTable.tenThousand': '万',
'pages.searchTable.batchDeletion': 'バッチ削除',
'pages.searchTable.batchApproval': 'バッチ承認',
};

View File

@ -0,0 +1,7 @@
export default {
'app.pwa.offline': 'あなたは今オフラインです',
'app.pwa.serviceworker.updated': '新しいコンテンツが利用可能です',
'app.pwa.serviceworker.updated.hint':
'現在のページをリロードするには、「更新」ボタンを押してください',
'app.pwa.serviceworker.updated.ok': 'リフレッシュ',
};

View File

@ -0,0 +1,31 @@
export default {
'app.setting.pagestyle': 'ページスタイル設定',
'app.setting.pagestyle.dark': 'ダークスタイル',
'app.setting.pagestyle.light': 'ライトスタイル',
'app.setting.content-width': 'コンテンツの幅',
'app.setting.content-width.fixed': '固定',
'app.setting.content-width.fluid': '流体',
'app.setting.themecolor': 'テーマカラー',
'app.setting.themecolor.dust': 'ダストレッド',
'app.setting.themecolor.volcano': 'ボルケ-',
'app.setting.themecolor.sunset': 'サンセットオレンジ',
'app.setting.themecolor.cyan': 'シアン',
'app.setting.themecolor.green': 'ポーラーグリーン',
'app.setting.themecolor.daybreak': '夜明けの青(デフォルト)',
'app.setting.themecolor.geekblue': 'ギーク ブルー',
'app.setting.themecolor.purple': 'ゴールデンパープル',
'app.setting.navigationmode': 'ナビゲーションモード',
'app.setting.sidemenu': 'サイドメニューのレイアウト',
'app.setting.topmenu': 'トップメニューのレイアウト',
'app.setting.fixedheader': '固定ヘッダー',
'app.setting.fixedsidebar': '固定サイドバー',
'app.setting.fixedsidebar.hint': 'サイドメニューのレイアウトで動作します',
'app.setting.hideheader': 'スクロール時の非表示ヘッダー',
'app.setting.hideheader.hint': '非表示ヘッダーが有効になっている場合に機能します',
'app.setting.othersettings': 'その他の設定',
'app.setting.weakmode': 'ウィークモード',
'app.setting.copy': 'コピー設定',
'app.setting.copyinfo':
'コピーが成功しました。src/models/setting.jsのdefaultSettingsを置き換えてください',
'app.setting.production.hint': '設定パネルは開発環境でのみ表示されます。手動で変更してください',
};

View File

@ -0,0 +1,59 @@
export default {
'app.settings.menuMap.basic': '基本設定',
'app.settings.menuMap.security': 'セキュリティ設定',
'app.settings.menuMap.binding': 'アカウントのバインド',
'app.settings.menuMap.notification': '新しいメッセージの通知',
'app.settings.basic.avatar': 'アバター',
'app.settings.basic.change-avatar': 'アバターを変更する',
'app.settings.basic.email': 'メール',
'app.settings.basic.email-message': 'メールアドレスを入力してください!',
'app.settings.basic.nickname': 'ニックネーム',
'app.settings.basic.nickname-message': 'ニックネームを入力してください!',
'app.settings.basic.profile': '個人プロフィール',
'app.settings.basic.profile-message': '個人プロフィールを入力してください!',
'app.settings.basic.profile-placeholder': '自己紹介',
'app.settings.basic.country': '国/地域',
'app.settings.basic.country-message': 'あなたの国を入力してください!',
'app.settings.basic.geographic': '州または市',
'app.settings.basic.geographic-message': '地理情報を入力してください!',
'app.settings.basic.address': '住所',
'app.settings.basic.address-message': '住所を入力してください!',
'app.settings.basic.phone': '電話番号',
'app.settings.basic.phone-message': '電話番号を入力してください!',
'app.settings.basic.update': '更新情報',
'app.settings.security.strong': '強い',
'app.settings.security.medium': 'ミディアム',
'app.settings.security.weak': '弱い',
'app.settings.security.password': 'アカウントパスワード',
'app.settings.security.password-description': '現在のパスワードの強度',
'app.settings.security.phone': 'セキュリティ電話番号',
'app.settings.security.phone-description': 'バインドされた電話番号',
'app.settings.security.question': '秘密の質問',
'app.settings.security.question-description':
'セキュリティの質問が設定されてません。セキュリティポリシーはアカウントのセキュリティを効果的に保護できます',
'app.settings.security.email': 'バックアップメール',
'app.settings.security.email-description': 'バインドされたメール',
'app.settings.security.mfa': '多要素認証デバイス',
'app.settings.security.mfa-description':
'バインドされていない多要素認証デバイスは、バインド後、2回確認できます',
'app.settings.security.modify': '変更する',
'app.settings.security.set': 'セットする',
'app.settings.security.bind': 'バインド',
'app.settings.binding.taobao': 'タオバオをバインドする',
'app.settings.binding.taobao-description': '現在バインドされていないタオバオアカウント',
'app.settings.binding.alipay': 'アリペイをバインドする',
'app.settings.binding.alipay-description': '現在バインドされていないアリペイアカウント',
'app.settings.binding.dingding': 'ディントークをバインドする',
'app.settings.binding.dingding-description': '現在バインドされていないディントークアカウント',
'app.settings.binding.bind': 'バインド',
'app.settings.notification.password': 'アカウントパスワード',
'app.settings.notification.password-description':
'他のユーザーからのメッセージは、ステーションレターの形式で通知されます',
'app.settings.notification.messages': 'システムメッセージ',
'app.settings.notification.messages-description':
'システムメッセージは、ステーションレターの形式で通知されます',
'app.settings.notification.todo': 'To Do用事) 通知',
'app.settings.notification.todo-description': 'To Doタスクは、内部レターの形式で通知されます',
'app.settings.open': '開く',
'app.settings.close': '閉じる',
};

Some files were not shown because too many files have changed in this diff Show More